Compare commits

..

3 Commits

Author SHA1 Message Date
Sriram Hariharan
a642d0d9d2 Merge pull request #46 from DereC4/a-fun-branch 2023-09-17 14:31:01 -05:00
Derek Chen
17e0ac9465 Update config.js 2023-04-02 23:50:22 -05:00
Derek Chen
8cdd04f0b4 Update config.js 2023-04-02 23:48:29 -05:00
284 changed files with 13594 additions and 26252 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
indent_style = space

View File

@@ -1,96 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories and management
node_modules/
jspm_packages/
package.json
package-lock.json
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# next.js build output
.next
# Webpack-built output
/dist
# Extension archives
/build
# VsCode
.vscode/*
!.vscode/launch.json
!.vscode/tasks.json
# macOS
.DS_Store
# Terraform
.terraform
# development dependencies
.dev/vue-devtools
.dev/browser-profiles
# IntelliJ
.idea
# Sylelint IntelliJ Plugin Requirement
.stylelintrc.json
# Local environment settings
.env.local
*.svg
config
.eslintrc.js
!.storybook

View File

@@ -1,217 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
webextensions: true,
},
ignorePatterns: ['*.html', 'tsconfig.json'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'airbnb-base',
'airbnb/rules/react',
'airbnb-typescript',
'@unocss',
'prettier',
],
plugins: [
'import',
'import-essentials',
'jsdoc',
'react-prefer-function-component',
'@typescript-eslint',
'simple-import-sort',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
debugger: true,
browser: true,
context: true,
JSX: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true,
},
},
settings: {
react: {
version: 'detect',
},
jsdoc: {
mode: 'typescript',
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
},
rules: {
'prefer-const': [
'off',
{
destructuring: 'any',
ignoreReadBeforeAssign: false,
},
],
'no-plusplus': 'off',
'no-inner-declarations': 'off',
'sort-imports': 'off',
'no-case-declarations': 'off',
'no-unreachable': 'warn',
'no-constant-condition': 'error',
'space-before-function-paren': 'off',
'no-undef': 'off',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/no-shadow': ['off'],
'@typescript-eslint/no-use-before-define': ['off'],
'class-methods-use-this': 'off',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/lines-between-class-members': 'off',
'no-param-reassign': [
'error',
{
props: false,
},
],
'no-console': 'off',
'consistent-return': 'off',
'react/destructuring-assignment': 'off',
'import/prefer-default-export': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'keyword-spacing': [
'error',
{
before: true,
after: true,
},
],
'no-continue': 'off',
'space-before-blocks': [
'error',
{
functions: 'always',
keywords: 'always',
classes: 'always',
},
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.tsx'],
},
],
'react/no-deprecated': 'warn',
'react/prop-types': 'off',
'react-prefer-function-component/react-prefer-function-component': [
'warn',
{
allowComponentDidCatch: false,
},
],
'react/function-component-definition': 'off',
'react/button-has-type': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/newline-after-description': 'off',
'react/require-default-props': 'off',
'jsdoc/require-jsdoc': [
'warn',
{
enableFixer: false,
publicOnly: true,
checkConstructors: false,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionExpression: true,
},
contexts: [
'MethodDefinition:not([key.name="componentDidMount"]):not([key.name="render"])',
'ArrowFunctionExpression',
'ClassDeclaration',
'ClassExpression',
'ClassProperty:not([key.name="state"]):not([key.name="componentDidMount"])',
'FunctionDeclaration',
'FunctionExpression',
'TSDeclareFunction',
'TSEnumDeclaration',
'TSInterfaceDeclaration',
'TSMethodSignature',
'TSModuleDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'warn',
'import/no-restricted-paths': [
'error',
{
zones: [
{
target: './src/background',
from: './src/views',
message:
'You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!',
},
{
target: './src/views',
from: './src/background',
message:
'You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!',
},
{
target: './src/shared',
from: './',
except: ['./src/shared', './node_modules'],
message: 'You cannot import into `shared` from an external directory.',
},
],
},
],
'import/extensions': 'off',
'no-restricted-syntax': [
'error',
'ForInStatement',
'LabeledStatement',
'WithStatement',
{
selector: 'TSEnumDeclaration',
message: "Don't declare enums",
},
],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-essentials/restrict-import-depth': 'error',
'import-essentials/check-path-alias': 'error',
},
};

View File

@@ -1,43 +0,0 @@
name: Best Practices
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run ESLint
run: pnpm run lint
format:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run Prettier
run: pnpm run prettier

View File

@@ -1,24 +0,0 @@
name: Type Check
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm run check-types

View File

@@ -1,26 +0,0 @@
name: 'Chromatic'
on: [push, pull_request]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: chpt_e8bd07b0b27d8eb
exitZeroOnChanges: true
autoAcceptChanges: 'main'

View File

@@ -1,25 +0,0 @@
name: Create Release
on:
push:
branches:
- production
- preview
jobs:
build:
name: build extension & create release
runs-on: ubuntu-latest
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@master
- name: Get file permission
run: chmod -R 777 .
- name: Install dependencies
run: npm ci
- name: Release with semantic-release
id: semantic-release
run: npx --no-install semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,24 +0,0 @@
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test

View File

@@ -1,43 +0,0 @@
name: Validate PR Title
# thank you ben limmer for this workflow:
# https://github.com/blimmer/semantic-release-demo-2/blob/main/.github/workflows/lint-pr.yml
on:
pull_request_target:
types:
- opened
- reopened
- edited
- synchronize
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: amannn/action-semantic-pull-request@v3.2.6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post Conventional Commit Comment (on failure)
uses: jungwinter/comment@v1
id: conventional-commit-help
with:
type: create
issue_number: ${{ github.event.pull_request.number }}
token: ${{ secrets.GITHUB_TOKEN }}
body: |
Your pull request title did not conform to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standards. Our upcoming automated release pipeline will automatically determine
the proper release version based on your pull request title.
**Cheat Sheet**
- feat: A new feature
- fix: A bug fix
- docs: Documentation only changes
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- refactor: A code change that neither fixes a bug nor adds a feature
- perf: A code change that improves performance
- test: Adding missing tests or correcting existing tests
- build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- chore: Other changes that don't modify src or test files
- revert: Reverts a previous commit
if: ${{ failure() }}

212
.gitignore vendored
View File

@@ -1,212 +0,0 @@
# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,node,react,storybookjs
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
### react ###
.DS_*
**/*.backup.*
**/*.back.*
node_modules
*.sublime*
psd
thumb
sketch
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
package-lock.json
storybook-static/

View File

@@ -1 +0,0 @@
npx --no -- commitlint --edit $1

1
.nvmrc
View File

@@ -1 +0,0 @@
v18.12.1

View File

@@ -1,19 +0,0 @@
*.css
# macOS
.DS_Store
# Webpack-built output
/dist
# Extension archives
/build
# Optional npm cache directory
.npm
# Dependency directories and management
node_modules/
jspm_packages/
package.json
package-lock.json
# Coverage directory used by tools like istanbul
coverage

View File

@@ -1,12 +0,0 @@
{
"useTabs": false,
"printWidth": 120,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "avoid",
"bracketSpacing": true,
"bracketSameLine": false,
"semi": true,
"jsxSingleQuote": true
}

View File

@@ -1,37 +0,0 @@
{
"branches": [
"production",
{
"name": "preview",
"channel": "alpha",
"prerelease": "alpha"
}
],
"plugins": [
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/exec",
{
"prepareCmd": "SEMANTIC_VERSION=${nextRelease.version} npm run build"
}
],
[
"@semantic-release/github",
{
"assets": "build/**/artifacts/*.*",
"failComment": false
}
]
]
}

View File

@@ -1,24 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-designs',
'@storybook/test',
'@chromatic-com/storybook',
],
framework: {
name: '@storybook/react-vite',
options: {
builder: {
viteConfigPath: '.storybook/vite-storybook.config.ts',
},
},
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -1,178 +0,0 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Preview } from '@storybook/react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
decorators: [
Story => (
<React.StrictMode>
<ExtensionRoot>
<Story />
</ExtensionRoot>
</React.StrictMode>
),
],
};
let localData = {};
type ListenerFunction = (
changes: { [key: string]: chrome.storage.StorageChange },
areaName: chrome.storage.AreaName
) => void;
const localDataListeners = new Map<
ListenerFunction, // key to remove listener
(changes: { [key: string]: chrome.storage.StorageChange }) => void
>();
// mock chrome api
globalThis.chrome = {
storage: {
local: {
/**
* Removes all items from storage.
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async clear() {
localData = {};
},
/**
* Gets one or more items from storage.
* @param keys A single key to get, list of keys to get, or a dictionary specifying default values.
* An empty list or object will return an empty result object. Pass in null to get the entire contents of storage.
* @return A Promise that resolves with an object containing items
*/
async get(keys?: string | string[] | { [key: string]: any } | null) {
if (keys === null) {
return localData;
}
if (Array.isArray(keys)) {
return keys.reduce((acc, key) => {
acc[key] = localData[key];
return acc;
}, {} as string); // funny types
}
if (typeof keys === 'string') {
return { [keys]: localData[keys] };
}
return keys;
},
/**
* Gets the amount of space (in bytes) being used by one or more items.
* @param keys Optional. A single key or list of keys to get the total usage for. An empty list will return 0. Pass in null to get the total usage of all of storage.
* @param callback Callback with the amount of space being used by storage, or on failure (in which case runtime.lastError will be set).
* Parameter bytesInUse: Amount of space being used in storage, in bytes.
*/
async getBytesInUse() {
return 0;
},
/**
* Removes one or more items from storage.
* @param keys A single key or a list of keys for items to remove.
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async remove(keys: string | string[]) {
if (Array.isArray(keys)) {
keys.forEach(key => {
for (const listener of localDataListeners.values()) {
listener({ [key]: { oldValue: localData[key], newValue: undefined } });
}
delete localData[key];
});
} else {
for (const listener of localDataListeners.values()) {
listener({ [keys]: { oldValue: localData[keys], newValue: undefined } });
}
delete localData[keys];
}
},
/**
* Sets multiple items.
* @param items An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.
* Primitive values such as numbers will serialize as expected. Values with a typeof "object" and "function" will typically serialize to {}, with the exception of Array (serializes as expected), Date, and Regex (serialize using their String representation).
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async set(items: { [key: string]: any }) {
for (const key in items) {
const oldValue = localData[key];
localData[key] = JSON.parse(JSON.stringify(items[key]));
for (const listener of localDataListeners.values()) {
listener({ [key]: { oldValue: oldValue, newValue: localData[key] } });
}
}
},
},
onChanged: {
/**
* Registers an event listener callback to an event.
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
*/
addListener(
listener: (
changes: { [key: string]: chrome.storage.StorageChange },
areaName: chrome.storage.AreaName
) => void
) {
localDataListeners.set(listener, (changes: { [key: string]: chrome.storage.StorageChange }) => {
listener(changes, 'local');
});
},
/**
* Deregisters an event listener callback from an event.
* @param callback Listener that shall be unregistered.
*/
removeListener(listener: ListenerFunction) {
localDataListeners.delete(listener);
},
},
},
runtime: {
id: 'fake-id',
getManifest(): chrome.runtime.Manifest {
return {
manifest_version: 3,
name: 'fake-name',
version: '0.0.0',
};
},
onMessage: {
/**
* Registers an event listener callback to an event.
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
*/
addListener<T extends Function>(callback: T) {},
/**
* Deregisters an event listener callback from an event.
* @param callback Listener that shall be unregistered.
*/
removeListener<T extends Function>(callback: T) {},
},
},
} as typeof chrome;
// set updatedAt dates to be fixed
UserScheduleStore.get('schedules').then(schedules => {
schedules.forEach(schedule => {
schedule.updatedAt = new Date('2024-01-01 12:00').getTime();
});
UserScheduleStore.set('schedules', schedules);
});
export default preview;

View File

@@ -1,28 +0,0 @@
import react from '@vitejs/plugin-react-swc';
import { resolve } from 'path';
import UnoCSS from 'unocss/vite';
import Icons from 'unplugin-icons/vite';
import { defineConfig } from 'vite';
const root = resolve(__dirname, '../src');
const pagesDir = resolve(root, 'pages');
const assetsDir = resolve(root, 'assets');
const publicDir = resolve(__dirname, '../public');
console.log(root);
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), UnoCSS(), Icons({ compiler: 'jsx', jsx: 'react' })],
resolve: {
alias: {
src: root,
'@assets': assetsDir,
'@pages': pagesDir,
'@public': publicDir,
'@shared': resolve(root, 'shared'),
'@background': resolve(pagesDir, 'background'),
'@views': resolve(root, 'views'),
},
},
});

View File

@@ -1,9 +0,0 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"antfu.unocss",
"editorconfig.editorconfig",
"figma.figma-vscode-extension"
]
}

18
.vscode/launch.json vendored
View File

@@ -1,18 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Run current script",
"runtimeExecutable": "npx",
"runtimeArgs": [
"tsx"
],
"program": "${file}",
"skipFiles": [
"<node_internals>/**"
],
}
]
}

40
.vscode/settings.json vendored
View File

@@ -1,40 +0,0 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[svg]": {
"editor.defaultFormatter": "jock.svg"
},
"material-icon-theme.activeIconPack": "react",
"material-icon-theme.folders.associations": {
"analytics": "Json",
"background": "Delta",
"navigation": "Routes",
"logging": "log",
"popup": "Layout",
"storage": "Database",
},
"material-icon-theme.files.associations": {
"tsconfig.extension.json": "tsconfig",
"tsconfig.build.json": "tsconfig",
"tsconfig.test.json": "tsconfig"
},
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib",
}

10
@types/vite-env.d.ts vendored
View File

@@ -1,10 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PACKAGE_VERSION: string;
readonly VITE_BETA_BUILD?: 'true';
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2023 Sriram Hariharan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,59 +1,40 @@
# UT Registration Plus
We've all been there. 20 tabs of Rate My Professor, Google Spreadsheet, and the UT Course Schedule open and you still don't know what classes to take.
This extension, UT Registration Plus (UTRP), tries to streamline most of the unnecessary steps and headaches of registering for classes at UT Austin.
# UT Registration Plus
## (or Sriram's Sexy Scheduling Script)
[Try it for yourself on the Chrome Web Store](https://chrome.google.com/webstore/detail/hboadpjkoaieogjimneceaahlppnipaa)
- For each class in the UT Course Schedule site, UTRP provides a "breakdown" popup, with quick and easy links to the instructor's RateMyProfessor, Course Evaluation Survey (CES) and past syllabi.
- Shows the course description with highlighted information on prerequisites, restrictions, etc.
We've all been there. 20 tabs of Rate My Professor, Catalyst, and the UT Course Catalog open and you still don't know what classes to take.
This extension tries to streamline most of the unnecessary steps and headaches of registering for classes at UT Austin.
- Shows an aggregate and semesterly graph of the grade distributions for each course.
- Gives you the ability to add "Add Course" and view them in the extension popup, a quick list of all the courses you have saved and an easy way to copy unique numbers.
- For each class on the UT Course Catalog it provides a "breakdown" popup, with quick and easy links to the RateMyProfessor and eCIS pages of the professor, as well as syllabi from when the professor taught the class in the past.
- Highlights and crosses-out what courses on the UT Course Catalog would conflict with your currently saved courses, making selecting courses that fit with your schedule so much easier.
- Gets the course description and highlight the important information like prerequisites, restrictions, etc.
- Display's a weekly schedule based on your saved courses.
- Shows an aggregate graph of the grade distributions for when the professor taught the class in the past.
- Give you the ability to create multiple schedules to plan for different scenarios.
- Gives you the ability to "Save Courses" and view them in the extension popup. This lets you see any schedule conflicts, and makes copy-pasting the unique code much easier when you're actually registering.
- ... and much more!
- Highlights and crosses-out what courses on the UT Course Catalog would conflict with your currently saved courses, making selecting courses that fit with your schedule so much easier.
## Toolchain
- Display's a weekly schedule based on your saved courses
- React 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Storybook
- Semantic-Release
- Custom Messaging & Storage Wrappers
<p align="center">
<img src="https://lh3.googleusercontent.com/X5hqHGPU-F2lF3_shT2injxd40eFYXLJfZVxpU1v2w1YvFRW1jQMEXu2yzWHKKpqn5huJL-NEHY=w640-h400-e365">
</p>
<p align="center">
<img src="https://lh3.googleusercontent.com/ZCRxTFKFjpGm5ZRMv2iHzMqdnrQHUx_Ih_XhGhy2O4Yn29YccvU5yXXrWXKuVKsNAmEJJ0As4xc=w640-h400-e365">
</p>
<p align="center">
<img src="https://lh3.googleusercontent.com/3iRi25wDnVqgzc7pnYUXQq1TvdPpAeDjCmIF9hLU-WKmlchEYQUh_xU-XV00fEbKUr2XVKGkOw=w640-h400-e365">
</p>
<p align="center">
<img src="https://lh3.googleusercontent.com/x95blI5D1mseNPLOtHETlLmoVtHm0eeye9uyeWSDd5W6m6fSoZxMMMyQTGUFo5swoTgRivGVyw=w640-h400-e365">
</p>
<p align="center">
<img src="https://lh3.googleusercontent.com/bbey8OGOTtJWUaHGVIU5wewbWg6X6s-gjD15RwXHhvgH_9kax2mE4bcrjem_iZGH-q5z6NT7g94=w640-h400-e365">
</p>
## Development: Getting Started
# 2.0 coming soon....
1. Clone this repo
2. This project uses `pnpm` to manage and patch dependencies. Run `pnpm install` to configure the repository for building/development
3. Using either of the methods listed below, the extension will build to the `dist/` directory.
### Development Builds
- Run `pnpm dev`
> [!NOTE]
> Injected content such as extension content on UT pages is not properly styled, and are missing class stylings. When developing for these pages, use `NODE_ENV='development' pnpm run dev build --mode development -w` to build and watch for changes. This will ensure you are seeing an accurate representation of the extension.
### Production Builds
- Run `pnpm build`
<details>
<summary>Beta builds</summary>
Use `BETA=true pnpm build` to build a beta build.
</details>
## Development: Loading the Extension Manually
Open [chrome://extensions](chrome://extensions), ensure you have 'Developer Mode' enabled, and click 'Load unpacked'.
Navigate to the `dist/` folder, and click 'select' to import the extension.

42
calendar.html Normal file
View File

@@ -0,0 +1,42 @@
<!DOCTYPE html>
<!-- This file is the html file serving the "My Schedule / Calendar" page. -->
<head>
<link rel='stylesheet' href='css/fullcalendar.min.css' />
<link rel='stylesheet' href='css/_materialFullCalendar.css' />
<script src='js/lib/jquery-3.3.1.min.js'></script>
<script src='js/lib/moment.min.js'></script>
<script src='js/lib/fullcalendar.min.js'></script>
</head>
<body>
<div style='display:flex'>
<div id='calendar' style="flex-grow: 1"></div>
<div class="card" id="header"
style="text-align:center;margin:5px 0px 0px 15px;display: inline-table;padding-bottom: 5px;">
<h1 id='hours'
style="font-size:30px;font-weight:500; border-bottom: 3px solid black;display: inline-block;padding-bottom: 5px;margin-bottom: 5px;">
0
hours</h1>
<h1 id='num' style="font-size:20px;font-weight:500; margin: 2px;">0
Courses</h1>
<br>
<div style="margin:5px;display: flex;flex-direction: column;">
<button id="clear" class="matbut"
style="font-size:medium; background:#4CAF50;margin: 10px;white-space: nowrap;text-align: center;">Clear
All</button>
<button id="save" class="matbut"
style="font-size:medium; background:#FF9800;margin: 10px;white-space: nowrap;text-align: center;">Save
as PNG</button>
<button id="export" class="matbut"
style="font-size:medium; background:#FF0000;margin: 10px;white-space: nowrap;text-align: center;">Export
Cal</button>
</div>
</div>
</div>
</body>
<script src='js/config.js'></script>
<script src='js/lib/html2canvas.min.js'></script>
<script src='js/lib/ics.min.js'></script>
<script src='js/Template.js'></script>
<script src='js/util.js'></script>
<script src='js/calendar.js'></script>

View File

@@ -1,5 +0,0 @@
{
"onlyChanged": true,
"projectId": "Project:65c5172964f36dcf207985bf",
"zip": true
}

View File

@@ -1,123 +0,0 @@
import { RuleConfigCondition, RuleConfigSeverity, TargetCaseType } from '@commitlint/types';
export default {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'body-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'body-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'footer-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'footer-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'header-max-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'header-trim': [RuleConfigSeverity.Error, 'always'] as const,
'subject-case': [
RuleConfigSeverity.Error,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
] as [RuleConfigSeverity, RuleConfigCondition, TargetCaseType[]],
'subject-empty': [RuleConfigSeverity.Error, 'never'] as const,
'subject-full-stop': [RuleConfigSeverity.Error, 'never', '.'] as const,
'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'] as const,
'type-empty': [RuleConfigSeverity.Error, 'never'] as const,
'type-enum': [
RuleConfigSeverity.Error,
'always',
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
] as [RuleConfigSeverity, RuleConfigCondition, string[]],
},
prompt: {
questions: {
type: {
description: "Select the type of change that you're committing",
enum: {
feat: {
description: 'A new feature',
title: 'Features',
emoji: '✨',
},
fix: {
description: 'A bug fix',
title: 'Bug Fixes',
emoji: '🐛',
},
docs: {
description: 'Documentation only changes',
title: 'Documentation',
emoji: '📚',
},
style: {
description:
'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
title: 'Styles',
emoji: '💎',
},
refactor: {
description: 'A code change that neither fixes a bug nor adds a feature',
title: 'Code Refactoring',
emoji: '📦',
},
perf: {
description: 'A code change that improves performance',
title: 'Performance Improvements',
emoji: '🚀',
},
test: {
description: 'Adding missing tests or correcting existing tests',
title: 'Tests',
emoji: '🚨',
},
build: {
description:
'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
title: 'Builds',
emoji: '🛠',
},
ci: {
description:
'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
title: 'Continuous Integrations',
emoji: '⚙️',
},
chore: {
description: "Other changes that don't modify src or test files",
title: 'Chores',
emoji: '♻️',
},
revert: {
description: 'Reverts a previous commit',
title: 'Reverts',
emoji: '🗑',
},
},
},
scope: {
description: 'What is the scope of this change (e.g. component or file name)',
},
subject: {
description: 'Write a short, imperative tense description of the change',
},
body: {
description: 'Provide a longer description of the change',
},
isBreaking: {
description: 'Are there any breaking changes?',
},
breakingBody: {
description:
'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
},
breaking: {
description: 'Describe the breaking changes',
},
isIssueAffected: {
description: 'Does this change affect any open issues?',
},
issuesBody: {
description:
'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
},
issues: {
description: 'Add issue references (e.g. "fix #123", "re #123".)',
},
},
},
};

View File

@@ -0,0 +1,344 @@
/*
This is the Material Design theme for FullCalendar Weekly Agenda view
Creation Date: Aug 19th 2015
Author: Jacky Liang
Version: FullCalendar 2.4.0
Tested Using the Following FC Settings:
editable: false,
handleWindowResize: true,
weekends: false, // Hide weekends
defaultView: 'agendaWeek', // Only show week view
header: false, // Hide buttons/titles
minTime: '07:30:00', // Start time for the calendar
maxTime: '22:00:00', // End time for the calendar
columnFormat: {
week: 'ddd' // Only show day of the week names
},
displayEventTime: true,
allDayText: 'Online/TBD'
Note: This has NOT been tested on Monthly or Daily views.
Colors: Use the following - https://www.google.com/design/spec/style/color.html#color-color-palette
at the 700 level. An opacity of 0.65 is automatically applied to the
700 level colors to generate a soft and pleasing look.
Color were applied to each event using the following code:
events.push({
title: 'This is a Material Design event!',
start: 'someStartDate',
end: 'someEndDate',
color: '#C2185B'
});
*/
.fc-state-highlight {
opacity: 0;
border: none;
}
/* Styling for each event from Schedule */
.fc-time-grid-event.fc-v-event.fc-event {
border-radius: 4px;
border: none;
padding: 5px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.3);
transition: 0.3s;
opacity: 1;
}
.html2canvas-container {
width: 3000px !important;
height: 3000px !important;
}
.fc-time-grid-event.fc-v-event.fc-event:hover {
box-shadow: 0 8px 12px 0 rgba(0, 0, 0, 0.3);
}
/* Bolds the name of the event and inherits the font size */
.fc-event {
font-size: small !important;
font-weight: bold !important;
}
/* Remove the header border from Schedule */
.fc td,
.fc th {
border-style: ridge !important;
border-width: 1px !important;
padding: 4px 3px 0px 3px !important;
vertical-align: top !important;
border-left-width: 0;
}
.fc-row fc-widget-header {
border-color: transparent;
}
.fc td {
border-top-width: 0;
padding: 3px !important;
}
.fc-widget-header {
background-color: #cc5500;
color: white;
}
/* Inherits background for each event from Schedule. */
.fc-event .fc-bg {
z-index: 1 !important;
background: inherit !important;
opacity: 0.25 !important;
}
/* Normal font weight for the time in each event */
.fc-time-grid-event .fc-time {
font-weight: normal !important;
}
/* Apply same opacity to all day events */
.fc-ltr .fc-h-event.fc-not-end,
.fc-rtl .fc-h-event.fc-not-start {
opacity: 0.65 !important;
margin-left: 12px !important;
padding: 5px !important;
}
/* Apply same opacity to all day events */
.fc-day-grid-event.fc-h-event.fc-event.fc-not-start.fc-end {
opacity: 0.65 !important;
margin-left: 12px !important;
padding: 5px !important;
}
/* Material design button */
.matbut {
border: none;
outline: none;
cursor: pointer;
color: white;
margin: 10px 10px 10px 0px;
padding: 10px 10px;
border-radius: 10px;
font-size: medium;
font-style: bold;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
}
.matbut {
position: relative;
overflow: hidden;
}
.matbut:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
@keyframes ripple {
0% {
transform: scale(0, 0);
opacity: 1;
}
20% {
transform: scale(25, 25);
opacity: 1;
}
100% {
opacity: 0;
transform: scale(40, 40);
}
}
.matbut:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
.fc-button {
display: inline-block;
position: relative;
cursor: pointer;
min-height: 36px;
min-width: 88px;
line-height: 36px;
vertical-align: middle;
-webkit-box-align: center;
-webkit-align-items: center;
align-items: center;
text-align: center;
border-radius: 2px;
box-sizing: border-box;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
outline: none;
border: 0;
padding: 0 6px;
margin: 6px 8px;
letter-spacing: 0.01em;
background: transparent;
color: currentColor;
white-space: nowrap;
text-transform: uppercase;
font-weight: 500;
font-size: 14px;
font-style: inherit;
font-variant: inherit;
font-family: inherit;
text-decoration: none;
overflow: hidden;
-webkit-transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
transition: box-shadow 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), background-color 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.fc-button:hover {
background-color: rgba(158, 158, 158, 0.2);
}
.fc-button:focus,
.fc-button:hover {
text-decoration: none;
}
/* The active button box is ugly so the active button will have the same appearance of the hover */
.fc-state-active {
background-color: rgba(158, 158, 158, 0.2);
}
/* Not raised button */
.fc-state-default {
box-shadow: None;
}
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 300px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
#classname {
font-weight: bold;
margin-bottom: -10px;
font-size: large;
}
.modal-content {
background-color: #fefefe;
margin: auto;
max-height: 85%;
overflow-y: auto;
padding: 15px;
border: 1px solid #888;
width: 35%;
}
#prof {
font-size: medium;
margin-bottom: -5px;
}
#info {
margin: 10px;
}
body a:link,
body a:visited {
font-weight: bold;
color: #3c87a3;
}
.time {
margin-left: auto;
margin-right: auto;
margin-bottom: 5px;
font-size: medium;
}
.fc td,
.fc th {
border-color: #E7E7E7;
}
.fc-time-grid .fc-slats td {
height: 1.5em;
border-bottom: initial;
border-color: #E7E7E7;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
padding: 5px;
font-size: 28px;
font-weight: bold;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.card {
transition: 0.3s;
margin-bottom: 5px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.card:hover {
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.19);
}
.cardcontainer {
padding: 2px 16px;
display: block;
transition: width 300ms ease-in-out, height 300ms ease-in-out;
}
tbody {
border-width: 0px;
}

5
css/fullcalendar.min.css vendored Normal file

File diff suppressed because one or more lines are too long

81
css/options.css Normal file
View File

@@ -0,0 +1,81 @@
.version {
padding: 0px 5px 5px 0px;
text-align: right;
}
.creator-tag {
margin: 10px 5px 5px 0px;
text-align: center;
}
.options-card {
width: 400px;
margin-left: auto;
margin-right: auto;
height: auto;
padding-bottom: 5px;
}
#version-container {
margin-left: auto;
margin-right: auto;
width: 400px;
}
.options-header {
padding: 16px 16px 0px 16px;
font-size: 20px;
}
#contributors_container {
text-align: center;
margin-top: 30;
padding: 5px 20px 20px 20px;
width: auto;
margin-right: 20%;
margin-left: 20%;
margin-top: 20px;
}
#contributor-list {
max-width: 1200px;
margin: 0 auto;
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.contributor-card {
cursor: pointer;
padding: 5px;
max-width: 1200px;
margin: 0 auto;
text-align: center;
display: grid;
grid-gap: 1rem;
}
.contributor-card img {
width: 200px;
height: 200px;
}
.contributor-name {
font-weight: bold;
margin: 0;
}
.contributor-username {
margin: 0 0 5px 0;
font-style: italic;
}
.contributor-title {
margin-bottom: 0;
font-size: 15px;
}
.open-source-tag {
padding: 10px;
font-size: 16px;
}

563
css/popup.css Normal file
View File

@@ -0,0 +1,563 @@
.card {
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
transition: 0.3s;
margin: 5px;
}
.card:hover {
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.19);
}
.container {
padding: 3px 16px 3px 12px;
}
h2 {
padding: 10px 0px 0 10px;
}
li {
width: 350px;
}
body {
min-width: 370px;
min-height: 400px;
position: relative;
}
.conflict_message{
font-size:small;
font-weight:bold;
color:red;
margin:5px 5px 5px 10px;
}
.course_list {
list-style-type: none;
padding: 5px;
margin-bottom: 30px;
}
.course_list_card {
cursor: pointer;
}
.course_list_item {
padding: 0px 5px 5px 5px;
overflow-y: auto;
max-height:400px;
}
.course_list_item_subtext {
font-size:medium;
}
.empty_message {
font-weight: normal;
font-size: large;
margin: 60px 30px 200px 30px;
text-align: center;
}
#empty #main {
margin-bottom: 5px;
}
#empty span {
font-size: small;
display: table;
margin: 0 auto;
font-weight: bold;
}
.time_line_days {
display:inline-block;
width: 20%;
}
.time_line_hours {
margin-left:10px;
display:inline-block;
width: 50%;
text-align:center;
}
.time_line_location {
float:right;
display:inline-block;
text-align:right;
width: 25%;
}
.time_line_location_link {
color:#3c87a3;
text-decoration:none;
}
.more_info_button{
background: #2196F3;
}
.remove_button {
background: #F44336;
}
.register_button {
background: #4CAF50;
}
.settings_button {
margin-right: 2px;
border: 0px;
border-radius: 50%;
transition: 0.3s;
opacity: 0.5;
}
.settings_button:focus {
outline: 0;
}
.settings_button:hover {
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.16), 0 4px 15px 0 rgba(0, 0, 0, 0.12);
opacity: 1;
}
.selected {
box-shadow: 0 0 0 1pt #FF9800;
}
.settings_button:focus:after:hover {
outline: 0;
transition: 0.3s;
}
.copy_button {
background-color: transparent;
padding: 0px;
border: none;
font-size: 15px;
cursor: pointer;
border-radius: 50%;
transition: .2s;
}
.copy_button:focus {
outline: 0;
}
.shadow {
box-shadow: 0 2px 20px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.16);
}
i {
padding: 4px 0px;
}
.copy_button_icon {
color:white;
float:left;
border-radius: 50%;
padding: 3px;
text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.16);
font-size: x-large;
}
.header_container{
border-radius: 7px;
}
.header_buttons {
padding: 5px 10px 5px 10px;
display: flex;
justify-content: space-between;
}
.header_button {
font-size:15px !important;
margin: 7px !important;
}
.clear_button {
background:#4CAF50;
}
.ris_button {
background:#FF9800;
}
.schedule_button {
background: #FF0000;
}
.settings {
position: absolute;
bottom: 0px;
right: 0px;
display: flex;
vertical-align: middle;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
transition: 0.3s;
padding: 7px 5px 5px 7px;
margin: 0px 5px 0px 0px;
border-radius: 7px;
}
.material_button {
border: none;
outline: none;
cursor: pointer;
color: white;
margin: 10px 10px 10px 0px;
padding: 10px 10px;
border-radius: 10px;
font-size: medium;
font-style: bold;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
position: relative;
overflow: hidden;
}
.course_name_truncate_box {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 80%;
color:white;
margin:5px;
display:inline-block;
font-size:large;
align-items:center;
}
.settings_divider {
margin-bottom: 0px;
display:inline-block;
}
.search_button {
background-color:white;
margin-left: 5px;
margin-right: 3px;
}
.course_list_item_options {
display: none;
}
.import_button {
background-color:white;
}
.options_button {
background-color:white;
margin-right: 0px;
}
.settings_icon {
color:#FF9800;
}
.course_list_item_time_box {
font-weight:bold;
padding:10px;
margin:0px 5px 0px 15px;
font-size:small;
}
.course_list_item_options_button_container {
border-radius:0px;
}
.course_list_item_options_buttons{
float:right;
margin:5px;
}
.arrow {
float:right;
font-size:small;
display:inline-block;
margin-top:10px;
color:white;
font-family: sans-serif;
}
.material_button:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
.card:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 100px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: #fefefe;
margin: auto;
max-height: 85%;
overflow-y: auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
}
/* The Close Button */
.close {
color: #aaaaaa;
float: right;
padding: 5px;
font-size: 28px;
font-weight: bold;
}
@keyframes ripple {
0% {
transform: scale(0, 0);
opacity: 1;
}
20% {
transform: scale(25, 25);
opacity: 1;
}
100% {
opacity: 0;
transform: scale(40, 40);
}
}
.material_button:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
.card:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
input {
border-radius: 5px;
color: rgba(0, 0, 45, 0.48)
}
input:focus {
outline: 0;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
#search-popup {
background: white;
color: #747474;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
position: absolute;
margin: auto;
bottom: 42px;
right: 20px;
padding: 10px;
border-radius: inherit;
width: 120px;
}
.flex-container {
display: flex;
flex-direction: column;
}
.item {
flex: 1 1 auto;
}
.select-style {
border: 1px solid #979797;
margin: 5px 0px;
border-radius: 5px;
display: none;
background: transparent no-repeat 90% 50%;
position: relative;
}
.select-style:after {
content: '\e5c5';
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
color: rgba(0, 0, 45, 0.48);
position: absolute;
pointer-events: none;
bottom: 0px;
right: 0px;
}
.select-style select {
padding: 5px 8px;
border: none;
color: rgba(0, 0, 45, 0.48);
box-shadow: none;
background: transparent;
background-image: none;
-webkit-appearance: none;
appearance: none;
width: 120px;
word-break: normal;
-ms-word-break: normal;
overflow: visible;
}
.select-style select:focus {
outline: none;
}
.class_id_input {
display: none;
border: 1px solid #979797;
border-radius: 5px;
margin: 10px 0px;
padding: 5px 8px;
}
.class_id_input::placeholder {
color: rgba(0, 0, 45, 0.48);
}
.search-button {
float: right;
font-size: inherit;
padding: 7px 11px;
margin: 8px 0px;
background-color: #FF9800;
}
#import-export-popup {
background: white;
color: #747474;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
position: absolute;
margin: auto;
bottom: 42px;
right: 10px;
border-radius: inherit;
width: 140px;
}
.simple-menu-option {
display: inline-block;
color: rgba(0, 0, 45, 0.48);
border: none;
background-color: white;
font-size: 15px;
overflow: hidden;
vertical-align: middle;
cursor: pointer;
text-align: left;
padding: 10px 0px 5px 5px;
border-radius: 7px;
}
.simple-menu-option i {
font-size: 19px;
vertical-align: middle;
margin-top: 0px;
margin-left: 0px;
margin-right: 3px;
margin-bottom: 3px;
}
.simple-menu-option:hover {
background-color: rgba(177, 175, 175, 0.200);
transition-duration: 0.4s;
/* color: #FF9800; */
}
.simple-menu-option:focus {
outline: none;
}
.hide {
display: none !important;
}
.meta{
margin:0;
color:#FF9800;
position: absolute;
bottom: 0px;
left: 0px;
vertical-align: middle;
padding: 7px 5px 5px 7px;
margin: 0px 5px 0px 5px;
font-size: 10px;
}
.meta-metric{
font-size: 20px;
font-weight: bold;
}
.meta-container{
margin: 5px 5px 10px 5px;
}
.input-box{
color: rgba(0, 0, 45, 0.48);
border: 1px solid #8C8C8C;
font-size: 11px;
padding: 5px;
border-radius: 7px;
width: 90%;
}
.input-box::placeholder {
color: rgba(0, 0, 45, 0.345);
}

308
css/styles.css Normal file
View File

@@ -0,0 +1,308 @@
.modal {
display: none;
position: fixed;
z-index: 1;
padding-top: 75px;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: #fefefe;
margin: auto;
max-height: 85%;
overflow-y: auto;
padding: 20px;
border: 1px solid #888;
width: 50%;
}
.loader {
border: 10px solid #f3f3f3;
border-top: 10px solid #FF9800;
border-radius: 50%;
width: 50px;
height: 50px;
display: none;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.close {
color: #aaaaaa;
float: right;
padding: 5px;
font-size: 28px;
font-weight: bold;
}
.title {
font-size: x-large;
font-weight: bold;
padding-top: 5px;
line-height: 1;
padding-left: 5px;
margin: 5px 0px 5px 0px;
}
.distButton {
vertical-align: bottom;
}
.description {
padding: 5px;
font-size: 15px;
font-weight: normal;
}
.chartloader {
position: absolute;
margin-left: auto;
margin-right: auto;
margin-top: 50px;
left: 0;
right: 0;
}
.profname {
margin-left: 5px;
padding-bottom: 5px;
font-size: medium;
margin-top: 5px;
}
.dateTimePlace {
margin-left: 5px;
margin-bottom: 0px;
margin-top: 0px;
font-size: smaller;
font-weight: bold;
}
#chartcontainer {
max-width: 100%;
height: 250px;
}
#chart {
min-width: auto;
max-width: 100%;
height: 250px;
margin: 0 auto;
z-index: 1;
}
.card {
transition: 0.3s;
margin-bottom: 10px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
}
.card:hover {
box-shadow: 0 4px 10px 0 rgba(0, 0, 0, 0.2), 0 4px 20px 0 rgba(0, 0, 0, 0.19);
}
.cardcontainer {
padding: 2px 16px;
transition: width 300ms ease-in-out, height 300ms ease-in-out;
}
.description {
padding: 10px;
}
.close:hover,
.close:focus {
color: #000;
text-decoration: none;
cursor: pointer;
}
.topbuttons .material-button {
display: inline-block;
}
.rmp-button {
}
.ecis-button {
}
.textbook-button{
}
.material-button {
border: none;
outline: none;
cursor: pointer;
color: white;
margin: 10px 10px 10px 0px;
padding: 10px 10px;
border-radius: 10px;
font-size: medium;
font-style: bold;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
background: #ff9800;
position: relative;
overflow: hidden;
}
.material-button:after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
@keyframes ripple {
0% {
transform: scale(0, 0);
opacity: 1;
}
15% {
transform: scale(25, 25);
opacity: 1;
}
100% {
opacity: 0;
transform: scale(40, 40);
}
}
.material-button:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
#snackbar {
visibility: hidden;
min-width: 250px;
margin-left: -200px;
background-color: #333;
color: #fff;
border-radius: 2px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
}
.descriptionli {
padding: 0px 5px 5px 5px;
}
#snackbar.show {
visibility: visible;
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
#semesters {
padding: 5px;
margin-right: 10px;
margin-top: 10px;
}
#semesters:focus {
outline: 0;
}
.tooltip {
position: relative;
}
.tooltip .tooltiptext {
visibility: hidden;
background-color: black;
background:rgba(1,1,1,0.5);
color: #fff;
text-align: left;
border-radius: 6px;
font-size: 10px;
max-width: 100px;
margin-left: 5px;
padding: 5px 10px;
z-index: 2;
position: absolute;
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
@-webkit-keyframes fadein {
from {
bottom: 0;
opacity: 0;
}
to {
bottom: 30px;
opacity: 1;
}
}
@keyframes fadein {
from {
bottom: 0;
opacity: 0;
}
to {
bottom: 30px;
opacity: 1;
}
}
@-webkit-keyframes fadeout {
from {
bottom: 30px;
opacity: 1;
}
to {
bottom: 0;
opacity: 0;
}
}
@keyframes fadeout {
from {
bottom: 30px;
opacity: 1;
}
to {
bottom: 0;
opacity: 0;
}
}

Binary file not shown.

1
docs/_config.yml Normal file
View File

@@ -0,0 +1 @@
theme: jekyll-theme-cayman

1
docs/departments.json Normal file
View File

@@ -0,0 +1 @@
["ACC","ADV","ASE","AFR","AFS","ASL","AMS","AHC","ANT","ALD","ARA","ARE","ARI","ARC","AED","ARH","ART","AET","AAS","ANS","AST","BSN","BEN","BCH","BIO","BME","BDP","B A","BAX","BGS","CHE","CH","CHI","C E","CLA","C C","CGS","COM","CLD","CMS","CRP","C L","COE","CSE","C S","CON","CTI","CRW","CDI","EDC","CZ","DAN","DSC","D S","DES","DEV","D B","DRS","DCH","ECO","ELP","EDP","E E","ECE","EER","EMA","ENM","E M","E S","E","ESL","ENS","EVE","EVS","EUP","EUS","FIN","F A","FLU","FR","F H","G E","GRG","GEO","GER","GSD","GOV","GRS","GK","GUI","HAR","H S","HCT","HED","HEB","HIN","HIS","HDF","HDO","H E","HMN","ILA","I","ISP","INF","ITD","I B","IRG","ISL","ITL","ITC","JPN","J S","J","KIN","KOR","LAR","LTC","LAT","LAL","LAS","LAW","LEB","L A","LAH","LIN","MAL","MAN","MIS","MFG","MNS","MKT","MSE","M","M E","MDV","MAS","MEL","MES","M S","MOL","MUS","NSC","N S","NEU","NOR","N","NTR","OBO","OPR","O M","ORI","ORG","PER","PRS","PGE","PGS","PHM","PHL","PED","P S","PHY","PIA","POL","POR","PRC","PSY","P A","PBH","P R","RIM","RTF","R E","R S","RHE","R M","RUS","REE","SAN","SAX","STC","STM","S C","SEL","S S","S W","SOC","SPN","SPC","SED","SLH","STA","SDS","SUS","SWE","TAM","TXA","T D","TRO","TRU","TBA","TUR","T C","UKR","UGS","UDN","URB","URD","UTS","UTL","VIA","VIO","V C","VAS","VOI","WGS","WRT","YID","YOR"]

BIN
grades.db Normal file

Binary file not shown.

BIN
icons/icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
icons/icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
icons/icon32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
icons/icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
images/disticon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

196
js/Template.js Normal file
View File

@@ -0,0 +1,196 @@
class Template {}
Template.Main = class {
static modal() {
return `<div class=modal id=myModal>
<div class=modal-content>
<span class=close>×</span>
<div class=card>
<div class=cardcontainer>
<h2 class=title id="title">Computer Fluency (C S 302)</h2>
<h2 class=profname id="profname">with <a id="professor_link">Bruce Porter</a></h2>
<div id="topbuttons" class=topbuttons>
<button class=material-button id="rateMyProf" style="background: #4CAF50;"> RMP </button>
<button class=material-button id="eCIS" style="background: #CDDC39;"> eCIS </button>
<button class=material-button id="textbook" style="background: #FFC107;"> Textbook </button>
<button class=material-button id="Syllabi"> Past Syllabi </button>
<button class=material-button id="saveCourse" value="add" style="background: #F44336;"> Save Course +</button>
</div>
</div>
</div>
<div class=card>
<div class=cardcontainer style="">
<div class="chartloader">
<div class="loader" id='descload'></div>
</div>
<ul class=description id="description" style="list-style-type:disc"></ul>
</div>
</div>
<div class=card style='text-align:center'>
<select id="semesters" style='text-align-last:center;color:#666666;fill:#666666;'>
</select>
<div class="chartloader">
<div class="loader" id='chartload'></div>
</div>
<div id="chartcontainer" class=cardcontainer>
<div id=chart></div>
</div>
</div>
</div>
</div>`;
}
static extension_button() {
return `<td data-th="Plus"><input type="image" class="distButton" id="distButton" width="20" height="20" src='${chrome.extension.getURL("images/disticon.png")}'/></td>`;
}
};
Template.Catalog = class {
static loading() {
return `<div style="text-align:center">
<div class="loader" id='loader'></div>
<br>
<h1 id="nextlabel"style="color: #FF9800;display:none;">Loading Courses</h1>
<h1 id="retrylabel"style="color: #F44336;display:none;">Failed to Load Courses</h1>
<br>
<button class=material-button id="retry" style="background: #F44336;display:none;">Retry</button>
</div>`;
}
};
Template.UTPlanner = class {
static modal() {
return `<div class=modal id=myModal>
<div class=modal-content>
<span class=close>×</span>
<div class=card>
<div class=cardcontainer>
<h2 class=title id="title">Computer Fluency (C S 302)</h2>
<h2 class=profname id="profname" style="margin-bottom:0px;">with Bruce Porter</h2>
<div id="topbuttons" class=topbuttons>
<button class=material-button id="moreInfo" style="background: #2196F3;"> More Info </button>
<button class=material-button id="textbook" style="background: #FFC107;"> Textbook </button>
<button class=material-button id="Syllabi"> Past Syllabi </button>
<button class=material-button id="saveCourse" value="add" style="background: #F44336;opacity:.4;"> Unable to Save</button>
</div>
</div>
</div>
<div class=card style='text-align:center'>
<select id="semesters" style='text-align-last:center;color:#666666;fill:#666666;'>
</select>
<div class="chartloader">
<div class="loader" id='chartload'></div>
</div>
<div id="chartcontainer" class=cardcontainer>
<div id=chart></div>
</div>
</div>
</div>
</div>`;
}
};
Template.Calendar = class {
static line(line) {
let { days, start_time, end_time, location_link, location_full } = line;
return `<p class='time' style='font-size:large;'>
<span style='display:inline-block;'>${days}:</span>
<span style='margin-left:10px;display:inline-block;text-align:center;'>${start_time} to ${end_time}</span>
<span style='float:right;display:inline-block;text-align:right;width: 25%;'>
<a target='_blank' style='color:#3c87a3;text-decoration:none;'href='${location_link}'>${location_full}</a>
</span>
</p>`;
}
static modal() {
return `<div id="myModal" class="modal">
<div class="modal-content">
<span class="close">&times;</span>
<div class="card">
<div id="colorStrip" style="height:10px;"></div>
<div class="cardcontainer">
<div id='header'>
<div style="display:flex;">
<h2 id="classname">Classname</h2>
</div>
<p id="prof">Prof</p>
</div>
<button id="info" class="matbut" style="font-size:medium; margin-right: auto; margin-left:auto; background: #2196F3;">More Info</button>
<button id="register" class="matbut" style="font-size:medium; margin-right: auto; margin-left:10px; background: #4CAF50;">Register</button>
<button id="remove" class="matbut" style="font-size:medium;margin:10px;background: #FF0000;">Remove</button>
</div>
</div>
</div>`;
}
};
Template.Popup = class {
static list_item(i, list_tile_color, unique, department, number, profname, list_sub_color, line) {
return `<li id='${i}' class='course_list_item'>
<div class='card course_list_card'>
<div class='container' style='background:${list_tile_color}'>
<button class='copy_button' title='Copy Unique #' value='${unique}'>
<i id='copyicon' class="material-icons copy_button_icon">content_copy</i>
</button>
<h4 class='course_name_truncate_box'>
<b>${department} ${number} <span class='course_list_item_subtext'> with ${profname} (${unique})</span></b>
</h4>
<p id='arrow' class='arrow'>&#9658;</p>
</div>
</div>
<div id='moreInfo' class='course_list_item_options'>
<p style='background-color:${list_sub_color};' class='course_list_item_time_box'>${line}</p>
<div id='infoButtons' class='course_list_item_options_button_container'>
<button class='material_button course_list_item_options_buttons remove_button' id='listRemove'>Remove</button>
<button class='material_button course_list_item_options_buttons register_button' id='register'>Register</button>
<button class='material_button course_list_item_options_buttons more_info_button' id='listMoreInfo'>More Info</button>
</div>
</div>
</li>`;
}
static conflict_message(conflict_message) {
return `<p id='conflict' class='conflict_message'>${conflict_message}</>`;
}
static line(line) {
let { days, start_time, end_time, location_link, location_full } = line;
return `<span class='time_line_days'>${days}:</span>
<span class='time_line_hours'>${start_time} to ${end_time}</span>
<span class='time_line_location'>
<a target='_blank' class= 'time_line_location_link' href='${location_link}'>${location_full}</a>
</span>
<br>`;
}
};
Template.Import = class {
static import_button() {
return `<button class='material-button' id='import' style='margin:15px 0px;'>${Text.button_text_default}</button><br>`;
}
static waitlist_import_button() {
return `<button class='material-button' id='import_waitlist' style='margin:0px'>${Text.waitlist_button_text_default}</button><br>`;
}
static store_waitlist_message() {
return `<h1 id="nextlabel"style="color: #FF9800;display:none;"></h1>`;
}
};
Template.Options = class {
static options_row(key, enabled) {
let button_text = enabled ? "Turn Off" : "Turn On";
let button_color = enabled ? Colors.closed : Colors.open;
let label_text = capitalizeString(key.replace(/([A-Z]+)*([A-Z][a-z])/g, "$1 $2"));
return `<h2 style="padding: 5px 16px 5px 16px; font-weight: normal;display: inline-block;text-align:left;">
${label_text}
</h2>
<button id="${key}" value=${enabled} class="material-button" style="display:inline-block;font-size:medium; float:right; background:${button_color}">
${button_text}
</button>
<br>`;
}
static contributor_card(username, name, image_url, profile_url) {
return `<div class='card contributor-card' id="${username}" data-url="${profile_url}">
<img class='contributor-image' src="${image_url}"></img>
${name ? `<p class='contributor-name'>${name}</p>` : ""}
<p class='contributor-username'>${username}</p>
</div>`;
}
};

487
js/background.js Normal file
View File

@@ -0,0 +1,487 @@
console.log(`UT Registration Plus background page: ${window.location.href}`);
var grades; // caching the grades database in memory for faster queries
var current_semesters = {};
var departments = [];
var should_open = false; // toggled flag for automatically opening popup on new pages when 'more info' hit
// these are the default options that the extension currently supports
const default_options = {
loadAll: true,
courseConflictHighlight: true,
storeWaitlist: true,
};
onStartup();
function onStartup() {
updateBadge(true);
loadDataBase();
getCurrentSemesters();
getCurrentDepartments();
}
/* Handle messages and their commands from content and popup scripts*/
chrome.runtime.onMessage.addListener(function (request, sender, response) {
switch (request.command) {
case "courseStorage":
if (request.action == "add") {
add(request, sender, response);
}
if (request.action == "remove") {
remove(request, sender, response);
}
break;
case "isSingleConflict":
isSingleConflict(request.dtarr, request.unique, response);
break;
case "checkConflicts":
checkConflicts(response);
break;
case "updateBadge":
updateBadge();
break;
case "updateStatus":
updateStatus(response);
break;
case "alreadyContains":
alreadyContains(request.unique, response);
break;
case "updateCourseList":
updateTabs();
break;
case "gradesQuery":
executeQuery(request.query, response);
break;
case "currentSemesters":
response({ semesters: current_semesters });
getCurrentSemesters();
break;
case "currentDepartments":
response({ departments: departments });
break;
case "setOpen":
should_open = true;
chrome.tabs.create({ url: request.url });
break;
case "shouldOpen":
response({ open: should_open });
should_open = false;
break;
case "getOptionsValue":
getOptionsValue(request.key, response);
break;
case "setOptionsValue":
setOptionsValue(request.key, request.value, response);
break;
default:
const xhr = new XMLHttpRequest();
const method = request.method ? request.method.toUpperCase() : "GET";
xhr.open(method, request.url, true);
console.log(request);
xhr.onload = () => {
console.log(xhr.responseUrl);
response(xhr.responseText);
};
xhr.onerror = () => response(xhr.statusText);
if (method == "POST") {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
}
xhr.send(request.data);
break;
}
return true;
});
/* Initially set the course data in storage */
chrome.runtime.onInstalled.addListener(function (details) {
if (details.reason == "install") {
setDefaultOptions();
chrome.storage.sync.get("savedCourses", function (data) {
if (!data.savedCourses) {
chrome.storage.sync.set({
savedCourses: [],
});
}
});
} else if (details.reason == "update") {
// if there's been an update, call setDefaultOptions in case their settings have gotten wiped
setDefaultOptions();
console.log("updated");
}
});
chrome.storage.onChanged.addListener(function (changes) {
for (key in changes) {
if (key === "savedCourses") {
updateBadge(false, changes.savedCourses.newValue); // update the extension popup badge whenever the savedCourses have been changed
}
}
});
// get the value of an option if it exists
function getOptionsValue(key, sendResponse) {
chrome.storage.sync.get("options", function (data) {
if (!data.options) {
setDefaultOptions();
} else {
sendResponse({
value: data.options[key],
});
}
});
}
// set the value of an option if it exists
function setOptionsValue(key, value, sendResponse) {
chrome.storage.sync.get("options", function (data) {
let new_options = data.options;
if (!data.options) {
// if there are no options set, set the defaults
setDefaultOptions();
new_options = default_options;
}
new_options[key] = value;
chrome.storage.sync.set(
{
options: new_options,
},
function () {
sendResponse({
value: new_options[key],
});
}
);
});
}
// set the default options if the options haven't been set before
function setDefaultOptions() {
chrome.storage.sync.get("options", function (data) {
if (!data.options) {
chrome.storage.sync.set(
{
options: default_options,
},
function () {
console.log("default options:", default_options);
}
);
}
});
}
async function getCurrentSemesters() {
let webData;
if(Object.keys(current_semesters).length > 0) {
chrome.storage.local.set({
semesterCache: current_semesters
});
}
async function goFetch(linkend="") {
console.log("lk " + linkend)
return fetch("https://registrar.utexas.edu/schedules/" + linkend)
.then((response) => {
return response.text()
.then((data) => {
return data;
}).catch((err) => {
console.log(err);
})
});
}
await goFetch().then((data) => {webData = data});
if(webData == null) {
webData = ""
}
let arr = webData.split("\n");
let i = 0
for(let row=0; row<arr.length; row++) {
let currentRow = arr[row]
if(currentRow.startsWith('<li><a href="https://registrar.utexas.edu/schedules/') && currentRow[52] != "a") {
let newWebData;
// let start = currentRow.indexOf('Schedule">')+10;
let start = Math.max(currentRow.lastIndexOf('Summer'), Math.max(currentRow.lastIndexOf('Spring'), currentRow.lastIndexOf('Fall')))
let end = currentRow.indexOf('</a></li>');
console.log(currentRow)
console.log(start + " " + end)
let name = currentRow.substring(start,end);
console.log("my name: " + name)
let num = currentRow.indexOf('"https://registrar.utexas.edu/schedules/">')+53;
let numend = currentRow.indexOf('" target');
let short_sem_num = currentRow.substring(num,numend);
current_semesters[name] = "code";
await goFetch(short_sem_num).then((data) => {newWebData = data});
arr2 = newWebData.split("\n")
for(let row2=0; row2<arr2.length; row2++) {
if(arr2[row2].startsWith('<div class="gobutton"><a href="')) {
let start2 = arr2[row2].indexOf('<div class="gobutton"><a href="')+31;
let end2 = arr2[row2].indexOf('" target="');
var scheduleLink = arr2[row2].substring(start2,end2);
var sem_num = scheduleLink.substring(scheduleLink.lastIndexOf("/") + 1).trim();
if (current_semesters[name] != sem_num) {
current_semesters[name] = sem_num;
}
}
}
}
}
}
// use the utexas review api for getting the list of departments
function getCurrentDepartments() {
$.get("https://raw.githubusercontent.com/sghsri/UT-Registration-Plus/master/docs/departments.json", function (response) {
if (response) {
departments = JSON.parse(response);
}
});
}
// update the badge text to reflect the new changes
function updateBadge(first, new_changes) {
if (new_changes) {
updateBadgeText(first, new_changes);
} else {
chrome.storage.sync.get("savedCourses", function (data) {
let courses = data.savedCourses;
updateBadgeText(first, courses);
});
}
}
// update the badge text to show the number of courses that have been saved by the user
function updateBadgeText(first, courses) {
let badge_text = courses.length > 0 ? `${courses.length}` : "";
let flash_time = !first ? 200 : 0;
chrome.browserAction.setBadgeText({
text: badge_text,
});
if (!first) {
// if isn't the first install of the extension, flash the badge to bring attention to it
chrome.browserAction.setBadgeBackgroundColor({
color: Colors.badge_flash,
});
}
setTimeout(function () {
chrome.browserAction.setBadgeBackgroundColor({
color: Colors.badge_default,
});
}, flash_time);
}
/* Find all the conflicts in the courses and send them out/ if there is even a conflict*/
function checkConflicts(sendResponse) {
chrome.storage.sync.get("savedCourses", function (data) {
var conflicts = [];
var courses = data.savedCourses;
for (let i = 0; i < courses.length; i++) {
for (let j = i + 1; j < courses.length; j++) {
let course_a = courses[i];
let course_b = courses[j];
if (isConflict(course_a.datetimearr, course_b.datetimearr)) conflicts.push([course_a, course_b]);
}
}
sendResponse({
isConflict: conflicts.length !== 0,
between: conflicts.length ? conflicts : undefined,
});
});
}
/* Find if the course at unique and with currdatearr is contained in the saved courses and if it conflicts with any other courses*/
function isSingleConflict(currdatearr, unique, sendResponse) {
chrome.storage.sync.get("savedCourses", function (data) {
var courses = data.savedCourses;
var conflict_list = [];
var conflict = false;
var contains = false;
for (let i = 0; i < courses.length; i++) {
let course = courses[i];
if (isConflict(currdatearr, course.datetimearr)) {
conflict = true;
conflict_list.push(course);
}
if (!contains && isSameCourse(course, unique)) {
contains = true;
}
}
sendResponse({
isConflict: conflict,
alreadyContains: contains,
conflictList: conflict_list,
});
});
}
/* Check if conflict between two date-time-arrs*/
function isConflict(adtarr, bdtarr) {
for (var i = 0; i < adtarr.length; i++) {
var current_day = adtarr[i][0];
var current_times = adtarr[i][1];
for (var j = 0; j < bdtarr.length; j++) {
var next_day = bdtarr[j][0];
var next_times = bdtarr[j][1];
if (next_day == current_day) {
if (current_times[0] < next_times[1] && current_times[1] > next_times[0]) {
return true;
}
}
}
}
return false;
}
/* Add the requested course to the storage*/
function add(request, sender, sendResponse) {
chrome.storage.sync.get("savedCourses", function (data) {
var courses = data.savedCourses;
if (!contains(courses, request.course.unique)) {
courses.push(request.course);
console.log(courses);
chrome.storage.sync.set({
savedCourses: courses,
});
}
sendResponse({
done: "Added: (" + request.course.unique + ") " + request.course.coursename,
label: "Remove Course -",
value: "remove",
});
});
}
/* Find and Remove the requested course from the storage*/
function remove(request, sender, sendResponse) {
chrome.storage.sync.get("savedCourses", function (data) {
var courses = data.savedCourses;
console.log(courses);
var index = 0;
while (index < courses.length && courses[index].unique != request.course.unique) {
index++;
}
courses.splice(index, 1);
chrome.storage.sync.set({
savedCourses: courses,
});
sendResponse({
done: "Removed: (" + request.course.unique + ") " + request.course.coursename,
label: "Add Course +",
value: "add",
});
});
}
/* Find if the unique is already contained within the storage*/
function alreadyContains(unique, sendResponse) {
chrome.storage.sync.get("savedCourses", function (data) {
var courses = data.savedCourses;
sendResponse({
alreadyContains: contains(courses, unique),
});
});
}
// find if a course with the current unique number exists in the user's saved courses
function contains(courses, unique) {
var i = 0;
while (i < courses.length) {
if (isSameCourse(courses[i], unique)) {
return true;
}
i++;
}
return false;
}
// does it have the same unique number as provided
function isSameCourse(course, unique) {
return course.unique == unique;
}
// send a message to every tab open to updateit's course list (and thus recalculate its conflicts highlighting)
function updateTabs() {
chrome.tabs.query({}, function (tabs) {
for (var i = 0; i < tabs.length; i++) {
chrome.tabs.sendMessage(tabs[i].id, {
command: "updateCourseList",
});
}
});
}
// const UPDATE_INTERVAL = 1000 * 60 * 16;
// setInterval(updateStatus, UPDATE_INTERVAL);
// // updateStatus();
// function updateStatus(sendResponse) {
// chrome.storage.sync.get("savedCourses", function (data) {
// var courses = data.savedCourses;
// var no_change = true;
// for (let i = 0; i < courses.length; i++) {
// try {
// let c = courses[i];
// let old_status = c.status;
// let old_link = c.link;
// $.ajax({
// url: old_link,
// success: function (result) {
// if (result) {
// console.log(result);
// var object = $("<div/>").html(result).contents();
// let new_status = object.find('[data-th="Status"]').text();
// let register_link = object.find('td[data-th="Add"] a');
// if (register_link) register_link = register_link.attr("href");
// var haschanged = new_status == old_status && register_link == old_link;
// if (!haschanged) console.log(c.unique + " updated from " + old_status + " to " + new_status + " and " + old_link + " to " + register_link);
// no_change &= haschanged;
// c.registerlink = register_link;
// c.status = new_status;
// }
// },
// });
// } catch (e) {
// console.log(e);
// console.log("Not logged into UT Coursebook. Could not update class statuses.");
// }
// }
// if (!no_change) {
// chrome.storage.sync.set({
// savedCourses: courses,
// });
// console.log("updated status");
// }
// });
// }
// execute a query on the grades database
function executeQuery(query, sendResponse) {
var res = grades.exec(query)[0];
sendResponse({
data: res,
});
}
/* Load the database*/
function loadDataBase() {
sql = window.SQL;
loadBinaryFile("grades.db", function (data) {
var sqldb = new SQL.Database(data);
grades = sqldb;
});
}
/* load the database from file */
function loadBinaryFile(path, success) {
var xhr = new XMLHttpRequest();
xhr.open("GET", chrome.extension.getURL(path), true);
xhr.responseType = "arraybuffer";
xhr.onload = function () {
var data = new Uint8Array(xhr.response);
var arr = new Array();
for (var i = 0; i != data.length; ++i) arr[i] = String.fromCharCode(data[i]);
success(arr.join(""));
};
xhr.send();
}

291
js/calendar.js Normal file
View File

@@ -0,0 +1,291 @@
var color_counter = 0;
var {
calendar_fade_time,
button_delay
} = Timing;
var saved_courses = [];
var curr_course = {}
$("#calendar").after(Template.Calendar.modal());
chrome.storage.sync.get("savedCourses", function (data) {
// Iterate through each saved course and add to 'event'
saved_courses = data.savedCourses;
console.log(saved_courses);
let event_source = buildEventSource(saved_courses);
$("#calendar").fullCalendar({
editable: false, // Don't allow editing of events
handleWindowResize: true,
weekends: false, // will hide Saturdays and Sundays
slotDuration: "00:30:00", // 15 minute intervals on vertical column
slotEventOverlap: false, // No overlapping between events
defaultView: "agendaWeek", // Only show week view
header: false, // Hide buttons/titles
minTime: "08:00:00", // Start time
maxTime: "21:00:01", // End time
columnHeaderFormat: "ddd", // Only show day of the week names
displayEventTime: true, // Display event time
allDaySlot: false,
Duration: {
hours: 1
},
height: 'auto',
events: event_source,
slotLabelFormat: [
'h:mm A' // lower level of text
],
eventRender: function (event, element, view) {
$(element).css("padding", "5px").css("margin-bottom", "5px");
},
eventClick: function (data, event, view) {
displayModal(data)
}
});
});
function displayModal(data) {
$("#myModal").fadeIn(calendar_fade_time);
$("#colorStrip").css('background-color', data.color);
curr_course = saved_courses[data.index];
setUpModal()
}
function setUpModal() {
let {
coursename,
unique,
datetimearr,
profname,
status,
registerlink
} = curr_course;
$("#classname").html(`${coursename} <span style='font-size:small'>(${unique})</span>`);
buildTimeTitle(datetimearr);
$("#prof").html(`with <span style='font-weight:bold;'>${capitalizeString(profname)}</span>`);
setRegisterButton(status, registerlink)
}
function setRegisterButton(status, registerlink) {
if (canNotRegister(status, registerlink))
$("#register").text("Can't Register").css("background-color", Colors.closed);
else if (status.includes("waitlisted"))
$("#register").text("Join Waitlist").css("background-color", Colors.waitlisted);
else
$("#register").text("Register").css("background-color", Colors.open);
}
function buildTimeTitle(datetimearr) {
$('#timelines').remove();
var arr = convertDateTimeArrToLine(datetimearr)
var output = "";
for (let i = 0; i < arr.length; i++) {
let line = arr[i];
output += Template.Calendar.line(line);
}
$("#header").after(`<div id='timelines'>${output}</div`);
}
// Iterate through each saved course and add to 'event'
function buildEventSource(saved_courses) {
color_counter = 0;
let event_source = [];
var hours = 0;
for (let i = 0; i < saved_courses.length; i++) {
let {
coursename,
datetimearr
} = saved_courses[i];
let number = separateCourseNameParts(coursename).number;
let class_length = parseInt(number.charAt(0));
let multi_semester_code = number.slice(-1);
if (["A","B"].includes(multi_semester_code)) {
hours += Math.floor(class_length/2);
} else if (["X","Y","Z"].includes(multi_semester_code)) {
hours += Math.floor(class_length/3);
} else {
hours += class_length;
}
for (let j = 0; j < datetimearr.length; j++) {
let session = datetimearr[j]; // One single session for a class
let event_obj = setEventForSection(session, color_counter, i);
event_source.push(event_obj);
}
color_counter++;
}
displayMetaData(hours, saved_courses);
return event_source;
}
function displayMetaData(hours, saved_courses) {
$("#hours").text(hours + " Hours");
$("#num").text(saved_courses.length + " Courses");
}
//create the event object for every section
function setEventForSection(session, colorCounter, i) {
let full_day = days.get(session[0]);
let course = saved_courses[i];
let {
coursename,
profname,
} = course;
let {
department,
number
} = separateCourseNameParts(coursename)
beg_day = calculateBeginningDate(full_day)
start_date = formatCalculateDate(beg_day, full_day, session[1][0]);
end_date = formatCalculateDate(beg_day, full_day, session[1][1]);
event_obj = {
title: `${department}-${number} with ${capitalizeString(profname)}`,
start: start_date,
end: end_date,
color: Colors.material_colors[colorCounter],
building: session[2],
index: i,
allday: false
};
return event_obj;
}
function formatCalculateDate(beg_day, full_day, hour) {
return beg_day + moment().day(full_day)._d.toString().split(" ")[2] + "T" + hour + ":00";
}
function calculateBeginningDate(full_day) {
var year = moment().day(full_day)._d.toString().split(" ")[3];
var month_num = moment(moment().day(full_day)._d.toString().split(" ")[1], "MMM").format('MM');
return `${year}-${month_num}-`;
}
function updateCalendar() {
chrome.storage.sync.get("savedCourses", function (data) {
saved_courses = data.savedCourses
let event_source = buildEventSource(saved_courses);
$('#calendar').fullCalendar('removeEventSources');
$("#calendar").fullCalendar('addEventSource', event_source, true);
});
}
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
if (request.command == "updateCourseList" || request.command == "courseAdded") {
updateCalendar();
}
}
);
$("#info").click(() => {
openMoreInfoWithOpenModal(curr_course.link);
});
$("#save").click(() => {
takePicture();
});
$("#clear").click(() => {
/*Clear the list and the storage of courses*/
chrome.storage.sync.set({
savedCourses: []
});
updateAllTabsCourseList();
updateCalendar();
});
$("#remove").click(() => {
setTimeout(() => {
chrome.runtime.sendMessage({
command: "courseStorage",
course: curr_course,
action: "remove"
}, function () {
$("#myModal").fadeOut(calendar_fade_time);
updateCalendar();
updateAllTabsCourseList();
});
}, button_delay);
});
$("#register").click(function () {
let {
registerlink,
status
} = curr_course;
if (!canNotRegister(status, registerlink)) {
setTimeout(() => {
window.open(registerlink);
}, button_delay);
}
});
$("#export").click(function () {
var cal = ics();
var calendarEvents = $('#calendar').fullCalendar('clientEvents');
for (i in calendarEvents) {
var event = calendarEvents[i];
buildICSFile(cal, event);
}
cal.download("My_Course_Calendar");
});
function buildICSFile(cal, event) {
let {
title,
start,
end,
building
} = event;
let class_name = title.split('with')[0];
let description = `with ${title.split('with')[1]}`;
let time = start._d.toUTCString();
cal.addEvent(class_name, description, building, start._i, end._i, {
rrule: `RRULE:FREQ=WEEKLY;BYDAY=${time.substring(0, time.indexOf(",") - 1).toUpperCase()};INTERVAL=1`
});
}
function takePicture() {
var width = $("#calendar").width() * window.devicePixelRatio;
var height = $("#calendar").height() * window.devicePixelRatio;
let cropper = document.createElement('canvas').getContext('2d');
html2canvas(document.querySelector("#calendar"), Export.png_options).then(c => {
cropper.canvas.width = width;
cropper.canvas.height = height;
cropper.drawImage(c, 0, 0);
var a = document.createElement('a');
a.href = cropper.canvas.toDataURL("image/png");
a.download = 'mySchedule.png';
a.click();
});
}
/*Close Modal when hit escape*/
$(document).keydown((e) => {
if (e.keyCode == 27) {
$("#myModal").fadeOut(calendar_fade_time);
}
});
$('.close').click(function () {
close();
});
$('#myModal').click(function (event) {
if (event.target.id == 'myModal') {
close();
}
});
function close() {
$("#myModal").fadeOut(calendar_fade_time);
}

93
js/config.js Normal file
View File

@@ -0,0 +1,93 @@
class Timing {}
Timing.fade_time = 100;
Timing.calendar_fade_time = 100;
Timing.button_delay = 75;
class Colors {}
Colors.material_colors = [
"#4CAF50",
"#CDDC39",
"#FFC107",
"#2196F3",
"#F57C00",
"#9C27B0",
"#FF5722",
"#673AB7",
"#FF5252",
"#E91E63",
"#009688",
"#00BCD4",
"#4E342E",
"#424242",
"#9E9E9E",
];
Colors.open = "#4CAF50";
Colors.waitlisted = "#FF9800";
Colors.closed = "#FF5722";
Colors.no_status = "#607D8B";
Colors.open_light = "#C8E6C9";
Colors.waitlisted_light = "#FFE0B2";
Colors.closed_light = "#FFCCBC";
Colors.no_status_light = "#CFD8DC";
Colors.highlight_conflict = "#F44336";
Colors.highlight_default = "#333333";
Colors.highlight_saved = "#4CAF50";
Colors.badge_flash = "#FF5722";
Colors.badge_default = "#bf5700";
class Export {}
Export.png_options = {
foreignObjectRendering: true,
logging: true,
removeContainer: true,
async: true,
};
class Popup {}
Popup.num_semesters = 2;
/*
* Funny comments that popup when no classes have been chosen
*/
class Text {}
Text.emptyText = function () {
let arr = [
"Doesn't Look Like Anything To Me.",
"You Can't Fail Classes You're Not In.",
"Pro-Tip: Don't Take O-Chem.",
"Jendy's Fofofo™",
"Fine Dining at Jester City Limits",
"Rec Sports is full and it's only 2pm.",
"Hope Domino is doing well rn &#129402;",
"The year is 2055 and Welch still isn't finished.",
"Wear a Mask.",
"Motivation dropping faster than ur GPA",
"No Work Happens On PCL 5th Floor.",
"Sophomore But Freshman By Credit.",
"Pain is temporary, GPA is forever.",
"You've Yee'd Your Last Haw.",
"lol everything is already waitlisted.",
"At Least You're Not At A&M.",
`It's ${moment().format("h:mm")} and OU Still Sucks.`,
"TeXAs iS BaCK GuYZ",
"'Academically Challenged'",
"Does McCombs teach Parseltongue?",
"Feel bad if you say Wampus.",
"No Cruce Enfrente Del Bus.",
"Midterm 1 has been Unmuted",
"Omae Wa Mou Shindeiru...",
"Bevo Bucks are the new Bitcoin",
"Every day, another brick disappears from Speedway",
"The GDC will annex the EER one day",
"To hike to Kins, or not to hike to Kins...",
];
let index = Math.floor(Math.random() * arr.length);
return arr[index];
};
Text.button_text_default = "<span style='font-size:small'>Import to </span><b>UT Reg +<b>";
Text.waitlist_button_text_default = "<span style='font-size:small'>Import Waitlists to </span><b>UT Reg +<b>";
Text.button_success = "Courses Saved!";

627
js/courseCatalog.js Normal file
View File

@@ -0,0 +1,627 @@
console.log(`UT Registration Plus is running on this page: ${window.location.href}`);
var curr_course = {}
var semester_code = new URL(window.location.href).pathname.split('/')[4];
var done_loading = true;
var next = $("#next_nav_link");
if (next) {
chrome.runtime.sendMessage({
command: "getOptionsValue",
key: "loadAll",
}, function (response) {
if(response.value){
$('[title*="next listing"]').remove();
}
});
}
//This extension may be super lit, but you know what's even more lit?
//Matthew Tran's twitter and insta: @MATTHEWTRANN and @matthew.trann
if (document.querySelector('#fos_fl')) {
let params = (new URL(document.location)).searchParams;
let dep = params.get("fos_fl");
let level = params.get("level");
if (dep && level) {
if (dep.length == 3 && (level == 'U' || level == 'L' || level == 'G')) {
document.querySelector('#fos_fl').value = dep;
document.querySelector('#level').value = level;
}
}
}
//make heading and modal
if (!$("#kw_results_table").length) {
$("table").after(Template.Catalog.loading());
$("#container").prepend(Template.Main.modal());
$("#myModal").prepend("<div id='snackbar'>save course popup...</div>");
// now add to the table
$("table thead th:last-child").after('<th scope=col>Plus</th>');
$('table').find('tr').each(function () {
if (!($(this).find('td').hasClass("course_header")) && $(this).has('th').length == 0) {
$(this).append(Template.Main.extension_button());
}
});
}
if(isIndividualCoursePage()){
chrome.runtime.sendMessage({
command: "shouldOpen",
}, function (response) {
if(response.open){
$("#distButton").click();
}
});
}
updateListConflictHighlighting();
$("body").on('click', '#distButton', function () {
var row = $(this).closest('tr');
$('.modal-content').stop().animate({
scrollTop: 0
}, 500);
$(this).blur();
curr_course = getCourseInfo(row);
getDistribution(curr_course);
});
function updateLinks(course_info, first_name) {
let {
prof_name,
number
} = course_info;
course_info["first_name"] = first_name;
course_info["links"]["rate_my_prof"] = `http://www.ratemyprofessors.com/search.jsp?queryBy=teacherName&schoolName=university+of+texas+at+austin&queryoption=HEADER&query=${first_name} ${prof_name};&facetSearch=true`;
course_info["links"]["ecis"] = profname ? `http://utdirect.utexas.edu/ctl/ecis/results/index.WBX?&s_in_action_sw=S&s_in_search_type_sw=N&s_in_search_name=${prof_name}%2C%20${first_name}` :
`http://utdirect.utexas.edu/ctl/ecis/results/index.WBX?s_in_action_sw=S&s_in_search_type_sw=C&s_in_max_nbr_return=10&s_in_search_course_dept=${department}&s_in_search_course_num=${number}`;
}
function buildCourseLinks(course_info) {
let {
department,
number,
unique,
prof_name
} = course_info
links = {
"textbook": `https://www.universitycoop.com/adoption-search-results?sn=${semester_code}__${department}__${number}__${unique}`,
"syllabi": `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${number}&course_title=&unique=&instructor_first=&instructor_last=${prof_name}&course_type=In+Residence&search=Search`,
//default ones (before first name can be used)
"rate_my_prof": "http://www.ratemyprofessors.com/campusRatings.jsp?sid=1255",
"ecis": "http://utdirect.utexas.edu/ctl/ecis/results/index.WBX?"
}
course_info["links"] = links;
return course_info;
}
function buildBasicCourseInfo(row, course_name, individual) {
let {
name,
department,
number
} = separateCourseNameParts(course_name);
let instructor_text = $(row).find('td[data-th="Instructor"]').text();
let has_initial = instructor_text.indexOf(',') > 0;
course_info = {
"full_name": course_name,
"name": name,
"department": department,
"number": number,
"individual": individual ? individual : $(row).find('td[data-th="Unique"] a').prop('href'),
"register": $(row).find('td[data-th="Add"] a').prop('href'),
"unique": $(row).find('td[data-th="Unique"]').text(),
"status": $(row).find('td[data-th="Status"]').text(),
"prof_name": instructor_text ? has_initial ? capitalizeString(instructor_text.split(', ')[0]) : capitalizeString(instructor_text) : "Undecided",
"initial": instructor_text && has_initial ? instructor_text.split(', ')[1].substring(0, 1) : "",
"time_data": {
"days": $(row).find('td[data-th="Days"]>span').toArray().map(x => $(x).text().trim()),
"times": $(row).find('td[data-th="Hour"]>span').toArray().map(x => $(x).text().trim()),
"places": $(row).find('td[data-th="Room"]>span').toArray().map(x => $(x).text().trim())
},
"links": {}
}
return buildCourseLinks(course_info);
}
/*For a row, get all the course information and add the date-time-lines*/
function getCourseInfo(row) {
let course_name = "";
let course_row = {}
let individual = undefined;
if (isIndividualCoursePage()) {
course_name = $("#details h2").text();
course_row = $('table');
individual = document.URL;
} else {
$('table').find('tr').each(function () {
if ($(this).find('td').hasClass("course_header")) {
course_name = $(this).find('td').text() + "";
}
if ($(this).is(row)) {
course_row = row;
return false;
}
});
}
curr_course = buildBasicCourseInfo(course_row, course_name, individual);
getDescription(curr_course);
return curr_course;
}
function saveCourse() {
console.log(curr_course);
console.log(JSON.stringify(curr_course));
let {
full_name,
unique,
prof_name,
status,
individual,
register
} = curr_course;
let dtarr = getDayTimeArray(undefined, curr_course);
var c = new Course(full_name, unique, prof_name, dtarr, status, individual, register);
chrome.runtime.sendMessage({
command: "courseStorage",
course: c,
action: $("#saveCourse").val()
}, function (response) {
$("#saveCourse").text(response.label);
$("#saveCourse").val(response.value);
$("#snackbar").text(response.done);
toggleSnackbar();
chrome.runtime.sendMessage({
command: "updateCourseList"
});
});
}
/* Update the course list to show if the row contains a course that conflicts with the saved course is one of the saved courses */
function updateListConflictHighlighting(start = 0) {
chrome.runtime.sendMessage({
command: "getOptionsValue",
key: "courseConflictHighlight",
}, function (response) {
let canHighlight = response.value;
$('table').find('tr').each(function (i) {
if (i >= start) {
if (!($(this).find('td').hasClass("course_header")) && $(this).has('th').length == 0) {
var unique = $(this).find('td[data-th="Unique"]').text();
chrome.runtime.sendMessage({
command: "isSingleConflict",
dtarr: getDayTimeArray(this),
unique: unique
}, (response) => {
let {
isConflict,
alreadyContains,
conflictList
} = response
updateTextHighlighting($(this).find('td'), canHighlight, isConflict, alreadyContains, conflictList, $(this), unique);
});
}
}
});
});
}
function updateTextHighlighting(tds, canHighlight, isConflict, alreadyContains, conflictList, row, unique) {
conflict_texts = row.find('.tooltiptext');
let unique_list = conflictList.filter(function(course){
if(course.unique != unique){
return true;
}
return false;
}).map(function(course){
let { name, department, number} = separateCourseNameParts(course.coursename);
return `${department} ${number} (${course.unique})`;
});
if(isConflict && unique_list.length){
if(conflict_texts){
row.find('.tooltiptext').remove();
}
row.addClass('tooltip');
row.append(`<span class='tooltiptext'><span style='text-decoration: underline;'>Conflicts:<br></span> ${unique_list.join('<br>')}</span>`);
} else {
row.removeClass('tooltip');
conflict_texts.remove();
}
let current_color = rgb2hex(tds.css('color'));
if (isConflict && canHighlight && !alreadyContains) {
if (current_color != Colors.highlight_conflict){
tds.css('color', Colors.highlight_conflict).css('text-decoration', 'line-through').css('font-weight', 'normal')
}
} else if (!alreadyContains) {
if (tds.css('color') != Colors.highlight_default)
tds.css('color', Colors.highlight_default).css('text-decoration', 'none').css('font-weight', 'normal');
}
if (alreadyContains) {
if (tds.css('color') != Colors.highlight_saved)
tds.css('color', Colors.highlight_saved).css('text-decoration', 'none').css('font-weight', 'bold');
}
}
/* For a row, get the date-time-array for checking conflicts*/
function getDayTimeArray(row, course_info) {
var day_time_array = []
let days = course_info ? course_info["time_data"]["days"] : $(row).find('td[data-th="Days"]>span').toArray().map(x => $(x).text().trim());
let times = course_info ? course_info["time_data"]["times"] : $(row).find('td[data-th="Hour"]>span').toArray().map(x => $(x).text().trim());
let places = course_info ? course_info["time_data"]["places"] : $(row).find('td[data-th="Room"]>span').toArray().map(x => $(x).text().trim());
for (var i = 0; i < days.length; i++) {
let date = days[i];
let time = times[i];
let place = places[i];
for (var j = 0; j < date.length; j++) {
let letter = date.charAt(j);
if (letter == "T" && j < date.length - 1 && date.charAt(j + 1) == "H") {
day_time_array.push(["TH", convertTime(time), place]);
} else {
if (letter != "H")
day_time_array.push([letter, convertTime(time), place]);
}
}
}
return day_time_array;
}
function convertDateTimeArrToLine(date, time, place) {
let arr = separateDays(date)
let output = prettifyDaysText(arr)
let building = place.substring(0, place.search(/\d/) - 1);
building = building ? building : "Undecided Location";
return `${output} at ${time.replace(/\./g, '').replace(/\-/g, ' to ')} in <a style='font-size:medium' target='_blank' href='https://maps.utexas.edu/buildings/UTM/${building}'>${building}</>`;
}
function badData(course_data, res) {
return typeof res == 'undefined' || course_data["prof_name"] == "Undecided";
}
/*Query the grades database*/
function getDistribution(course_data, sem) {
toggleChartLoading(true);
let query = buildQuery(course_data, sem);
chrome.runtime.sendMessage({
command: "gradesQuery",
query: query
}, function (response) {
var res = response.data;
if (!sem) {
openDialog(course_data, res);
} else {
var data = badData(course_data, res) ? [] : res.values[0];
setChart(data);
}
});
}
function buildTitle(course_data) {
return `${course_data["name"]} (${course_data["department"]} ${course_data["number"]})`
}
function buildTimeTitle(course_info) {
$("h2.dateTimePlace").remove();
let {
days,
times,
places
} = course_info["time_data"]
var lines = [];
for (let i = 0; i < days.length; i++) {
var date = days[i];
var time = times[i];
var place = places[i];
lines.push($(`<h2 class="dateTimePlace">${convertDateTimeArrToLine(date, time, place)}</th>`));
}
return lines;
}
function buildProfTitle(course_data) {
const {
initial,
prof_name
} = course_data;
return `with ${initial?initial+". ":""}${prof_name}`;
}
function buildSemestersDropdown(course_data, res) {
$("#semesters").empty();
if (badData(course_data, res)) {
$("#semesters").append("<option>No Data</option>")
} else {
var semesters = res.values[0][18].split(",");
semesters.sort(semesterSort);
semesters.reverse().unshift('Aggregate');
var sems = [];
for (var i = 0; i < semesters.length; i++) {
sems.push($(`<option value="${semesters[i]}">${semesters[i]}</option>`));
}
$("#semesters").append(sems);
}
}
function displayBasicCourseInfo(course_info){
$("#title").text(buildTitle(course_info))
$("#topbuttons").before(buildTimeTitle(course_info));
$("#profname").text(buildProfTitle(course_info));
$("#myModal").fadeIn(Timing.fade_time);
console.log(course_info);
}
/*Open the modal and show all the data*/
function openDialog(course_info, res) {
displayBasicCourseInfo(course_info);
//initial text on the "save course button"
chrome.runtime.sendMessage({
command: "alreadyContains",
unique: course_info["unique"]
}, function (response) {
let button_text = response.alreadyContains ? "Remove Course -" : "Add Course +";
let button_val = response.alreadyContains ? "remove" : "add";
$("#saveCourse").text(button_text);
$("#saveCourse").val(button_val);
});
buildSemestersDropdown(course_info, res)
var data = []
if (!badData(course_info, res))
data = res.values[0];
allowClosing();
setChart(data);
}
function setChart(data) {
// set up the chart
toggleChartLoading(false);
Highcharts.chart('chart', buildChartConfig(data), function (chart) { // on complete
if (data.length == 0) {
//if no data, then show the message and hide the series
chart.renderer.text('Could not find data for this Instructor teaching this Course.', 100, 120)
.css({
fontSize: '20px',
width: '300px',
align: 'center',
left: '160px'
})
.add();
$.each(chart.series, function (i, ser) {
ser.hide();
});
}
});
}
var error_message = "<p style='color:red;font-style:bold'>You have been logged out. Please refresh the page and log back in using your UT EID and password.</p>";
function buildFormattedDescription(description_lines) {
let description = ""
for (let i in description_lines) {
let sentence = description_lines[i];
if (sentence.indexOf("Prerequisite") == 0)
sentence = `<li style='font-weight: bold;' class='descriptionli'>${sentence}</li>`;
else if (sentence.indexOf("May be") >= 0)
sentence = `<li style='font-style: italic;' class='descriptionli'>${sentence}</li>`;
else if (sentence.indexOf("Restricted to") == 0)
sentence = `<li style='color:red;' class='descriptionli'>${sentence}</li>`;
else
sentence = `<li class='descriptionli'>${sentence}</li>`;
description += sentence;
}
if (!description)
description = error_message;
return description;
}
function extractFirstName(response_node) {
let full_name = response_node.find('td[data-th="Instructor"]').text().split(', ');
let first = full_name[full_name.length - 1];
first = first.indexOf(' ') > 0 ? first.split(' ')[0] : first;
return capitalizeString(first);
}
function displayDescription(description) {
toggleDescriptionLoading(false);
$("#description").animate({
'opacity': 0
}, 200, function () {
$(this).html(description).animate({
'opacity': 1
}, 200);
});
}
/*Get the course description from the profurl and highlight the important elements, as well as set the eCIS, and rmp links.*/
function getDescription(course_info) {
toggleDescriptionLoading(true);
$.ajax({
url: course_info["individual"],
success: function (response) {
if (response) {
let response_node = htmlToNode(response);
description_lines = response_node.find('#details > p').toArray().map(x => $(x).text());
displayDescription(buildFormattedDescription(description_lines));
let first_name = extractFirstName(response_node);
updateLinks(course_info, first_name);
} else {
displayDescription(error_message);
}
}
});
}
function loadNextPages(num_pages) {
if (num_pages === undefined) num_pages = 1;
if (num_pages == 0) return;
chrome.runtime.sendMessage({
command: "getOptionsValue",
key: "loadAll",
}, function (response) {
if(response.value){
let link = next.prop('href');
if (done_loading && next && link) {
toggleLoadingPage(true);
$.get(link, function (response) {
if (response) {
var next_page = htmlToNode(response);
var current = $('tbody');
var old_length = $('tbody tr').length;
var last = current.find('.course_header>h2:last').text();
next = next_page.find("#next_nav_link");
toggleLoadingPage(false);
var new_rows = [];
next_page.find('tbody>tr').each(function () {
let has_course_header = $(this).find('td').hasClass("course_header");
if (!(has_course_header && $(this).has('th').length == 0))
$(this).append(Template.Main.extension_button());
if (!(has_course_header && last == $(this).find('td').text()))
new_rows.push($(this));
});
current.append(new_rows);
updateListConflictHighlighting(old_length + 1)
}
loadNextPages(num_pages-1);
}).fail(function () {
toggleLoadingPage(false);
$("#retrylabel").css('display', 'inline-block');
$('#retry').css('display', 'inline-block');
});
}
}
});
}
$("#myModal").on('click', '#saveCourse', function () {
setTimeout(function () {
saveCourse();
}, 0);
});
$("#Syllabi").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["syllabi"]);
}, Timing.button_delay);
});
$("#rateMyProf").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["rate_my_prof"]);
}, Timing.button_delay);
});
$("#eCIS").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["ecis"]);
}, Timing.button_delay);
});
$("#textbook").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["textbook"]);
}, Timing.button_delay);
});
$("#semesters").on('change', function () {
let sem = $(this).val();
sem = sem == "Aggregate" ? undefined : sem;
getDistribution(curr_course, sem);
});
$("#retry").click(function () {
$("#retrylabel").hide();
$(this).hide();
loadNextPages();
});
function toggleLoadingPage(loading) {
if (loading) {
done_loading = false;
$('#loader').css('display', 'inline-block');
$("#nextlabel").css('display', 'inline-block');
} else {
done_loading = true;
$('#loader').hide();
$("#nextlabel").hide();
}
}
function toggleChartLoading(loading) {
if (loading) {
$('#chartload').css('display', 'inline-block');
$("#chart").hide();
} else {
$('#chartload').hide();
$("#chart").show();
}
}
function toggleDescriptionLoading(loading) {
if (loading) {
$('#descload').css('display', 'inline-block');
} else {
$('#descload').hide();
}
}
function toggleSnackbar() {
setTimeout(function () {
$("#snackbar").attr("class", "show");
}, 200);
setTimeout(function () {
$("#snackbar").attr("class", "");
}, 3000);
}
function allowClosing() {
$('.close').click(function () {
close();
});
$('#myModal').click(function (event) {
if (event.target.id == 'myModal') {
close();
}
});
}
function close() {
$("#myModal").fadeOut(Timing.fade_time);
$("#snackbar").attr("class", "");
}
/*Listen for update mssage coming from popup or calendar or other course catalog pages*/
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
if (request.command == "updateCourseList") {
updateListConflictHighlighting(0);
}
}
);
$(document).keydown(function (e) {
/*Close Modal when hit escape*/
if (e.keyCode == 27) {
close();
} else if (e.keyCode == 13 && $('#myModal').is(':visible')) {
saveCourse();
}
});
$(window).scroll(function () {
if ($(document).height() <= $(window).scrollTop() + $(window).height() + 150)
loadNextPages();
});
$(window).on('load', function () {
loadNextPages(3);
});

164
js/import.js Normal file
View File

@@ -0,0 +1,164 @@
var waitlist;
var sem;
$(function () {
waitlist = !(window.location.href.includes('https://utdirect.utexas.edu/registration/classlist.WBX'));
sem = waitlist ? $('[name="s_ccyys"]').val() : $("option[selected='selected']").val();
if (waitlist) {
$("[href='#top']").before(Template.Import.import_button());
$("[name='wl_see_my_waitlists']").before(Template.Import.store_waitlist_message());
$("[name='wl_see_my_waitlists']").after(Template.Import.waitlist_import_button());
extractWaitlistInfo();
} else {
$("table").after(Template.Import.import_button());
}
$("#import").prepend("<div id='snackbar'>import snackbar..</div>");
$("#import").click(function () {
search_nodes = waitlist ? $(".tbg").last().find(".tbon>td:first-child") : $("tr>td:first-child");
$(search_nodes).each(function () {
importCourse($(this), true);
})
importButtonAnimation($(this));
});
$("#import_waitlist").click(function () {
search_nodes = $("tr.tb span:first-child");
$(search_nodes).each(function () {
importCourse($(this), false);
})
importButtonAnimation($(this));
});
});
function extractWaitlistInfo(){
let class_boxes = $("[name='wl_see_my_waitlists']>table");
let waitlist_info = [];
$(class_boxes).each(function(){
let data = $(this).find('tr.tb span');
let unique_num = $(data[0]).text().trim();
let class_name = $(data[1]).text().trim().split('\n').filter(part => part.trim() != '').map(part => part.trim()).join(' ');
let waitlist_size = $(this).find('tr.tbon:eq(2) td:eq(1)').text().trim().split(' of ')[1];
waitlist_info.push({
"id": unique_num,
"class": class_name,
"wait": waitlist_size,
"time": moment().format('DD-MM-YYYY HH:mm:ss')
});
});
console.log(waitlist_info);
return waitlist_info;
}
function importButtonAnimation(button) {
let is_waitlisted_button = $(button).attr('id') == "import_waitlist";
let return_text = is_waitlisted_button ? Text.waitlist_button_text_default : Text.button_text_default;
$(button).text(Text.button_success).css("background-color", Colors.open);
setTimeout(function () {
$(button).html(return_text).css('background-color', Colors.waitlisted);
}, 1000);
}
function importCourse(unique_node, force) {
let unique = $(unique_node).text().replace(/\s/g, '').substring(0,5);
link = `https://utdirect.utexas.edu/apps/registrar/course_schedule/${sem}/${unique}/`;
buildAddCourse(link, force)
}
function buildAddCourse(link, force) {
$.get(link, function (response) {
if (response) {
let simp_course = buildSimplifiedCourseObject(response, link, force);
chrome.runtime.sendMessage({
command: "courseStorage",
course: simp_course,
action: "add"
}, function () {
chrome.runtime.sendMessage({
command: "updateCourseList"
});
});
}
})
}
function buildSimplifiedCourseObject(response, link, force) {
let imported_course = getCourseObject(htmlToNode(response), link);
let {
full_name,
unique,
prof_name,
individual,
status,
register
} = curr_course;
let dtarr = getDayTimeArray(undefined, curr_course);
if(force === true) {
status = "open" //forces the green status for courses a user is already registered for
}
return new Course(full_name, unique, prof_name, dtarr, status, individual, register);
}
/*For a row, get all the course information and add the date-time-lines*/
function getCourseObject(response_node, individual) {
let course_name = $(response_node).find("#details h2").text();
let course_row = $(response_node).find('table');
curr_course = buildBasicCourseInfo(course_row, course_name, individual);
}
function buildBasicCourseInfo(row, course_name, individual) {
let {
name,
department,
number
} = separateCourseNameParts(course_name);
let instructor_text = $(row).find('td[data-th="Instructor"]').text();
let has_initial = instructor_text.indexOf(',') > 0;
course_info = {
"full_name": course_name,
"name": name,
"department": department,
"number": number,
"individual": individual ? individual : $(row).find('td[data-th="Unique"] a').prop('href'),
"register": $(row).find('td[data-th="Add"] a').prop('href'),
"unique": $(row).find('td[data-th="Unique"]').text(),
"status": $(row).find('td[data-th="Status"]').text(),
"prof_name": instructor_text ? has_initial ? capitalizeString(instructor_text.split(', ')[0]) : capitalizeString(instructor_text) : "Undecided",
"initial": instructor_text && has_initial ? instructor_text.split(', ')[1].substring(0, 1) : "",
"time_data": {
"days": $(row).find('td[data-th="Days"]>span').toArray().map(x => $(x).text().trim()),
"times": $(row).find('td[data-th="Hour"]>span').toArray().map(x => $(x).text().trim()),
"places": $(row).find('td[data-th="Room"]>span').toArray().map(x => $(x).text().trim())
},
"links": {}
}
return course_info;
}
/* For a row, get the date-time-array for checking conflicts*/
function getDayTimeArray(row, course_info) {
var day_time_array = []
let days = course_info ? course_info["time_data"]["days"] : $(row).find('td[data-th="Days"]>span').toArray().map(x => $(x).text().trim());
let times = course_info ? course_info["time_data"]["times"] : $(row).find('td[data-th="Hour"]>span').toArray().map(x => $(x).text().trim());
let places = course_info ? course_info["time_data"]["places"] : $(row).find('td[data-th="Room"]>span').toArray().map(x => $(x).text().trim());
for (var i = 0; i < days.length; i++) {
let date = days[i];
let time = times[i];
let place = places[i];
for (var j = 0; j < date.length; j++) {
let letter = date.charAt(j);
if (letter == "T" && j < date.length - 1 && date.charAt(j + 1) == "H") {
day_time_array.push(["TH", convertTime(time), place]);
} else {
if (letter != "H")
day_time_array.push([letter, convertTime(time), place]);
}
}
}
return day_time_array;
}

1
js/lib/fullcalendar.min.js vendored Normal file

File diff suppressed because one or more lines are too long

8737
js/lib/highcharts.js Normal file

File diff suppressed because it is too large Load Diff

6
js/lib/html2canvas.min.js vendored Normal file

File diff suppressed because one or more lines are too long

231
js/lib/ics.min.js vendored Normal file
View File

@@ -0,0 +1,231 @@
/*! ics.js Wed Aug 20 2014 17:23:02 */
var saveAs = saveAs || function (e) {
"use strict";
if (typeof e === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) {
return
}
var t = e.document,
n = function () {
return e.URL || e.webkitURL || e
},
r = t.createElementNS("http://www.w3.org/1999/xhtml", "a"),
o = "download" in r,
a = function (e) {
var t = new MouseEvent("click");
e.dispatchEvent(t)
},
i = /constructor/i.test(e.HTMLElement) || e.safari,
f = /CriOS\/[\d]+/.test(navigator.userAgent),
u = function (t) {
(e.setImmediate || e.setTimeout)(function () {
throw t
}, 0)
},
s = "application/octet-stream",
d = 1e3 * 40,
c = function (e) {
var t = function () {
if (typeof e === "string") {
n().revokeObjectURL(e)
} else {
e.remove()
}
};
setTimeout(t, d)
},
l = function (e, t, n) {
t = [].concat(t);
var r = t.length;
while (r--) {
var o = e["on" + t[r]];
if (typeof o === "function") {
try {
o.call(e, n || e)
} catch (a) {
u(a)
}
}
}
},
p = function (e) {
if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(e.type)) {
return new Blob([String.fromCharCode(65279), e], {
type: e.type
})
}
return e
},
v = function (t, u, d) {
if (!d) {
t = p(t)
}
var v = this,
w = t.type,
m = w === s,
y, h = function () {
l(v, "writestart progress write writeend".split(" "))
},
S = function () {
if ((f || m && i) && e.FileReader) {
var r = new FileReader;
r.onloadend = function () {
var t = f ? r.result : r.result.replace(/^data:[^;]*;/, "data:attachment/file;");
var n = e.open(t, "_blank");
if (!n) e.location.href = t;
t = undefined;
v.readyState = v.DONE;
h()
};
r.readAsDataURL(t);
v.readyState = v.INIT;
return
}
if (!y) {
y = n().createObjectURL(t)
}
if (m) {
e.location.href = y
} else {
var o = e.open(y, "_blank");
if (!o) {
e.location.href = y
}
}
v.readyState = v.DONE;
h();
c(y)
};
v.readyState = v.INIT;
if (o) {
y = n().createObjectURL(t);
setTimeout(function () {
r.href = y;
r.download = u;
a(r);
h();
c(y);
v.readyState = v.DONE
});
return
}
S()
},
w = v.prototype,
m = function (e, t, n) {
return new v(e, t || e.name || "download", n)
};
if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) {
return function (e, t, n) {
t = t || e.name || "download";
if (!n) {
e = p(e)
}
return navigator.msSaveOrOpenBlob(e, t)
}
}
w.abort = function () {};
w.readyState = w.INIT = 0;
w.WRITING = 1;
w.DONE = 2;
w.error = w.onwritestart = w.onprogress = w.onwrite = w.onabort = w.onerror = w.onwriteend = null;
return m
}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content);
if (typeof module !== "undefined" && module.exports) {
module.exports.saveAs = saveAs
} else if (typeof define !== "undefined" && define !== null && define.amd !== null) {
define("FileSaver.js", function () {
return saveAs
})
}
var ics = function (e, t) {
"use strict"; {
if (!(navigator.userAgent.indexOf("MSIE") > -1 && -1 == navigator.userAgent.indexOf("MSIE 10"))) {
void 0 === e && (e = "default"), void 0 === t && (t = "Calendar");
var r = -1 !== navigator.appVersion.indexOf("Win") ? "\r\n" : "\n",
n = [],
i = ["BEGIN:VCALENDAR", "PRODID:" + t, "VERSION:2.0"].join(r),
o = r + "END:VCALENDAR",
a = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
return {
events: function () {
return n
},
calendar: function () {
return i + r + n.join(r) + o
},
addEvent: function (t, i, o, l, u, s) {
if (void 0 === t || void 0 === i || void 0 === o || void 0 === l || void 0 === u) return !1;
if (s && !s.rrule) {
if ("YEARLY" !== s.freq && "MONTHLY" !== s.freq && "WEEKLY" !== s.freq && "DAILY" !== s.freq) throw "Recurrence rrule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'";
if (s.until && isNaN(Date.parse(s.until))) throw "Recurrence rrule 'until' must be a valid date string";
if (s.interval && isNaN(parseInt(s.interval))) throw "Recurrence rrule 'interval' must be an integer";
if (s.count && isNaN(parseInt(s.count))) throw "Recurrence rrule 'count' must be an integer";
if (void 0 !== s.byday) {
if ("[object Array]" !== Object.prototype.toString.call(s.byday)) throw "Recurrence rrule 'byday' must be an array";
if (s.byday.length > 7) throw "Recurrence rrule 'byday' array must not be longer than the 7 days in a week";
s.byday = s.byday.filter(function (e, t) {
return s.byday.indexOf(e) == t
});
for (var c in s.byday)
if (a.indexOf(s.byday[c]) < 0) throw "Recurrence rrule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'"
}
}
var g = new Date(l),
d = new Date(u),
f = new Date,
S = ("0000" + g.getFullYear().toString()).slice(-4),
E = ("00" + (g.getMonth() + 1).toString()).slice(-2),
v = ("00" + g.getDate().toString()).slice(-2),
y = ("00" + g.getHours().toString()).slice(-2),
A = ("00" + g.getMinutes().toString()).slice(-2),
T = ("00" + g.getSeconds().toString()).slice(-2),
b = ("0000" + d.getFullYear().toString()).slice(-4),
D = ("00" + (d.getMonth() + 1).toString()).slice(-2),
N = ("00" + d.getDate().toString()).slice(-2),
h = ("00" + d.getHours().toString()).slice(-2),
I = ("00" + d.getMinutes().toString()).slice(-2),
R = ("00" + d.getMinutes().toString()).slice(-2),
M = ("0000" + f.getFullYear().toString()).slice(-4),
w = ("00" + (f.getMonth() + 1).toString()).slice(-2),
L = ("00" + f.getDate().toString()).slice(-2),
O = ("00" + f.getHours().toString()).slice(-2),
p = ("00" + f.getMinutes().toString()).slice(-2),
Y = ("00" + f.getMinutes().toString()).slice(-2),
U = "",
V = "";
y + A + T + h + I + R != 0 && (U = "T" + y + A + T, V = "T" + h + I + R);
var B, C = S + E + v + U,
j = b + D + N + V,
m = M + w + L + ("T" + O + p + Y);
if (s)
if (s.rrule) B = s.rrule;
else {
if (B = "rrule:FREQ=" + s.freq, s.until) {
var x = new Date(Date.parse(s.until)).toISOString();
B += ";UNTIL=" + x.substring(0, x.length - 13).replace(/[-]/g, "") + "000000Z"
}
s.interval && (B += ";INTERVAL=" + s.interval), s.count && (B += ";COUNT=" + s.count), s.byday && s.byday.length > 0 && (B += ";BYDAY=" + s.byday.join(","))
}(new Date).toISOString();
var H = ["BEGIN:VEVENT", "UID:" + n.length + "@" + e, "CLASS:PUBLIC", "DESCRIPTION:" + i, "DTSTAMP;VALUE=DATE-TIME:" + m, "DTSTART;VALUE=DATE-TIME:" + C, "DTEND;VALUE=DATE-TIME:" + j, "LOCATION:" + o, "SUMMARY;LANGUAGE=en-us:" + t, "TRANSP:TRANSPARENT", "END:VEVENT"];
return B && H.splice(4, 0, B), H = H.join(r), n.push(H), H
},
download: function (e, t) {
if (n.length < 1) return !1;
t = void 0 !== t ? t : ".ics", e = void 0 !== e ? e : "calendar";
var a, l = i + r + n.join(r) + o;
if (-1 === navigator.userAgent.indexOf("MSIE 10")) a = new Blob([l]);
else {
var u = new BlobBuilder;
u.append(l), a = u.getBlob("text/x-vCalendar;charset=" + document.characterSet)
}
return saveAs(a, e + t), l
},
build: function () {
return !(n.length < 1) && i + r + n.join(r) + o
}
}
}
console.log("Unsupported Browser")
}
};

2
js/lib/jquery-3.3.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
js/lib/jquery.initialize.min.js vendored Normal file
View File

@@ -0,0 +1 @@
(function($){"use strict";var combinators=[" ",">","+","~"];var fraternisers=["+","~"];var complexTypes=["ATTR","PSEUDO","ID","CLASS"];function grok(msobserver){if(!$.find.tokenize){msobserver.isCombinatorial=true;msobserver.isFraternal=true;msobserver.isComplex=true;return}msobserver.isCombinatorial=false;msobserver.isFraternal=false;msobserver.isComplex=false;var token=$.find.tokenize(msobserver.selector);for(var i=0;i<token.length;i++){for(var j=0;j<token[i].length;j++){if(combinators.indexOf(token[i][j].type)!=-1)msobserver.isCombinatorial=true;if(fraternisers.indexOf(token[i][j].type)!=-1)msobserver.isFraternal=true;if(complexTypes.indexOf(token[i][j].type)!=-1)msobserver.isComplex=true}}}var MutationSelectorObserver=function(selector,callback,options){this.selector=selector.trim();this.callback=callback;this.options=options;grok(this)};var msobservers=[];msobservers.initialize=function(selector,callback,options){var seen=[];var callbackOnce=function(){if(seen.indexOf(this)==-1){seen.push(this);$(this).each(callback)}};$(options.target).find(selector).each(callbackOnce);var msobserver=new MutationSelectorObserver(selector,callbackOnce,options);this.push(msobserver);var observer=new MutationObserver(function(mutations){var matches=[];for(var m=0;m<mutations.length;m++){if(mutations[m].type=="attributes"){if(mutations[m].target.matches(msobserver.selector))matches.push(mutations[m].target);if(msobserver.isFraternal)matches.push.apply(matches,mutations[m].target.parentElement.querySelectorAll(msobserver.selector));else matches.push.apply(matches,mutations[m].target.querySelectorAll(msobserver.selector))}if(mutations[m].type=="childList"){for(var n=0;n<mutations[m].addedNodes.length;n++){if(!(mutations[m].addedNodes[n]instanceof Element))continue;if(mutations[m].addedNodes[n].matches(msobserver.selector))matches.push(mutations[m].addedNodes[n]);if(msobserver.isFraternal)matches.push.apply(matches,mutations[m].addedNodes[n].parentElement.querySelectorAll(msobserver.selector));else matches.push.apply(matches,mutations[m].addedNodes[n].querySelectorAll(msobserver.selector))}}}for(var i=0;i<matches.length;i++)$(matches[i]).each(msobserver.callback)});var defaultObeserverOpts={childList:true,subtree:true,attributes:msobserver.isComplex};observer.observe(options.target,options.observer||defaultObeserverOpts);return observer};$.fn.initialize=function(callback,options){return msobservers.initialize(this.selector,callback,$.extend({},$.initialize.defaults,options))};$.initialize=function(selector,callback,options){return msobservers.initialize(selector,callback,$.extend({},$.initialize.defaults,options))};$.initialize.defaults={target:document.documentElement,observer:null}})(jQuery);

1
js/lib/moment.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

60
js/options.js Normal file
View File

@@ -0,0 +1,60 @@
var manifestData = chrome.runtime.getManifest();
$("#version").text(manifestData.version);
chrome.storage.sync.get("options", function (data) {
if (data.options) {
console.log(data.options);
Object.keys(data.options).forEach(key => {
let enabled = data.options[key];
$("#options_container").append(Template.Options.options_row(key, enabled));
});
}
});
$("body").on("click", "button", function () {
let key = $(this).attr("id");
let old_status = $(this).val() === "true";
let new_status = !old_status;
chrome.runtime.sendMessage(
{
command: "setOptionsValue",
key: key,
value: new_status,
},
function (response) {
console.log(response.value);
toggle(key, response.value);
updateAllTabsCourseList();
}
);
});
$.get("https://api.github.com/repos/sghsri/UT-Registration-Plus/stats/contributors", data => {
data = data.sort((a, b) => b.total - a.total);
console.log("data", data);
for (var contributorData of data) {
$.get(`https://api.github.com/users/${contributorData.author.login}`, userData => {
let fullData = { ...contributorData, ...userData };
let { login, avatar_url, html_url, name } = fullData;
if(name){
$("#contributor-list").append(Template.Options.contributor_card(login, name, avatar_url, html_url));
}
else{
$("#contributor-list").append(Template.Options.contributor_card("", login, avatar_url, html_url));
}
});
}
});
$("body").on("click", ".contributor-card", function () {
console.log("hello world");
window.open($(this).data("url"), "_blank");
});
function toggle(key, value) {
let button_text = value ? "Turn Off" : "Turn On";
let button_color = value ? Colors.closed : Colors.open;
$(`#${key}`).text(button_text);
$(`#${key}`).css("background", button_color);
$(`#${key}`).val(value);
}

437
js/popup.js Normal file
View File

@@ -0,0 +1,437 @@
var courses;
setCourseList();
getSemesters();
getDepartments();
var can_remove = true;
function setCourseList() {
$("#courseList").empty();
chrome.storage.sync.get("savedCourses", function (data) {
updateConflicts();
courses = data.savedCourses;
handleEmpty();
let hours = 0;
// build and append the course list element
for (var i = 0; i < courses.length; i++) {
let { coursename, unique, profname, status, datetimearr } = courses[i];
profname = capitalizeString(profname);
let line = buildTimeLines(datetimearr);
let list_tile_color = getStatusColor(status);
let list_sub_color = getStatusColor(status, true);
let { department, number } = separateCourseNameParts(coursename);
let class_length = parseInt(number.charAt(0));
let multi_semester_code = number.slice(-1);
if (["A", "B"].includes(multi_semester_code)) {
hours += Math.floor(class_length / 2);
} else if (["X", "Y", "Z"].includes(multi_semester_code)) {
hours += Math.floor(class_length / 3);
} else {
hours += class_length;
}
let list_html = Template.Popup.list_item(i, list_tile_color, unique, department, number, profname, list_sub_color, line);
$("#courseList").append(list_html);
}
$("#meta-metric").text(hours);
});
}
/* convert from the dtarr and maek the time lines*/
function buildTimeLines(datetimearr) {
let lines = convertDateTimeArrToLine(datetimearr);
let output = "";
if (lines.length == 0) {
output = "<span style='font-size:medium;'>This class has no meeting times.</span>";
} else {
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
output += Template.Popup.line(line);
}
}
return output;
}
/* Update the conflict messages */
function updateConflicts() {
chrome.runtime.sendMessage(
{
command: "checkConflicts",
},
function (response) {
console.log("updateConflicts -> response", response);
if (response.isConflict) {
var between = response.between;
let conflict_message = "";
for (var i = 0; i < between.length; i++) {
let courseA = between[i][0];
let courseB = between[i][1];
conflict_message += `CONFLICT: ${formatShortenedCourseName(courseA)} and ${formatShortenedCourseName(courseB)}`;
if (i != between.length - 1) conflict_message += "<br>";
}
$(Template.Popup.conflict_message(conflict_message)).prependTo("#courseList").hide().fadeIn(200);
}
}
);
}
/* prettify the name for the conflict messages*/
function formatShortenedCourseName(course) {
let { number, department } = separateCourseNameParts(course.coursename);
return `${department} ${number} (${course.unique})`;
}
$(document).click(function (event) {
$target = $(event.target);
// If we're not clicking on search button or search popup, and popup is visible, hide it
if (!$target.closest("#search").length && !$target.closest("#search-popup").length && $("#search-popup").is(":visible")) {
hideSearchPopup();
}
// If we're not clicking on import/export button or imp/exp popup, and popup is visible, hide it
if (!$target.closest("#impexp").length && !$target.closest("#import-export-popup").length && $("#import-export-popup").is(":visible")) {
hideImportExportPopup();
}
});
$("#clear").click(function () {
chrome.storage.sync.set({
savedCourses: [],
});
$("#courseList").empty();
updateAllTabsCourseList();
showEmpty();
});
$("#RIS").click(function () {
chrome.tabs.create({
url: "https://utdirect.utexas.edu/registrar/ris.WBX",
});
});
$("#calendar").click(function () {
chrome.tabs.create({
url: "calendar.html",
});
});
$("#impexp").click(function () {
if ($("#impexp>i").text() == "close") {
hideImportExportPopup();
} else {
if ($("#search>i").text() == "close") {
hideSearchPopup();
}
showImportExportPopup();
}
});
$("#search").click(function () {
if ($("#search>i").text() == "close") {
hideSearchPopup();
} else {
if ($("#impexp>i").text() == "close") {
hideImportExportPopup();
}
showSearchPopup();
}
});
$("#import-class").click(function () {
$("#import_input").click();
console.log("back to improting");
});
function isImportedValid(imported_courses) {
return imported_courses && imported_courses.length && (imported_courses.length == 0 || validateCourses(imported_courses));
}
$("#import_input").change(function (e) {
console.log("hello");
var files = e.target.files;
var reader = new FileReader();
reader.onload = function () {
try {
var imported_courses = JSON.parse(this.result);
if (isImportedValid(imported_courses)) {
chrome.storage.sync.set({
savedCourses: imported_courses,
});
updateAllTabsCourseList();
setCourseList();
hideImportExportPopup();
$("#import_input").val("");
} else {
Alert("There was an error.");
}
} catch (err) {
console.log(err);
}
};
reader.readAsText(files[0]);
});
function exportCourses(url) {
var exportlink = document.createElement("a");
exportlink.setAttribute("href", url);
exportlink.setAttribute("download", "my_courses.json");
exportlink.click();
}
function createBlob(export_courses) {
return new Blob([JSON.stringify(export_courses, null, 4)], {
type: "octet/stream",
});
}
$("#export-class").click(function () {
chrome.storage.sync.get("savedCourses", function (data) {
let export_courses = data.savedCourses;
if (export_courses.length > 0) {
let url = window.URL.createObjectURL(createBlob(export_courses));
exportCourses(url);
} else {
alert("No Saved Courses to Export.");
}
hideImportExportPopup();
});
});
function openSearch(semester, department, level, courseCode) {
var link = "";
if (courseCode) {
link = `https://utdirect.utexas.edu/apps/registrar/course_schedule/${semester}/results/?search_type_main=COURSE&fos_cn=${department}&course_number=${courseCode}`;
} else {
link = `https://utdirect.utexas.edu/apps/registrar/course_schedule/${semester}/results/?fos_fl=${department}&level=${level}&search_type_main=FIELD`;
}
chrome.tabs.create({ url: link });
}
$("#search-class").click(() => {
let semester = $("#semesters").find(":selected").val();
let department = $("#department").find(":selected").val();
let level = $("#level").find(":selected").val();
let courseCode = $("#courseCode").val();
openSearch(semester, department, level, courseCode);
});
$("#options_button").click(function () {
chrome.tabs.create({
url: "options.html",
});
});
$("#courseList")
.on("mouseover", ".copy_button", function () {
$(this).addClass("shadow");
})
.on("mouseleave", ".copy_button", function () {
$(this).removeClass("shadow");
});
$("#courseList").on("click", ".copy_button", function (e) {
e.stopPropagation();
copyButtonAnimation($(this));
let unique = $(this).val();
copyUnique(unique);
});
function copyUnique(unique) {
var temp = $("<input>");
$("body").append(temp);
temp.val(unique).select();
document.execCommand("copy");
temp.remove();
}
$("#courseList").on("click", "li", function () {
let clicked_item = $(this).closest("li");
let curr_course = courses[$(clicked_item).attr("id")];
handleMoreInfo(clicked_item, curr_course);
handleRegister(clicked_item, curr_course);
handleRemove(clicked_item, curr_course);
toggleTimeDropdown(clicked_item);
});
function handleRegister(clicked_item, curr_course) {
let { status, registerlink } = curr_course;
let register_button = $(clicked_item).find("#register");
let can_not_register = canNotRegister(status, registerlink);
let register_text = can_not_register ? "Can't Register" : status.includes("waitlisted") ? "Join Waitlist" : "Register";
let register_color = can_not_register ? Colors.closed : status.includes("waitlisted") ? Colors.waitlisted : Colors.open;
if (!status) {
register_text = "No Status";
register_color = Colors.no_status;
}
$(register_button).text(register_text).css("background-color", register_color);
if (!can_not_register) {
$(register_button).click(function () {
setCurrentTabUrl(registerlink);
});
}
}
function handleRemove(clicked_item, curr_course) {
let list = $(clicked_item).closest("ul");
$(clicked_item)
.find("#listRemove")
.click(function () {
if (can_remove) {
can_remove = false;
$(list)
.find("#conflict")
.fadeOut(300, function () {
$(clicked_item).remove();
});
subtractHours(curr_course);
chrome.runtime.sendMessage(
{
command: "courseStorage",
course: curr_course,
action: "remove",
},
() => {
$(clicked_item).fadeOut(200);
if ($(list).children(":visible").length === 1) showEmpty();
can_remove = true;
updateConflicts();
updateAllTabsCourseList();
}
);
}
});
}
function subtractHours(curr_course) {
let curr_total_hours = parseInt($("#meta-metric").text());
let curr_course_number = separateCourseNameParts(curr_course.coursename).number;
let class_length = parseInt(curr_course_number.charAt(0));
let multi_semester_code = curr_course_number.slice(-1);
if (["A", "B"].includes(multi_semester_code)) {
$("#meta-metric").text(curr_total_hours - Math.floor(class_length / 2));
} else if (["X", "Y", "Z"].includes(multi_semester_code)) {
$("#meta-metric").text(curr_total_hours - Math.floor(class_length / 3));
} else {
$("#meta-metric").text(curr_total_hours - class_length);
}
}
function handleMoreInfo(clicked_item, curr_course) {
$(clicked_item)
.find("#listMoreInfo")
.click(function () {
openMoreInfoWithOpenModal(curr_course.link);
});
}
function handleEmpty() {
if (courses.length != 0) {
$("#empty").hide();
$("#courseList").show();
} else {
showEmpty();
}
}
function copyButtonAnimation(copy_button) {
$(copy_button).find("i").text("check");
$(copy_button).stop(true, false).removeAttr("style").removeClass("shadow", {
duration: 200,
});
$(copy_button)
.find("i")
.delay(400)
.queue(function (n) {
$(this).text("content_copy");
$(this).parent().removeClass("shadow");
if ($(this).parent().is(":hover")) {
$(this).parent().addClass("shadow");
}
n();
});
}
function toggleTimeDropdown(clicked_item) {
let more_info_button = $(clicked_item).find("#moreInfo");
let arrow = $(clicked_item).find("#arrow");
if ($(more_info_button).is(":hidden")) {
$(more_info_button).fadeIn(200);
$(arrow).css("transform", "rotate(90deg)");
} else {
$(more_info_button).fadeOut(200);
$(arrow).css("transform", "");
}
}
function showEmpty() {
$("#courseList").hide();
$("#empty").fadeIn(200);
$("#main").html(Text.emptyText());
$("#meta-metric").text("0");
}
function hideSearchPopup() {
$("#search>i").text("search");
$("#semcon").hide();
$("#depcon").hide();
$("#semesters").hide();
$("#levcon").hide();
$("#search-popup").addClass("hide");
}
function showSearchPopup() {
$("#search>i").text("close");
$("#class_id_input").show();
$("#semesters").show();
$("#semcon").show();
$("#depcon").show();
$("#levcon").show();
$("#search-popup").removeClass("hide");
}
function hideImportExportPopup() {
$("#import-export-popup").addClass("hide");
$("#impexp>i").text("import_export");
}
function showImportExportPopup() {
$("#impexp>i").text("close");
$("#import-export-popup").removeClass("hide");
}
function getSemesters() {
chrome.runtime.sendMessage(
{
command: "currentSemesters",
},
function (response) {
let { semesters } = response;
let semester_names = Object.keys(semesters);
for (let i = 0; i < semester_names.length; i++) {
let name = semester_names[i];
$("#semesters").append(`<option value='${semesters[name]}'>${name}</option>`);
}
}
);
}
function getDepartments() {
chrome.runtime.sendMessage(
{
command: "currentDepartments",
},
function (response) {
let { departments } = response;
console.log(departments);
for (let i = 0; i < departments.length; i++) {
let abv = departments[i];
$("#department").append(`<option value='${abv}'>${abv}</option>`);
}
// $("#department").val('C S');
}
);
}

242
js/utPlanner.js Normal file
View File

@@ -0,0 +1,242 @@
let semester_code = "";
curr_course = {}
chrome.runtime.sendMessage({
command: "currentSemesters"
}, function(response){
let semester_text = $('.row:contains(Semester)').find('span').text();
let key = semester_text.split(' ').reverse().join(' ');
semester_code = response.semesters[key];
});
$.initialize("table.section-detail-grid", function () {
$(this).find('thead>tr').append('<th> Plus</th')
$(this).find('tbody>tr').each(function () {
$(this).append(Template.Main.extension_button());
})
});
$("body").prepend(Template.UTPlanner.modal());
$("body").on('click', '#distButton', function () {
var row = $(this).closest('tr');
$('.modal-content').stop().animate({ scrollTop: 0 }, 500);
$(this).blur();
getCourseInfo(row)
});
function getCourseInfo(row) {
let rowdata = $(row).find('td').slice(3).toArray().map(x => $(x).text().trim());
let [uniquenum, department, coursenum, coursename, profname, notes, rawtime] = rowdata
let profinit = ""
if (profname !== undefined && profname != "Staff") {
profinit = profname.split(',')[1].trim();
profname = profname.split(',')[0].trim();
}
let times = rawtime.split('\n').map(x => x.trim());
var course_data = {
"unique": uniquenum,
"department": department,
"number": coursenum,
"name": coursename,
"prof_name": profname,
"initial": profinit,
"notes": notes,
"individual": `https://utdirect.utexas.edu/apps/registrar/course_schedule/${semester_code}/${uniquenum}/`,
"times": times,
}
curr_course = buildCourseLinks(course_data);
getDistribution(course_data);
var modal = document.getElementById('myModal');
window.onclick = function (event) {
if (event.target == modal) {
close();
}
}
}
function buildCourseLinks(course_info) {
console.log(semester_code);
let {
department,
number,
unique,
prof_name
} = course_info
links = {
"textbook": `https://www.universitycoop.com/adoption-search-results?sn=${semester_code}__${department}__${number}__${unique}`,
"syllabi": `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${number}&course_title=&unique=&instructor_first=&instructor_last=${prof_name}&course_type=In+Residence&search=Search`,
}
course_info["links"] = links;
return course_info;
}
function badData(course_data, res) {
return typeof res == 'undefined' || course_data["prof_name"] == "Staff";
}
$("#semesters").on('change', function () {
var sem = $(this).val();
sem = sem == "Aggregate" ? undefined : sem;
getDistribution(curr_course, sem);
});
$("#Syllabi").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["syllabi"]);
}, Timing.button_delay);
});
$("#textbook").click(function () {
setTimeout(function () {
window.open(curr_course["links"]["textbook"]);
}, Timing.button_delay);
});
$("#moreInfo").click(function () {
openMoreInfoWithOpenModal(curr_course["individual"]);
});
function toggleChartLoading(loading) {
if (loading) {
$('#chartload').css('display', 'inline-block');
$("#chart").hide();
} else {
$('#chartload').hide();
$("#chart").show();
}
}
function openDialog(course_data, res) {
console.log(course_data);
$("#title").text(buildTitle(course_data))
$("#topbuttons").before(buildTimeTitle(course_data["times"]));
$("#profname").text(buildProfTitle(course_data));
$("#myModal").fadeIn(Timing.fade_time);
buildSemestersDropdown(course_data, res)
var data = []
if (!badData(course_data, res))
data = res.values[0];
setChart(data);
allowClosing();
}
function buildProfTitle(course_data) {
const {
initial,
prof_name
} = course_data;
return `with ${initial?initial+". ":""}${prof_name}`;
}
function buildSemestersDropdown(course_data, res) {
$("#semesters").empty();
if (badData(course_data, res)) {
$("#semesters").append("<option>No Data</option>")
} else {
var semesters = res.values[0][18].split(",");
semesters.sort(semesterSort);
semesters.reverse().unshift('Aggregate');
var sems = [];
for (var i = 0; i < semesters.length; i++) {
sems.push($(`<option value="${semesters[i]}">${semesters[i]}</option>`));
}
$("#semesters").append(sems);
}
}
/*Query the grades database*/
function getDistribution(course_data, sem) {
toggleChartLoading(true);
let query = buildQuery(course_data, sem);
chrome.runtime.sendMessage({
command: "gradesQuery",
query: query
}, function (response) {
var res = response.data;
if (!sem) {
openDialog(course_data, res);
} else {
var data = badData(course_data, res) ? [] : res.values[0];
setChart(data);
}
});
}
function buildTitle(course_data) {
return `${course_data["name"]} (${course_data["department"]} ${course_data["number"]})`
}
function buildTimeTitle(times) {
$("h2.dateTimePlace").remove();
var lines = []
for (var i = 0; i < times.length; i++) {
date = times[i].substring(0, times[i].indexOf(' ')).toUpperCase();
time = times[i].substring(times[i].indexOf(' ') + 1, times[i].lastIndexOf('-')).trim();
place = times[i].substring(times[i].lastIndexOf('-') + 1).trim();
lines.push($(`<h2 class="dateTimePlace">${makeLine(date, time, place)}</th>`));
}
return lines
}
function makeLine(date, time, place) {
var arr = separateDays(date)
var output = prettifyDaysText(arr)
var building = place.substring(0, place.search(/\d/) - 1);
building = building == "" ? "Undecided Location" : building;
return `${output} at ${time.replace(/\./g, '').replace(/\-/g, ' to ')} in <a style='font-size:medium' target='_blank' href='https://maps.utexas.edu/buildings/UTM/${building}'>${building}</>`;
}
function setChart(data) {
//set up the chart
toggleChartLoading(false);
chart = Highcharts.chart('chart', buildChartConfig(data), function (chart) { // on complete
if (data.length == 0) {
//if no data, then show the message and hide the series
chart.renderer.text('Could not find data for this Instructor teaching this Course.', 100, 120)
.css({
fontSize: '20px',
width: '300px',
align: 'center',
left: '160px'
})
.add();
$.each(chart.series, function (i, ser) {
ser.hide();
});
}
});
}
function standardizeName(department, number, name){
return `${department} ${number} ${name}`
}
function allowClosing() {
$('.close').click(function () {
close();
});
$('#myModal').click(function (event) {
if (event.target.id == 'myModal') {
close();
}
});
}
function close() {
$("#myModal").fadeOut(Timing.fade_time);
$("#snackbar").attr("class", "");
}
$(document).keydown(function (e) {
/*Close Modal when hit escape*/
if (e.keyCode == 27) {
close();
}
});

386
js/util.js Normal file
View File

@@ -0,0 +1,386 @@
const days = new Map([
["M", "Monday"],
["T", "Tuesday"],
["W", "Wednesday"],
["TH", "Thursday"],
["F", "Friday"]
]);
function getStatusColor(status, sub = false) {
let color = "black";
if (status.includes("open")) {
color = sub ? Colors.open_light : Colors.open;
} else if (status.includes("waitlisted")) {
color = sub ? Colors.waitlisted_light : Colors.waitlisted;
} else if (status.includes("closed") || status.includes("cancelled")) {
color = sub ? Colors.closed_light : Colors.closed;
} else {
color = sub ? Colors.no_status_light : Colors.no_status;
}
return color;
}
function buildQuery(course_data, sem) {
let query = !sem ? "select * from agg" : "select * from grades";
query += " where dept like '%" + course_data["department"] + "%'";
query += " and prof like '%" + course_data["prof_name"].replace(/'/g, "") + "%'";
query += " and course_nbr like '%" + course_data["number"] + "%'";
if (sem) {
query += "and sem like '%" + sem + "%'";
}
return query + "order by a1+a2+a3+b1+b2+b3+c1+c2+c3+d1+d2+d3+f desc";
}
/*Course object for passing to background*/
function Course(coursename, unique, profname, datetimearr, status, link, registerlink) {
this.coursename = coursename;
this.unique = unique;
this.profname = profname;
this.datetimearr = datetimearr;
this.status = status;
this.link = link;
this.registerlink = registerlink;
}
function capitalizeString(string) {
//if one word, and if multiple words:
let output = "";
words = string.split(/[. ,\/ -]/);
for (let i in words) {
word = words[i];
capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
output += capitalizedWord + " ";
}
return output.trim();
}
function separateCourseNameParts(name) {
let num_index = name.search(/\d/);
department = name.substring(0, num_index).trim();
number = name.substring(num_index, name.indexOf(" ", num_index)).trim();
name = capitalizeString(name.substring(name.indexOf(" ", num_index)).trim());
return {
name: name,
department: department,
number: number
}
}
function separateDays(date, simple=false) {
let arr = [];
for (var i = 0; i < date.length; i++) {
let letter = date.charAt(i);
let separated_letter = letter;
if (letter == "T" && i < date.length - 1 && date.charAt(i + 1) == "H") {
arr.push(simple ? "TH" : days.get("TH"));
} else {
if (letter != "H") {
arr.push(simple ? letter : days.get(letter));
}
}
}
return arr;
}
/*Convert time to 24hour format*/
function convertTime(time) {
var converted = time.replace(/\./g, '').split("-");
for (var i = 0; i < 2; i++) {
converted[i] = moment(converted[i], ["h:mm A"]).format("HH:mm");
}
return converted;
}
function prettifyDaysText(arr) {
var output = "";
if (arr.length > 2) {
for (var i = 0; i < arr.length; i++) {
if (i < arr.length - 1)
output += arr[i] + ", "
if (i == arr.length - 2)
output += "and ";
if (i == arr.length - 1)
output += arr[i];
}
} else if (arr.length == 2) {
output = arr[0] + " and " + arr[1];
} else {
output = arr[0];
}
return output
}
function isIndividualCoursePage(){
return $("#textbook_button").length != 0;
}
function updateAllTabsCourseList() {
chrome.tabs.query({}, function (tabs) {
for (var i = 0; i < tabs.length; i++) {
chrome.tabs.sendMessage(tabs[i].id, {
command: "updateCourseList"
});
}
});
}
function htmlToNode(response) {
return $('<div/>').html(response).contents();
}
function setCurrentTabUrl(link) {
chrome.tabs.query({
currentWindow: true,
active: true
}, function (tab) {
chrome.tabs.update(tab.id, {
url: link
});
});
}
function openMoreInfoWithOpenModal(link){
chrome.runtime.sendMessage({ command: "setOpen", url: link });
}
function semesterSort(semA, semB) {
let semOrder = {
"Spring": 0,
"Fall": 1,
"Summer": 2,
"Winter": 3
}
let aName = semA.split(' ')[0];
let aYear = parseInt(semA.split(' ')[1]);
let bName = semB.split(' ')[0];
let bYear = parseInt(semB.split(' ')[1]);
if (aYear < bYear)
return -1;
if (aYear > bYear)
return 1;
if (semOrder[aName] < semOrder[bName])
return -1;
if (semOrder[aName] > semOrder[bName])
return 1;
return 0;
}
/* convert from the dtarr and maek the time lines*/
function convertDateTimeArrToLine(datetimearr) {
var output = [];
var dtmap = makeDateTimeMap(datetimearr);
var timearr = Array.from(dtmap.keys());
var temporary = Array.from(dtmap.values())
var dayarr = []
var locarr = []
for(x in temporary) {
dayarr.push(temporary[x][0])
locarr.push(temporary[x][1])
}
for (var i = 0; i < dayarr.length; i++) {
//var place = findLocation(dayarr[i], timearr[i], datetimearr);
var place = locarr[i]
var building = place.substring(0, place.search(/\d/)).trim();
building = building ? building : "Undecided Location"
var timearrsplit = timearr[i].split(',')
output.push({
"days": dayarr[i],
"start_time": timearrsplit[0],
"end_time": timearrsplit[1],
"location_link": `https://maps.utexas.edu/buildings/UTM/${building}`,
"location_full": place
})
}
return output;
}
function makeDateTimeMap(datetimearr) {
var dtmap = new Map([]);
for (var i = 0; i < datetimearr.length; i++) {
datetimearr[i][1][0] = moment(datetimearr[i][1][0], ["HH:mm A"]).format("h:mm A");
datetimearr[i][1][1] = moment(datetimearr[i][1][1], ["HH:mm A"]).format("h:mm A");
}
for (var i = 0; i < datetimearr.length; i++) {
var instance = datetimearr[i]
var day = String(instance[0])
var timeslot = String(instance[1])
var location = String(instance[2])
var key = timeslot + "," + location
if (dtmap.has(key) && dtmap.get(key)[1] === location) {
dtmap.set(key, [dtmap.get(key)[0] + day, location]);
} else {
dtmap.set(key, [day, location]);
}
}
return dtmap
}
//find the location of a class given its days and timearrs.
function findLocation(day, timearr, datetimearr) {
for (let i = 0; i < datetimearr.length; i++) {
var dtl = datetimearr[i];
if (day.includes(dtl[0])) {
if (JSON.stringify(timearr) == JSON.stringify(reformatDateTime(dtl[1]))) {
return dtl[2];
}
}
}
}
function validateCourses(courses) {
for (var i = 0; i < courses.length; i++) {
if (!validateCourseObject(courses[i])) {
return false;
}
}
return true;
}
function validateCourseObject(course) {
var is_valid = true;
var props = ["coursename", "datetimearr", "link", "profname", "status", "unique"];
for (let j = 0; j < props.length; j++) {
is_valid &= course.hasOwnProperty(props[j]);
}
return is_valid;
}
function reformatDateTime(dtl1) {
let output = "";
for (let i = 0; i < dtl1.length; i++) {
output += dtl1[i];
if (i != dtl1.length - 1) {
output += ",";
}
}
return output;
}
function rgb2hex(rgb) {
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
function hex(x) {
return ("0" + parseInt(x).toString(16)).slice(-2);
}
return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
}
function buildChartConfig(data) {
return {
chart: {
type: 'column',
backgroundColor: ' #fefefe',
spacingLeft: 10
},
title: {
text: null
},
subtitle: {
text: null
},
legend: {
enabled: false
},
xAxis: {
title: {
text: 'Grades'
},
categories: [
'A',
'A-',
'B+',
'B',
'B-',
'C+',
'C',
'C-',
'D+',
'D',
'D-',
'F'
],
crosshair: true
},
yAxis: {
min: 0,
title: {
text: 'Students'
}
},
credits: {
enabled: false
},
lang: {
noData: "The professor hasn't taught this class :("
},
tooltip: {
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
pointFormat: '<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
footerFormat: '</table>',
shared: true,
useHTML: true
},
plotOptions: {
bar: {
pointPadding: 0.2,
borderWidth: 0
},
series: {
animation: {
duration: 700
}
}
},
series: [{
name: 'Grades',
data: [{
y: data[6],
color: '#4CAF50'
}, {
y: data[7],
color: '#8BC34A'
}, {
y: data[8],
color: '#CDDC39'
}, {
y: data[9],
color: '#FFEB3B'
}, {
y: data[10],
color: '#FFC107'
}, {
y: data[11],
color: '#FFA000'
}, {
y: data[12],
color: '#F57C00'
}, {
y: data[13],
color: '#FF5722'
}, {
y: data[14],
color: '#FF5252'
}, {
y: data[15],
color: '#E64A19'
}, {
y: data[16],
color: '#F44336'
}, {
y: data[17],
color: '#D32F2F'
}]
}]
}
}
function canNotRegister(status, register_link) {
return status.includes("closed") || status.includes("cancelled") || !status || !register_link
}

70
manifest.json Normal file
View File

@@ -0,0 +1,70 @@
{
"manifest_version": 2,
"name": "UT Registration Plus",
"version": "1.2.2.7",
"options_page": "options.html",
"description": "Improves the course registration process at the University of Texas at Austin!",
"permissions": [
"storage",
"*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*",
"*://*.utexas.collegescheduler.com/*",
"*://*.catalog.utexas.edu/ribbit/",
"*://*.registrar.utexas.edu/schedules/*",
"*://*.login.utexas.edu/login/*"
],
"content_scripts": [
{
"css": ["css/styles.css"],
"js": [
"js/config.js",
"js/lib/moment.min.js",
"js/lib/highcharts.js",
"js/lib/jquery-3.3.1.min.js",
"js/lib/jquery.initialize.min.js",
"js/util.js",
"js/Template.js",
"js/courseCatalog.js"
],
"matches": ["https://utdirect.utexas.edu/apps/registrar/course_schedule/*"]
},
{
"css": ["css/styles.css"],
"js": [
"js/config.js",
"js/lib/moment.min.js",
"js/lib/highcharts.js",
"js/lib/jquery-3.3.1.min.js",
"js/lib/jquery.initialize.min.js",
"js/util.js",
"js/Template.js",
"js/utPlanner.js"
],
"matches": ["https://utexas.collegescheduler.com/*"]
},
{
"css": ["css/styles.css"],
"js": ["js/config.js", "js/lib/moment.min.js", "js/lib/highcharts.js", "js/lib/jquery-3.3.1.min.js", "js/Template.js", "js/util.js", "js/import.js"],
"matches": ["https://utdirect.utexas.edu/registrar/waitlist/wl_see_my_waitlists.WBX", "https://utdirect.utexas.edu/registration/classlist.WBX*"]
}
],
"web_accessible_resources": ["grades.db", "images/disticon.png"],
"background": {
"scripts": ["js/lib/jquery-3.3.1.min.js", "js/lib/sql-memory-growth.js", "js/lib/moment.min.js", "js/config.js", "js/util.js", "js/background.js"],
"persistent": true
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}

30
options.html Normal file
View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<!-- This file is serving as the template for the options page -->
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/options.css" />
</head>
<body>
<div id="version-container">
<p class="version">(v<span id="version"></span>)</p>
</div>
<div class="card options-card" id="header">
<h2 class="options-header"><u>Options</u></h2>
<div id="options_container"></div>
<p class="creator-tag"><a href="https://sghsri.github.io">Sriram Hariharan</a> (2018)</p>
</div>
<div class="card options-card" id="contributors_container">
<h3 class="contributor-title">Amazing people who've contributed to the extension!</h3>
<p class="creator-tag open-source-tag">Code is open source here <a href="https://github.com/sghsri/UT-Registration-Plus">here</a> :)</p>
<div id="contributor-list"></div>
</div>
<script src="js/config.js"></script>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/util.js"></script>
<script src="js/Template.js"></script>
<script src="js/options.js"></script>
</body>
</html>

View File

@@ -1,123 +0,0 @@
{
"name": "ut-registration-plus",
"displayName": "UT Registration Plus",
"version": "2.0.0-beta4",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
"check-types": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"preview": "vite preview",
"preinstall": "npx only-allow pnpm",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^2.0.3",
"@hello-pangea/dnd": "^16.5.0",
"@unocss/vite": "^0.58.6",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.54",
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
"html-to-image": "^1.11.11",
"husky": "^9.0.11",
"nanoid": "^5.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.71.1",
"sql.js": "1.10.2"
},
"devDependencies": {
"@chromatic-com/storybook": "^1.4.0",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"@commitlint/types": "^19.0.3",
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/material-symbols": "^1.1.73",
"@iconify-json/ri": "^1.1.20",
"@storybook/addon-designs": "^8.0.1",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
"@storybook/blocks": "^8.1.1",
"@storybook/react": "^8.1.1",
"@storybook/react-vite": "^8.1.1",
"@storybook/test": "^8.1.1",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.268",
"@types/node": "^20.12.12",
"@types/prompts": "^2.4.9",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@unocss/eslint-config": "^0.58.6",
"@unocss/postcss": "^0.58.6",
"@unocss/preset-uno": "^0.58.6",
"@unocss/preset-web-fonts": "^0.58.6",
"@unocss/reset": "^0.58.6",
"@unocss/transformer-directives": "^0.58.6",
"@unocss/transformer-variant-group": "^0.58.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"@vitest/coverage-v8": "^1.3.1",
"@vitest/ui": "^1.3.1",
"chromatic": "^11.3.5",
"cssnano": "^6.0.5",
"cssnano-preset-advanced": "^6.0.5",
"dotenv": "^16.4.5",
"es-module-lexer": "^1.4.1",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import-essentials": "^0.2.1",
"eslint-plugin-jsdoc": "^48.2.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-prefer-function-component": "^3.3.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.8.0",
"path": "^0.12.7",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"storybook": "^8.1.1",
"typescript": "^5.4.3",
"unocss": "^0.58.6",
"unocss-preset-primitives": "0.0.2-beta.0",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3",
"vitest": "^1.3.1"
},
"pnpm": {
"patchedDependencies": {
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
"@unocss/vite@0.58.6": "patches/@unocss__vite@0.58.6.patch"
},
"overrides": {
"es-module-lexer": "^1.4.1"
}
}
}

View File

@@ -1,105 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 5c3f6291168987c56b816428080e6f1fe9de7107..abaf6290fe9454ae036a81eacbe7dc3be2fdfbc3 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -499,16 +499,43 @@ ${sourceMap}
}),
mergeMap(async ({ target, code, deps }) => {
await lexer.init;
- const [imports] = lexer.parse(code, fileName);
+ const [imports, exports] = lexer.parse(code, fileName);
const depSet = new Set(deps);
const magic = new MagicString(code);
- for (const i of imports)
+ for (const i of imports) {
if (i.n) {
depSet.add(i.n);
const fileName2 = getFileName({ type: "module", id: i.n });
const fullImport = code.substring(i.s, i.e);
- magic.overwrite(i.s, i.e, fullImport.replace(i.n, `/${fileName2}`));
+ const hmrTimestamp = fullImport.match(/\bt=\d{13}&?\b/);
+ magic.overwrite(
+ i.s,
+ i.e,
+ fullImport.replace(
+ i.n,
+ `/${fileName2}${hmrTimestamp ? `?${hmrTimestamp[0]}` : ""}`
+ )
+ );
+ }
+ }
+ for (const e of exports) {
+ if (e.n === "default") {
+ const regex = /\s+['"](.*)['"]/y;
+ regex.lastIndex = e.e;
+ const fullExport = regex.exec(code)?.[1];
+ if (!fullExport)
+ continue;
+ const start = regex.lastIndex - fullExport.length - 1;
+ const end = regex.lastIndex - 1;
+ if (fullExport.startsWith("/node_modules")) {
+ magic.overwrite(
+ start,
+ end,
+ `http://localhost:5173${fullExport}`
+ );
+ }
}
+ }
return { target, source: magic.toString(), deps: [...depSet] };
})
);
@@ -1229,10 +1256,14 @@ const pluginHMR = () => {
handleHotUpdate({ modules, server }) {
const { root } = server.config;
const relFiles = /* @__PURE__ */ new Set();
- for (const m of modules)
+ function getRelFile(file) {
+ return file.startsWith(root) ? file.slice(server.config.root.length) : file;
+ }
+ for (const m of modules) {
if (m.id?.startsWith(root)) {
relFiles.add(m.id.slice(server.config.root.length));
}
+ }
if (inputManifestFiles.background.length) {
const background = prefix$1("/", inputManifestFiles.background[0]);
if (relFiles.has(background) || modules.some(isImporter(join(server.config.root, background)))) {
@@ -1244,7 +1275,14 @@ const pluginHMR = () => {
for (const [key, script] of contentScripts)
if (key === script.id) {
if (relFiles.has(script.id) || modules.some(isImporter(join(server.config.root, script.id)))) {
- relFiles.forEach((relFile) => update(relFile));
+ modules.filter((mod) => mod.id?.startsWith(root)).forEach((mod) => {
+ update(getRelFile(mod.id));
+ if (mod.file?.endsWith(".scss")) {
+ mod.importers.forEach((imp) => {
+ update(getRelFile(imp.id));
+ });
+ }
+ });
}
}
}
@@ -1882,7 +1920,7 @@ const pluginWebAccessibleResources = () => {
if (contentScripts.size > 0) {
const viteManifest = parseJsonAsset(
bundle,
- "manifest.json"
+ ".vite/manifest.json"
);
const viteFiles = /* @__PURE__ */ new Map();
for (const [, file] of Object.entries(viteManifest))
diff --git a/package.json b/package.json
index e0c47ae66ff399ad3a78abf38d8d93d1f038c55d..f84eb09ffbb5c41094935dd06e04ffe831e2d05a 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"connect-injector": "^0.4.4",
"convert-source-map": "^1.7.0",
"debug": "^4.3.3",
- "es-module-lexer": "^0.10.0",
+ "es-module-lexer": "^1.4.1",
"fast-glob": "^3.2.11",
"fs-extra": "^10.0.1",
"jsesc": "^3.0.2",

View File

@@ -1,108 +0,0 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 560f423a07f21b0c47abd494d77654de4c874481..35ae1fdca8bd5546f7e40a23edacb1dbbbd34b58 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -35,15 +35,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
];
const LAYER_MARK_ALL = "__ALL__";
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
function resolveId(id) {
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
return id;
for (const alias of VIRTUAL_ENTRY_ALIAS) {
const match = id.match(alias);
if (match) {
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
}
}
}
@@ -745,7 +745,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
const { hash, css } = await generateCSS(layer);
return {
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
- code: `__uno_hash_${hash}{--:'';}${css}`,
+ code: `uno_hash_${hash}{--:'';}${css}`,
map: { mappings: "" }
};
},
@@ -764,7 +764,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
if (layer && code.includes("import.meta.hot")) {
let hmr = `
try {
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
hash = hash && hash[1]
if (!hash)
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')
diff --git a/dist/index.mjs b/dist/index.mjs
index bbbccb7cad7421cbdb97223a451ec5853c0476cb..4bf6a08d94e562090a530308c0ab8337afdf8243 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,17 +1,17 @@
-import process$1 from 'node:process';
+import remapping from '@ampproject/remapping';
+import { createFilter } from '@rollup/pluginutils';
+import { loadConfig } from '@unocss/config';
+import { BetterMap, createGenerator, cssIdRE, notNull, toEscapedSelector } from '@unocss/core';
import UnocssInspector from '@unocss/inspector';
-import { resolve, isAbsolute, dirname } from 'node:path';
-import fs from 'node:fs/promises';
import fg from 'fast-glob';
import MagicString from 'magic-string';
-import remapping from '@ampproject/remapping';
-import { createHash } from 'node:crypto';
-import { cssIdRE, createGenerator, BetterMap, notNull, toEscapedSelector } from '@unocss/core';
import { Buffer } from 'node:buffer';
-import { createFilter } from '@rollup/pluginutils';
+import { createHash } from 'node:crypto';
import fs$1 from 'node:fs';
+import fs from 'node:fs/promises';
+import { dirname, isAbsolute, resolve } from 'node:path';
+import process$1 from 'node:process';
import { fileURLToPath } from 'node:url';
-import { loadConfig } from '@unocss/config';
const defaultPipelineExclude = [cssIdRE];
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/];
@@ -20,15 +20,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
];
const LAYER_MARK_ALL = "__ALL__";
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
function resolveId(id) {
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
return id;
for (const alias of VIRTUAL_ENTRY_ALIAS) {
const match = id.match(alias);
if (match) {
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
}
}
}
@@ -730,7 +730,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
const { hash, css } = await generateCSS(layer);
return {
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
- code: `__uno_hash_${hash}{--:'';}${css}`,
+ code: `uno_hash_${hash}{--:'';}${css}`,
map: { mappings: "" }
};
},
@@ -749,7 +749,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
if (layer && code.includes("import.meta.hot")) {
let hmr = `
try {
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
hash = hash && hash[1]
if (!hash)
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')

14024
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

90
popup.html Normal file
View File

@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<!-- This file is serving as the page for the browser action popup -->
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/popup.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div class="card" id="card-header" class='header_container'>
<div id="buttons" class="header_buttons">
<button id="clear" class="material_button header_button clear_button">Clear All</button>
<button id="RIS" class="material_button header_button ris_button">Registrar Info </button>
<button id="calendar" class="material_button header_button schedule_button">My Schedule</button>
</div>
</div>
<div>
<ul id="courseList" class='course_list'></ul>
<h2 id="empty" class='empty_message'>
<div id="main">Doesn't Look Like Anything To Me.</div>
<span>(No Courses Saved)</span>
</h2>
<div class="settings_divider"></div>
<input type="file" id="import_input" accept=".json" class="hide" />
<div>
<div id="meta-data" class="meta-container">
<p class="meta"> <span class="meta-metric" id="meta-metric">17</span> hr</p>
</div>
<div class="settings">
<button title='Search' class="settings_button search_button" id='search'>
<i class="material-icons settings_icon">search</i>
</button>
<div id="search-popup" class="hide">
<div class="flex-container">
<div id='semcon' class="select-style item">
<label>
<select id="semesters"></select>
</label>
</div>
<div id='depcon' class="select-style item">
<label>
<select id="department"></select>
</label>
</div>
<div id='levcon' class="select-style item">
<label>
<select id="level">
<option value="L">Lower</option>
<option value="U">Upper</option>
<option value="G">Grad</option>
</select>
</label>
</div>
<div>
<input class = "input-box" placeholder="Course # (optional)" type="text" id="courseCode"></input>
</div>
</div>
<button id="search-class" class="material_button search-button">Search</button>
</div>
<button title='Import/Export' class="settings_button import_button" id='impexp'>
<i class="material-icons settings_icon">import_export</i>
</button>
<div id="import-export-popup" class="hide">
<div class="flex-container">
<button id="import-class" class="simple-menu-option">
<i class="material-icons">file_upload</i>Import Classes
</button>
<button id="export-class" class="simple-menu-option">
<i class="material-icons">file_download</i>Export Classes
</button>
</div>
</div>
<button title='Options' class="settings_button options_button" id='options_button'>
<i class="material-icons settings_icon">settings</i>
</button>
</div>
</div>
</div>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/moment.min.js"></script>
<script src="js/Template.js"></script>
<script src="js/config.js"></script>
<script src="js/util.js"></script>
<script src="js/popup.js"></script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
/* eslint-disable global-require */
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
cssnano: process.env.NODE_ENV !== 'development' ? {} : false,
// '@unocss/postcss': {},
},
};
module.exports = config;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Beta" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #005f86;
}
.cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-2" width="1024" height="1024"/>
<g>
<circle class="cls-1" cx="512" cy="512" r="362"/>
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Development" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #bf2178;
}
.cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-2" x="0" width="1024" height="1024"/>
<g>
<circle class="cls-1" cx="512" cy="512" r="362"/>
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Production" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: #bf5700;
}
.cls-1, .cls-2 {
stroke-width: 0px;
}
.cls-3 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-1" width="1024" height="1024"/>
<g>
<circle class="cls-3" cx="512" cy="512" r="362"/>
<rect class="cls-2" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-2" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,221 +0,0 @@
[
"ACC",
"ADV",
"ASE",
"AFR",
"AFS",
"ASL",
"AMS",
"AHC",
"ANT",
"ALD",
"ARA",
"ARE",
"ARI",
"ARC",
"AED",
"ARH",
"ART",
"AET",
"AAS",
"ANS",
"AST",
"BSN",
"BEN",
"BCH",
"BIO",
"BME",
"BDP",
"B A",
"BAX",
"BGS",
"CHE",
"CH",
"CHI",
"C E",
"CLA",
"C C",
"CGS",
"COM",
"CLD",
"CMS",
"CRP",
"C L",
"COE",
"CSE",
"C S",
"CON",
"CTI",
"CRW",
"CDI",
"EDC",
"CZ",
"DAN",
"DSC",
"D S",
"DES",
"DEV",
"D B",
"DRS",
"DCH",
"ECO",
"ELP",
"EDP",
"E E",
"ECE",
"EER",
"EMA",
"ENM",
"E M",
"E S",
"E",
"ESL",
"ENS",
"EVE",
"EVS",
"EUP",
"EUS",
"FIN",
"F A",
"FLU",
"FR",
"F H",
"G E",
"GRG",
"GEO",
"GER",
"GSD",
"GOV",
"GRS",
"GK",
"GUI",
"HAR",
"H S",
"HCT",
"HED",
"HEB",
"HIN",
"HIS",
"HDF",
"HDO",
"H E",
"HMN",
"ILA",
"I",
"ISP",
"INF",
"ITD",
"I B",
"IRG",
"ISL",
"ITL",
"ITC",
"JPN",
"J S",
"J",
"KIN",
"KOR",
"LAR",
"LTC",
"LAT",
"LAL",
"LAS",
"LAW",
"LEB",
"L A",
"LAH",
"LIN",
"MAL",
"MAN",
"MIS",
"MFG",
"MNS",
"MKT",
"MSE",
"M",
"M E",
"MDV",
"MAS",
"MEL",
"MES",
"M S",
"MOL",
"MUS",
"NSC",
"N S",
"NEU",
"NOR",
"N",
"NTR",
"OBO",
"OPR",
"O M",
"ORI",
"ORG",
"PER",
"PRS",
"PGE",
"PGS",
"PHM",
"PHL",
"PED",
"P S",
"PHY",
"PIA",
"POL",
"POR",
"PRC",
"PSY",
"P A",
"PBH",
"P R",
"RIM",
"RTF",
"R E",
"R S",
"RHE",
"R M",
"RUS",
"REE",
"SAN",
"SAX",
"STC",
"STM",
"S C",
"SEL",
"S S",
"S W",
"SOC",
"SPN",
"SPC",
"SED",
"SLH",
"STA",
"SDS",
"SUS",
"SWE",
"TAM",
"TXA",
"T D",
"TRO",
"TRU",
"TBA",
"TUR",
"T C",
"UKR",
"UGS",
"UDN",
"URB",
"URD",
"UTS",
"UTL",
"VIA",
"VIO",
"V C",
"VAS",
"VOI",
"WGS",
"WRT",
"YID",
"YOR"
]

View File

@@ -1,54 +0,0 @@
const splashText: string[] = [
"You can't fail classes you're not in, that's for sure.",
'Steer clear of O-Chem, unless you fancy a challenge.',
'Rec Sports fills up fast, even before the sun reaches its peak.',
"Ah, Jendy's! A taste ever so refined.",
'Fine dining at Jester City Limits, eh?',
'Rec Sports fills up fast, even before the sun reaches its peak.',
'RIP Domino, you beloved campus feline.',
"The year is 2055 and Welch still isn't finished.",
'Motivation dropping faster than ur GPA',
'No Work Happens On PCL 5th Floor.',
'I may be a sophomore in name, but my credit count screams freshman!',
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
"At Least You're Not At A&M.",
'TeXAs iS BaCK GuYZ',
'mAke iT yOuR tExAS',
"'Academically Challenged'",
'Does McCombs teach Parseltongue?',
'No Cruce Enfrente Del Bus.',
'Omae Wa Mou Shindeiru...',
'Bevo Bucks are the new Bitcoin',
'Every day another brick disappears from Speedway',
'The GDC will annex the EER one day',
'Just you wait. Our CNS operatives will topple the EER regime',
'To hike to Kins or not to hike to Kins...',
'The road to Kinsolving is long, but their delicacies makes it worth every step.',
"C'mon you Longhorns! You want to study forever?",
'HOW BOUT A NICE CUP OF LIBER TEA',
"It's called the quiet floor of the PCL for a reason",
"'Whose car is this and why is it attempting to enter Welch?'",
"'I really like one of my TAs and I wanna ask her out after this semester ends'",
'CaN YoU aSk OuT a tA aFtEr tHe SeMeStEr Is oVeR AnD gRaDeS ArE DoNe?',
"The Block of Butter incident of '22",
"Arrows of Christ vs Church of Scientology was the crossover we didn't know we needed",
'THE WALK SIGN IS ON TO CROSS GUADALUPE AND 21ST',
'Days since last GDC door alarm: 0',
'Finding a parking spot is like winning the lottery... if the lottery required parallel parking skills.',
'The squirrels are more ambitious than most freshmen during finals week.',
'The line at the on-campus Starbucks is longer than your course waitlist.',
'The weather changes more often than your class schedule.',
"'Sorry, the PCL is full' is the most heartbreaking message you'll ever receive.",
'Getting to class on time is like navigating a maze of construction zones.',
"'studying' often means refreshing Canvas every five minutes to see if the professor posted lecture slides.",
"'I'll just skip this lecture' often turns into a semester-long habit.",
'The libraries are full of students pretending to study but actually napping with their eyes open.',
"The 'campus loop' refers to both the bus route and the endless cycle of trying to find your way around campus.",
'The squirrels have mastered the art of begging for food better than most students during finals week.',
"'going for a run' often means 'running to get food from food trucks'.",
'The struggle to fit all your classes into one schedule is as real as the struggle to fit all your groceries into a mini-fridge.',
];
export default splashText;

View File

@@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -1,157 +0,0 @@
import { DevStore } from '@shared/storage/DevStore';
import React, { useEffect } from 'react';
import { createRoot } from 'react-dom/client';
const manifest = chrome.runtime.getManifest();
/**
* Handles editing the storage for a specific area.
*
* @param {string} areaName - The name of the storage area.
* @returns {Function} - A function that accepts changes and sets them in the storage.
*/
const handleEditStorage = (areaName: 'local' | 'sync' | 'session') => (changes: Record<string, unknown>) => {
chrome.storage[areaName].set(changes);
};
interface JSONEditorProps {
data: unknown;
onChange: ReturnType<typeof handleEditStorage>;
}
function JSONEditor(props: JSONEditorProps) {
const { data, onChange } = props;
const [isEditing, setIsEditing] = React.useState(false);
const [json, setJson] = React.useState(JSON.stringify(data, null, 2));
useEffect(() => {
setJson(JSON.stringify(data, null, 2));
}, [data]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setJson(e.target.value);
};
const handleSave = () => {
try {
const updates = JSON.parse(json);
onChange(updates);
setIsEditing(false);
} catch (e) {
console.error(e);
// eslint-disable-next-line no-alert
alert('Invalid JSON');
}
};
return (
<div>
{isEditing ? (
<div>
<div style={{ flex: 1, marginBottom: 10, gap: 10, display: 'flex' }}>
<button style={{ color: 'green' }} onClick={handleSave}>
Save
</button>
<button style={{ color: 'red' }} onClick={() => setIsEditing(false)}>
Cancel
</button>
</div>
<textarea style={{ width: '100%', height: '300px' }} value={json} onChange={handleChange} />
</div>
) : (
<div>
<pre onClick={() => setIsEditing(true)}>{json}</pre>
</div>
)}
</div>
);
}
// const PrettyPrintJson = React.memo(({ data }: any) => (
// <div>
// <pre>{JSON.stringify(data, null, 2)}</pre>
// </div>
// ));
function DevDashboard() {
const [localStorage, setLocalStorage] = React.useState<Record<string, unknown>>({});
const [syncStorage, setSyncStorage] = React.useState<Record<string, unknown>>({});
const [sessionStorage, setSessionStorage] = React.useState<Record<string, unknown>>({});
useEffect(() => {
const onVisibilityChange = () => {
DevStore.set('wasDebugTabVisible', document.visibilityState === 'visible');
};
document.addEventListener('visibilitychange', onVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', onVisibilityChange);
};
}, []);
useEffect(() => {
chrome.storage.local.get(null, result => {
setLocalStorage(result);
});
chrome.storage.sync.get(null, result => {
setSyncStorage(result);
});
chrome.storage.session.get(null, result => {
setSessionStorage(result);
});
}, []);
// listen for changes to the chrome storage to update the local storage state displayed in the dashboard
useEffect(() => {
const onChanged = (changes: chrome.storage.StorageChange, areaName: chrome.storage.AreaName) => {
let copy: Record<string, unknown> = {};
if (areaName === 'local') {
copy = { ...localStorage };
} else if (areaName === 'sync') {
copy = { ...syncStorage };
} else if (areaName === 'session') {
copy = { ...sessionStorage };
}
Object.keys(changes).forEach((key: string) => {
copy[key] = changes[key as keyof typeof changes].newValue;
});
if (areaName === 'local') {
setLocalStorage(copy);
}
if (areaName === 'sync') {
setSyncStorage(copy);
}
if (areaName === 'session') {
setSessionStorage(copy);
}
};
chrome.storage.onChanged.addListener(onChanged);
return () => {
chrome.storage.onChanged.removeListener(onChanged);
};
}, [localStorage, syncStorage, sessionStorage]);
return (
<div>
<h1>
{manifest.name} {manifest.version} - {process.env.NODE_ENV}
</h1>
<p>This tab is used for hot reloading and debugging. We will update this tab further in the future.</p>
<h2>Local Storage</h2>
<JSONEditor data={localStorage} onChange={handleEditStorage('local')} />
<h2>Sync Storage</h2>
<JSONEditor data={syncStorage} onChange={handleEditStorage('sync')} />
<h2>Session Storage</h2>
<JSONEditor data={sessionStorage} onChange={handleEditStorage('session')} />
<br />
</div>
);
}
createRoot(document.getElementById('root')!).render(<DevDashboard />);

14
src/global.d.ts vendored
View File

@@ -1,14 +0,0 @@
declare module '*.jpg' {
const content: string;
export default content;
}
declare module '*.png' {
const content: string;
export default content;
}
declare module '*.json' {
const content: string;
export default content;
}

View File

@@ -1,64 +0,0 @@
import { defineManifest } from '@crxjs/vite-plugin';
import packageJson from '../package.json';
// Convert from Semver (example: 0.1.0-beta6)
const [major, minor, patch, label = '0'] = packageJson.version
// can only contain digits, dots, or dash
.replace(/[^\d.-]+/g, '')
// split into version parts
.split(/[.-]/);
const isBeta = !!process.env.BETA;
const mode = isBeta ? 'beta' : process.env.NODE_ENV;
if (isBeta && process.env.NODE_ENV !== 'production') throw new Error('Cannot have beta non-production build');
// eslint-disable-next-line no-nested-ternary
const nameSuffix = isBeta ? ' (beta)' : mode === 'development' ? ' (dev)' : '';
const HOST_PERMISSIONS: string[] = [
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
'*://*.utexas.collegescheduler.com/*',
'*://*.catalog.utexas.edu/ribbit/',
'*://*.registrar.utexas.edu/schedules/*',
'*://*.login.utexas.edu/login/*',
'https://utexas.bluera.com/*',
];
const manifest = defineManifest(async () => ({
manifest_version: 3,
name: `${packageJson.displayName ?? packageJson.name}${nameSuffix}`,
version: `${major}.${minor}.${patch}.${label}`,
description: packageJson.description,
options_page: 'src/pages/options/index.html',
background: { service_worker: 'src/pages/background/background.ts' },
permissions: ['storage', 'unlimitedStorage', 'background', 'scripting'],
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
action: {
default_popup: 'src/pages/popup/index.html',
default_icon: `icons/icon_${mode}_32.png`,
},
icons: {
'16': `icons/icon_${mode}_16.png`,
'32': `icons/icon_${mode}_32.png`,
'48': `icons/icon_${mode}_48.png`,
'128': `icons/icon_${mode}_128.png`,
},
content_scripts: [
{
matches: HOST_PERMISSIONS,
js: ['src/pages/content/index.tsx'],
},
],
web_accessible_resources: [
{
resources: ['assets/js/*.js', 'assets/css/*.css', 'assets/img/*'],
matches: ['*://*/*'],
},
],
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
},
}));
export default manifest;

View File

@@ -1,59 +0,0 @@
import type { BACKGROUND_MESSAGES } from '@shared/messages';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import updateBadgeText from '@shared/util/updateBadgeText';
import { MessageListener } from 'chrome-extension-toolkit';
import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate';
import browserActionHandler from './handler/browserActionHandler';
import calendarBackgroundHandler from './handler/calendarBackgroundHandler';
import CESHandler from './handler/CESHandler';
import tabManagementHandler from './handler/tabManagementHandler';
import userScheduleHandler from './handler/userScheduleHandler';
onServiceWorkerAlive();
/**
* will be triggered on either install or update
* (will also be triggered on a user's sync'd browsers (on other devices)))
*/
chrome.runtime.onInstalled.addListener(details => {
switch (details.reason) {
case 'install':
onInstall();
break;
case 'update':
onUpdate();
break;
default:
break;
}
});
// initialize the message listener that will listen for messages from the content script
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler,
...tabManagementHandler,
...userScheduleHandler,
...CESHandler,
...calendarBackgroundHandler,
});
messageListener.listen();
UserScheduleStore.listen('schedules', async schedules => {
const index = await UserScheduleStore.get('activeIndex');
const numCourses = schedules.newValue[index]?.courses?.length;
if (!numCourses) return;
updateBadgeText(numCourses);
});
UserScheduleStore.listen('activeIndex', async ({ newValue }) => {
const schedules = await UserScheduleStore.get('schedules');
const numCourses = schedules[newValue]?.courses?.length;
if (!numCourses) return;
updateBadgeText(numCourses);
});

Some files were not shown because too many files have changed in this diff Show More