diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fixed.md b/.github/PULL_REQUEST_TEMPLATE/bug_fixed.md new file mode 100644 index 00000000..4cc2a8a3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug_fixed.md @@ -0,0 +1,15 @@ +--- +name: Bug fixed +about: Fixed a bug +title: '' +labels: '' +assignees: '' +--- + +**Describe the pull request** +Describe what bug was fixed in this pr. + +### Checklist + +- [ ] Reference the related issues +- [ ] Update documentation (if applicable) diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_added.md b/.github/PULL_REQUEST_TEMPLATE/feature_added.md new file mode 100644 index 00000000..0b49093c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature_added.md @@ -0,0 +1,15 @@ +--- +name: Feature added +about: Implemented a new feature +title: '' +labels: '' +assignees: '' +--- + +**Describe the pull request** +Describe what feature was added in this pr. + +### Checklist + +- [ ] Reference the related issues +- [ ] Add documentation diff --git a/.github/PULL_REQUEST_TEMPLATE/new_balloon.md b/.github/PULL_REQUEST_TEMPLATE/new_balloon.md new file mode 100644 index 00000000..95e4b4a7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_balloon.md @@ -0,0 +1,16 @@ +--- +name: New balloon +about: Add a new balloon +title: '' +labels: '' +assignees: '' +--- + +**Describe the pull request** +Describe the added balloon in this pr. + +### Checklist + +- [ ] Reference the related issues +- [ ] Balloon documentation updated +- [ ] Test file added for balloon diff --git a/.github/PULL_REQUEST_TEMPLATE/new_release.md b/.github/PULL_REQUEST_TEMPLATE/new_release.md new file mode 100644 index 00000000..477dc5fc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_release.md @@ -0,0 +1,14 @@ +--- +name: New release +about: Publish a new release +title: vx.x.x update +labels: '' +assignees: '' +--- + +**Describe the pull request** +Describe the release. + +### Checklist + +- [ ] Updated [package.json](/package.json) and [package-lock.json](/package-lock.json) version diff --git a/.prettierrc b/.prettierrc index 02847ae9..1cb3d4b5 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,10 +1,14 @@ { - "plugins": ["prettier-plugin-tailwindcss"], + "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-sort-imports"], "semi": true, "singleQuote": true, "printWidth": 80, "tabWidth": 2, "useTabs": false, "endOfLine": "lf", - "trailingComma": "es5" + "trailingComma": "es5", + "sortingMethod": "alphabetical", + "stripNewlines": true, + "newlineBetweenTypes": false, + "importTypeOrder": ["NPMPackages", "localImportsValue", "localImportsType"] } diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 00000000..b478b763 --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1,155 @@ +

Architecture

+ +The pop-a-loon architecture is designed to be modular and extensible. This document provides an overview of the architecture of the extension. + +## Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Directory Structure](#directory-structure) +- [Polyfilling](#polyfilling) +- [Webpack](#webpack) + - [Environment Variables](#environment-variables) + - [Resources folder](#resources-folder) + - [Compiling the manifests](#compiling-the-manifests) +- [Managers](#managers) + - [Log](#log) + - [Storage](#storage) +- [Manifest](#manifest) +- [Background](#background) +- [Content Scripts](#content-scripts) +- [Popup](#popup) +- [Testing](#testing) +- [Building and Deployment](#building-and-deployment) + + + +## Directory Structure + +- `docs/`: Contains documentation files. +- `resources/`: Contains resources used in the extension and are all available when the extension is running. +- `src/`: This directory contains the source code of the extension. + - `popup/`: Contains the code for the popup UI of the extension. + - `background/`: Contains the background scripts of the extension. + - `content/`: Contains the content scripts of the extension. + - `utils.ts`: Contains utility functions used across the extension. +- `tests/`: Contains test files for the extension. +- `manifest.json`: The manifest file of the extension, which provides important metadata for the extension. + +## Polyfilling + +To ensure compatibility with multiple browsers, the pop-a-loon extension uses the [webextension polyfill](https://github.com/mozilla/webextension-polyfill) library. This polyfill provides a consistent API for browser extensions across different browsers, allowing the extension to work seamlessly on Chrome, Firefox, and other supported browsers. + +By including the webextension polyfill in the background scripts and content scripts, the extension can make use of browser APIs and features without worrying about browser-specific implementations. This greatly simplifies the development process and ensures a consistent experience for users on different browsers. + +This means when you want to access browser API's you don't use the `chrome` or `browser` namespaces directly. You first import the polyfill and then use the `browser` namespace. + +```ts +import browser from 'webextension-polyfill'; + +// For example query some tabs +const tabs = await browser.tabs.query({ active: true }); +``` + +Now, after compiling the extension, the polyfill will be included and make the code access the correct browser API's. + +## Webpack + +The pop-a-loon extension uses [Webpack](https://webpack.js.org/) to compile its JavaScript code. The webpack configuration is set up to compile the background scripts, popup UI and an entry for each content script into separate bundles. This allows for better organization of the code and ensures that each part of the extension is compiled correctly. Configuration can be found in the [webpack.config.js](/webpack.config.js) file. + +### Environment Variables + +The webpack configuration uses environment variables to determine the build mode. The `NODE_ENV` environment variable is used to determine whether the extension is being built for development or production. This allows for different optimizations and settings to be applied based on the build mode. + +Webpack also sets some environment variables for the extension. In the source code they are accessed via the `process.env` object. For example, the `process.env.NODE_ENV` variable is used to determine the build mode. + +> [!WARNING] +> The `process` namespace is not available in the browser. This is a Node.js specific feature. Webpack replaces the `process` object with the correct values during compilation. (e.g. `process.env.NODE_ENV` is replaced with `production`). + +### Resources folder + +The `resources` folder contains all the resources used in the extension. This includes images, icons, and other assets that are used in the extension. These resources are copied to the build directory during the compilation process and are available when the extension is running. + +### Compiling the manifests + +Webpack is used to compile the manifests. More information can be found [here](#manifest) + +## Managers + +Pop-a-loon has a few custom managers that handle different aspects of the extension. + +### Log + +The `log` manager is used to log messages to the console. It provides a simple interface for logging messages with different levels of severity, such as `info`, `warn`, and `error`. + +```ts +import log from '@/managers/log'; + +log.debug('This is a debug message'); +log.info('This is an info message'); +log.warn('This is a warning message'); +log.error('This is an error message'); +log.softwarn( + "Like the warning message but doesn't throw an actual warning in the console" +); +log.softerror( + "Like the error message but doesn't throw an actual error in the console" +); +``` + +This manager also includes log functionallity from the console namespace. Like `log.time`, `log.timeEnd`, `log.group`, `log.groupEnd`, …. + +### Storage + +The `storage` managers provides a type-safe way to interact with the browser storage via the browser API's. + +```ts +import storage from '@/managers/storage'; + +const config = await storage.sync.get('config'); +await storage.sync.set('config', { + ...config, + popVolume: 0.5, +}); +``` + +In this example we update the `popVolume` property of the `config` object in the `sync` storage. + +## Manifest + +The [`manifest.json`](/manifest.json) file is the metadata file for the extension. It contains information about the extension, such as its name, version, description, and permissions. The manifest file also specifies the background scripts, content scripts, and popup UI of the extension. + +There are also browser specific manifest files. These are used to specify browser specific settings. For example, the [`manifest.firefox.json`](/manifest.firefox.json) file is used to specify a `browser_specific_settings` key that is only available in Firefox. + +The browser specific manifest files are merged with the base manifest file during the [build process](#webpack). This allows for browser-specific settings to be applied when the extension is compiled. The browser specific manifest file keys can override the options defined in the base manifest file. + +## Background + +The background scripts handle events and perform tasks that require access to browser APIs. + +Read the [background scripts documentation](./architecture/background.md) for more information. + +## Content Scripts + +Content scripts are injected into web pages and have access to the DOM. This is used to make the balloons appear on web pages. + +Read the [content scripts documentation](./architecture/content-scripts.md) for more information. + +## Popup + +The popup UI provides a user-friendly interface for accessing the extension's features. It can display information, receive user input. It acts as a bridge between the user and the extension and is internally just a web page. + +Read the [popup documentation](./architecture/popup.md) for more information. + +## Testing + +Run the following command to run tests: + +```bash +npm run test +``` + +## Building and Deployment + +See [the development guide](./README.md#development). diff --git a/docs/README.md b/docs/README.md index 48d916c3..5d66de20 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,18 +10,29 @@ - [Installation](#installation) - [Development](#development) - [dev:chrome](#devchrome) + - [dev:chrome:noremote](#devchromenoremote) + - [dev:chrome:remote](#devchromeremote) - [dev:firefox](#devfirefox) + - [dev:firefox:noremote](#devfirefoxnoremote) + - [dev:firefox:remote](#devfirefoxremote) + - [Load the extension to chrome](#load-the-extension-to-chrome) + - [Load the extension to firefox](#load-the-extension-to-firefox) - [Debugging in Visual Studio Code](#debugging-in-visual-studio-code) - [Deployment](#deployment) - [build:chrome](#buildchrome) - [build:chrome:zip](#buildchromezip) - [build:firefox](#buildfirefox) - [build:firefox:zip](#buildfirefoxzip) + - [build:all:zip](#buildallzip) + - [zip:source](#zipsource) + - [Adding a balloon](#adding-a-balloon) - [Architecture](#architecture) - [Balloon spawn chances](#balloon-spawn-chances) +- [Inheritance Tree](#inheritance-tree) +- [Abstract balloon class](#abstract-balloon-class) - [Balloons](#balloons) - - [Abstract balloon class](#abstract-balloon-class) - [Default balloon](#default-balloon) + - [Confetti balloon](#confetti-balloon) @@ -52,8 +63,18 @@ npm install Building for development can be done with the `dev:{browser}` script. Replace `{browser}` with the browser you want to build for. The available options are `chrome` and `firefox`. +> [!TIP] +> See the [Load the extension to chrome](#load-the-extension-to-chrome) and [Load the extension to firefox](#load-the-extension-to-firefox) sections for instructions on how to load the extension in the browser. + #### dev:chrome + + +> [!IMPORTANT] +> This will call the [dev:chrome:noremote](#devchromenoremote) script. + + + To build for Chrome: ```bash @@ -66,18 +87,40 @@ This will build the extension in development mode for chrome. You can also inclu npm run dev:chrome -- --watch ``` -The extension can be loaded in the browser by following the steps below: +#### dev:chrome:noremote -1. Open the Extension Management page by navigating to [`chrome://extensions`](chrome://extensions). +To build for Chrome without a remote server[^1]: - > Don't forget to enable Developer mode in the top right corner. +```bash +npm run dev:chrome:noremote +``` -2. Click the `Load unpacked` button and select the `dist/` directory. -3. The extension should now be loaded and you can see the icon in the browser toolbar. -4. Pin the extension to the toolbar for easy access. +This will build the extension in development mode for chrome without a remote server. You can also include the `--watch` flag to automatically rebuild the extension when files change. + +```bash +npm run dev:chrome:noremote -- --watch +``` + +#### dev:chrome:remote + +> [!IMPORTANT] +> This will connect to a [pop-a-loon-backend](https://github.com/SimonStnn/pop-a-loon-backend) server which is expected to be running on `http://localhost:3000`. + +To build for Chrome with a remote server: + +```bash +npm run dev:chrome:remote +``` #### dev:firefox + + +> [!IMPORTANT] +> This will call the [dev:firefox:noremote](#devfirefoxnoremote) script. + + + To build for Firefox: ```bash @@ -90,6 +133,53 @@ This will build the extension in development mode for firefox. You can also incl npm run dev:firefox -- --watch ``` +#### dev:firefox:noremote + +To build for Firefox without a remote server[^1]: + +```bash +npm run dev:firefox:noremote +``` + +This will build the extension in development mode for firefox without a remote server. You can also include the `--watch` flag to automatically rebuild the extension when files change. + +```bash +npm run dev:firefox:noremote -- --watch +``` + +#### dev:firefox:remote + +> [!IMPORTANT] +> This will connect to a [pop-a-loon-backend](https://github.com/SimonStnn/pop-a-loon-backend) server which is expected to be running on `http://localhost:3000`. + +To build for Firefox with a remote server: + +```bash +npm run dev:firefox:remote +``` + +This will build the extension in development mode for firefox with a remote server. You can also include the `--watch` flag to automatically rebuild the extension when files change. + +```bash +npm run dev:firefox:remote -- --watch +``` + +[^1]: The requests to the remote will be 'mocked' so the extension can be developed without the need for a running server. + +### Load the extension to chrome + +The extension can be loaded in the browser by following the steps below: + +1. Open the Extension Management page by navigating to [`chrome://extensions`](chrome://extensions). + + > Don't forget to enable Developer mode in the top right corner. + +2. Click the `Load unpacked` button and select the `dist/` directory. +3. The extension should now be loaded and you can see the icon in the browser toolbar. +4. Pin the extension to the toolbar for easy access. + +### Load the extension to firefox + The extension can be loaded in the browser by following the steps below: 1. Open the Add-ons page by navigating to [`about:addons`](about:addons). @@ -147,10 +237,37 @@ This will build the extension in production mode for firefox. You can also inclu npm run build:firefox:zip ``` +#### build:all:zip + + + +This will build the extension in production for all browsers and include a [zip file of the source code](#zipsource). + + + +```bash +npm run build:all:zip +``` + +#### zip:source + +This will create a zip file of the source code. The zip file will be created in the `build/` directory with the name `source-v{version}.zip`. + +```bash +npm run zip:source +``` + The zip file will be created in the `build/` directory. +### Adding a balloon + +Refer to [adding a balloon](./adding-balloon.md) for instructions on how to add a new balloon to the extension. + ## Architecture +> [!NOTE] +> Read the [Architecture](./Architecture.md) document for a more detailed explanation of the architecture. + ## Balloon spawn chances ```mermaid @@ -158,11 +275,22 @@ pie showdata title Balloon spawn chances "Default" : 0.90 "Confetti" : 0.10 + "Gold": 0.05 ``` -## Balloons +## Inheritance Tree -### Abstract balloon class +```mermaid +classDiagram +direction BT +class Balloon { <> } + +Default --|> Balloon +Confetti --|> Default +Gold --|> Default +``` + +## Abstract balloon class The abstract balloon class is the base class for all balloons. @@ -171,14 +299,11 @@ classDiagram direction LR class Balloon { <> - -element: HTMLDivElement - #balloonImage: HTMLImageElement - #<< get >>balloonImageUrl: string - #<< get >>popSoundUrl: string - #<< get >>popSound: HTMLAudioElement + +name: string* + +build() void* + +element: HTMLDivElement +constructor() - +getRandomDuration() number* +isRising() boolean +rise() void +remove() void @@ -186,20 +311,25 @@ class Balloon { } ``` +The class serves as a base class for each balloon in pop-a-loon, providing essential functionality that must operate from this class. + +> [!IMPORTANT] +> The class has the following abstract properties and methods: +> +> - `name`: The name of the balloon. Should be the same as the name of the class, but in lowercase. +> - `build()`: Is called when the balloon should be built. In this method the class should for example add the styling and balloon image to the balloon element. + +These properties and methods **must** be implemented in the child classes. + +> [!IMPORTANT] +> The `element` is the html element that will be added to the DOM after the balloon is built. + +## Balloons + ### Default balloon -The default balloon is a simple balloon that rises and pops when clicked. +See [Default balloon documentation](./balloons/default.md) for more information. -```mermaid -classDiagram -direction LR -class Balloon { <> } -click Balloon href "#abstract-balloon-class" "Abstract balloon class" +### Confetti balloon -class Default { - +spawn_chance: number$ - +name: string* - +getRandomDuration() number* -} -Default --|> Balloon -``` +See [Confetti balloon documentation](./balloons/confetti.md) for more information. diff --git a/docs/adding-balloon.md b/docs/adding-balloon.md new file mode 100644 index 00000000..5c3a250d --- /dev/null +++ b/docs/adding-balloon.md @@ -0,0 +1,124 @@ +

Adding a new balloon

+ +Adding a new balloon to the extension is a simple process. In this document we will go over the steps to add a new balloon. + +## Table of Contents + + + +- [Table of Contents](#table-of-contents) +- [Choosing a name](#choosing-a-name) +- [Implementation](#implementation) + - [Extending the abstract balloon class](#extending-the-abstract-balloon-class) + - [Extending the Default balloon class](#extending-the-default-balloon-class) + - [Custom balloon styles](#custom-balloon-styles) +- [Making the balloon available](#making-the-balloon-available) +- [Tests](#tests) +- [Documentation](#documentation) + + + +## Choosing a name + +The name of the balloon is prefered to be a single word. The name should fit in the text ` balloon`. This name will be used in the UI. + +## Implementation + +Each balloon is it's own class. To add a new balloon, create a new file in the [`/src/balloons/`](/src/balloons/) directory. The file should be named `.ts`. Make a class in this file and export it. Your new balloon should extend the Balloon class or any other balloon in the [balloon hierarchy](./README.md#inheritance-tree). + +### Extending the abstract balloon class + +Here we will discuss how to add a balloon extending the [abstract balloon class](./README.md#abstract-balloon-class). This is more complicated as there is less functionality provided in the abstract balloon class. + +> [!TIP] +> For a simpler implementation refer to [extending the Default balloon class](#extending-the-default-balloon-class). This class has more functionality implemented. + +```ts +// example.ts +import Balloon from '@/balloon'; + +export default class Example extends Balloon { + public static readonly spawn_chance: number = 0.1; + public readonly name = 'example'; + + public build() { + // Build the balloon element with the `this.element` div + } +} +``` + +Now you build your class you can [make your balloon available](#making-the-balloon-available) to pop-a-loon and see it on screen. + +### Extending the Default balloon class + +Extending the [Default balloon](./balloons/default.md) is a simpler process. + +```ts +// example.ts +import Default from '@/balloons/default'; + +class Example extends Default { + public static readonly spawn_chance: number = 0.1; + // @ts-ignore + public get name(): 'example' { + return 'example'; + } + + public get options(): BalloonOptions { + return { + ...super.options, + // Override options here + // e.g. the image url + imageUrl: 'example.svg', + }; + } +} +``` + +In this example the `Example` class extends the `Default` class and overrides the `spawn_chance`, `name` and `options` properties. The options property overrides the image url to `example.svg`. Pop-a-loon will look for this `example.svg` file in the `resources/balloons/example` directory. The image for the balloon doesn't need to be an `svg`, but it is recommended. + +You can find what other options you can override in the [default balloon documentation](./balloons/default.md). Further implementation is up to you. + +Now you build your class you can [make your balloon available](#making-the-balloon-available) to pop-a-loon and see it on screen. + +### Custom balloon styles + +Implementing a custom balloon may require custom styles. To do this you can add a new css file to your resources folder. To import the css file you can use the `importStylesheet` function from [utils](/src/utils.ts). + +```ts +import { importStylesheet } from '@/utils'; + +class Example extends Default { + public build() { + super.build(); + this.importStylesheet('style.css'); + } +} +``` + +In this example the `importStylesheet` function is used to import the `style.css` file from the `resources/balloons/example` directory. The `resourceLocation` property is provided by the `Default` class and is the path to the balloon resources. + +> The default value for the file name is `'style.css'`. So in this example it can even be excluded. + +## Making the balloon available + +Now we need to export it from the [`/src/balloons/`](/src/balloons/) module. So we include it in [`/src/balloons/index.ts`](/src/balloons/index.ts) + +```ts +// index.ts +// ... other balloons +export { default as Example } from './example'; +// ... other balloons +``` + +Balloon exports happen preferable in alphabetical order. + +## Tests + +Add your balloon test file to the [`/tests/`](/tests/) folder with the name: `.test.ts` and add the required tests that need to pass for your balloon. + +## Documentation + +Add your balloon documentation to the [`/docs/balloons/`](/docs/balloons/) folder with the name: `.md` and add there the documentation for your balloon. + +Add your balloon spawn chance to the [balloon spawn chances](./README.md#balloon-spawn-chances) and the balloon class to the [inheritance tree](./README.md#inheritance-tree). Refer to your balloon documentation at the [Balloons section](./README.md#balloons). diff --git a/docs/architecture/background.md b/docs/architecture/background.md new file mode 100644 index 00000000..b6be7693 --- /dev/null +++ b/docs/architecture/background.md @@ -0,0 +1,60 @@ +# Background + +The background scripts handle events and perform tasks that require access to browser APIs. + +## Alarms + +The background script creates and listens to alarms. There are currently two alarms: + +1. `spawnBalloon`: This alarm goes off at random intervals. When it goes off, the background script sends a content script to one of the active tabs to spawn a balloon. Because you can only specify a specific interval for alarms, e.g. every 5 minutes, the background script creates an alarm that goes off once and then creates a new alarm when that alarm was triggered. This new alarm will have a different random delay. This way, the balloons are spawned at random intervals. + + When the `spawnBalloon` the background script performs a series of checks before actually spawning a balloon. + + 1. Is there a spawn timeout? + 2. Should there be a spawn timeout? if so, set the spawn timeout. + 3. Is the browser [idle](https://developer.chrome.com/docs/extensions/reference/api/idle)? + 4. Is there already a `spawnBalloon` alarm? + + If all checks pass, the background script will query all [active tabs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/query#active) and pick one to send the [spawn-balloon script](/src/content/spawn-balloon.ts) to. + +2. `restart`: When this alarm goes off, [runtime.reload](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/reload) is called. This restarts the extension. + + A `restart` alarm is created when the remote server is not available or when any unexpected error occurs during setup. These alarms are created with a delay of one minute. This way, the extension will try to restart itself after a minute. Why a minute? Because the extension is not supposed to be restarted too often. If the remote server is not available, it probably also isn't available the next second, but maybe it is in one minute. + +### Alarm Types + +There is an `AlarmName` type in [const.ts](/src/const.ts) that defines the alarm names. This way, there is type safety and the alarm names are consistent throughout the extension. + +## Messages + +Pop-a-loon uses the [browser messaging API](https://developer.chrome.com/docs/extensions/develop/concepts/messaging) to communicate between different scripts. + +These are the actions for the messages (see [message types](#message-types)): + +1. `updateCounter`: When this message is received, the background script will update the counter in the browser action badge. +2. `incrementCount`: When this message is received, the background will send a request to the remote server with the popped balloo. +3. `setLogLevel`: When this message is received, the background script will set the log level specified in the message. + +### Message Types + +There are a few types defined in [const.ts](/src/const.ts) and exported under the `Message` type. They can be distinguished using the `Message.action` property. + +## Important methods + +The background script has a few important methods initalized: + +- `setup()`: The setup function where all setups happen. +- `spawnBalloon()`: A balloon is sent to a tab. +- `createSpawnAlarm(name)`: Creates a balloon spawn alarm. Is triggered after `spawnBalloon()` was called. +- `backgroundScript()`: This is the 'main' function. This is the function that is initially called and calls the `setup()` function. + +These include most of the functionallity of the background. + +## Event listeners + +The background also listens to some events. + +- Alarms with [`onAlarm`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/alarms/onAlarm). See [alarms](#alarms). +- Messages with [`onMessage`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage). See [messages](#messages). +- [`onStartup`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onStartup) +- [`onInstalled`](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onInstalled) diff --git a/docs/architecture/content-scripts.md b/docs/architecture/content-scripts.md new file mode 100644 index 00000000..461378f9 --- /dev/null +++ b/docs/architecture/content-scripts.md @@ -0,0 +1,11 @@ +# Content scripts + +## spawn-balloon + +This script is sent to a tab and will spawn a balloon. + +It will add a [balloon container](#balloon-container) to the page if its not already in the page. After this it will gather all balloon types and their spawn chances and pick a random balloon to spawn. + +### Balloon container + +This is div element added directly to the body of the page a balloon is sent to. All other page modifications happen in this element. diff --git a/docs/architecture/popup.md b/docs/architecture/popup.md new file mode 100644 index 00000000..742996dc --- /dev/null +++ b/docs/architecture/popup.md @@ -0,0 +1 @@ +# Popup diff --git a/docs/balloons/confetti.md b/docs/balloons/confetti.md new file mode 100644 index 00000000..e87c970d --- /dev/null +++ b/docs/balloons/confetti.md @@ -0,0 +1,20 @@ +# Confetti + +The confetti balloon is a balloon that spawns confetti when popped. + +```mermaid +classDiagram +direction LR +class Balloon { <> } +click Balloon href "#abstract-balloon-class" "Abstract balloon class" + +class Confetti { + +spawn_chance: number$ + +name: string + -mask: HTMLImageElement + +constructor() + +build() void + +pop(event: MouseEvent) void +} +Confetti --|> Balloon +``` diff --git a/docs/balloons/default.md b/docs/balloons/default.md new file mode 100644 index 00000000..be50f3e3 --- /dev/null +++ b/docs/balloons/default.md @@ -0,0 +1,39 @@ +# Default + +The default balloon is a simple balloon that rises and pops when clicked. + +```mermaid +classDiagram +direction LR +class Balloon { <> } +click Balloon href "#abstract-balloon-class" "Abstract balloon class" + +class Default { + +spawn_chance: number$ + +<< get >>name: string + +<< get >>options: BalloonOptions + +balloonImage: HTMLImageElement + +popSound: HTMLAudioElement + +<< get >>balloonImageUrl: string + +<< get >>popSoundUrl: string + +constructor() + #originalPath(path: string) string + +build() void + +pop() Promise~void~ +} +Default --|> Balloon + +class BalloonOptions { + <> + dir_name: string + imageUrl: string + popSoundUrl: string + size: [number, number] + riseDurationThreshold: [number, number] + swingDurationThreshold: [number, number] + swingOffset: number + waveDegrees: number +} + +Default <|-- BalloonOptions +``` diff --git a/docs/balloons/gold.md b/docs/balloons/gold.md new file mode 100644 index 00000000..41e41502 --- /dev/null +++ b/docs/balloons/gold.md @@ -0,0 +1,21 @@ +# Gold + +Just like the [default balloon](./default.md), but :sparkles: golden :sparkles:. + +```mermaid +classDiagram +direction LR +class Balloon { <> } +click Balloon href "#abstract-balloon-class" "Abstract balloon class" + +class Gold { + +spawn_chance: number$ + +<< get >>name: string + +<< get >>options: BalloonOptions +} +Gold --|> Balloon +``` + +Has a custom image resource in [`/resources/balloons/gold/balloon.svg`](/resources/balloons/gold/balloon.svg). + +![Gold balloon](/resources/balloons/gold/balloon.svg) diff --git a/package-lock.json b/package-lock.json index 686404af..0a6dae3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pop-a-loon", - "version": "1.11.2", + "version": "1.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pop-a-loon", - "version": "1.11.2", + "version": "1.12.0", "license": "Apache-2.0", "dependencies": { "@hookform/resolvers": "^3.3.4", @@ -55,7 +55,8 @@ "mini-css-extract-plugin": "^2.9.0", "mkdirp": "^3.0.1", "postcss": "^8.4.33", - "prettier": "^3.3.2", + "prettier": "^3.3.3", + "prettier-plugin-sort-imports": "^1.8.6", "prettier-plugin-tailwindcss": "^0.5.14", "rimraf": "^5.0.5", "style-loader": "^3.3.4", @@ -8143,9 +8144,9 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -8157,6 +8158,18 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-sort-imports": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-1.8.6.tgz", + "integrity": "sha512-jOEzFCyvdDL8geCmr4DP/VBKULZ6OaDQHBEmHTuFHf4EzWyedmwnHg2KawNy5rnrQ6gnCqwrfgymMQZctzCE1Q==", + "dev": true, + "dependencies": { + "prettier": "^3.1.1" + }, + "peerDependencies": { + "typescript": ">4.0.0" + } + }, "node_modules/prettier-plugin-tailwindcss": { "version": "0.5.14", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.14.tgz", diff --git a/package.json b/package.json index 42092745..00df365d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pop-a-loon", - "version": "1.11.2", + "version": "1.12.0", "description": "The new rising trend (literally) that changes the browser game completely.", "private": true, "scripts": { @@ -79,7 +79,8 @@ "mini-css-extract-plugin": "^2.9.0", "mkdirp": "^3.0.1", "postcss": "^8.4.33", - "prettier": "^3.3.2", + "prettier": "^3.3.3", + "prettier-plugin-sort-imports": "^1.8.6", "prettier-plugin-tailwindcss": "^0.5.14", "rimraf": "^5.0.5", "style-loader": "^3.3.4", diff --git a/resources/balloons/base-styles.css b/resources/balloons/base-styles.css new file mode 100644 index 00000000..383ffc57 --- /dev/null +++ b/resources/balloons/base-styles.css @@ -0,0 +1,9 @@ +#balloon-container { + position: fixed; + z-index: 9999; + width: 100%; + height: 100%; + top: 0; + left: 0; + pointer-events: none; +} diff --git a/resources/balloons/default/balloon.svg b/resources/balloons/default/balloon.svg new file mode 100644 index 00000000..24090317 --- /dev/null +++ b/resources/balloons/default/balloon.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/content/style.css b/resources/balloons/default/default.css similarity index 64% rename from src/content/style.css rename to resources/balloons/default/default.css index 711a799b..e00d707c 100644 --- a/src/content/style.css +++ b/resources/balloons/default/default.css @@ -1,13 +1,3 @@ -#balloon-container { - position: fixed; - z-index: 9999; - width: 100%; - height: 100%; - top: 0; - left: 0; - pointer-events: none; -} - .balloon { position: fixed; z-index: 9999; @@ -39,27 +29,27 @@ transform: translateY(100vh); } 100% { - transform: translateY(-10vh); + transform: translateY(var(--rise-to, -10vh)); } } @keyframes swing { 0% { - transform: translateX(15px); + transform: translateX(var(--swing-offset, 15px)); } 50% { - transform: translateX(-15px); + transform: translateX(calc(-1 * var(--swing-offset, 15px))); } 100% { - transform: translateX(15px); + transform: translateX(var(--swing-offset, 15px)); } } @keyframes wave { 0% { - transform: rotate(-8deg); + transform: rotate(calc(-1 * var(--wave-deg, 8deg))); } 100% { - transform: rotate(8deg); + transform: rotate(var(--wave-deg, 8deg)); } -} \ No newline at end of file +} diff --git a/resources/balloons/default/icon.png b/resources/balloons/default/icon.png deleted file mode 100644 index 4d6e70dc..00000000 Binary files a/resources/balloons/default/icon.png and /dev/null differ diff --git a/resources/balloons/gold/balloon.svg b/resources/balloons/gold/balloon.svg new file mode 100644 index 00000000..aaf16a0c --- /dev/null +++ b/resources/balloons/gold/balloon.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/balloon.ts b/src/balloon.ts index e0e99aaa..1b02e41c 100644 --- a/src/balloon.ts +++ b/src/balloon.ts @@ -1,19 +1,31 @@ import browser from 'webextension-polyfill'; -import { getBalloonContainer, sendMessage } from '@/utils'; +import { + getBalloonContainer, + importStylesheet, + joinPaths, + sendMessage, +} from '@/utils'; import { BalloonName } from './const'; /** * The location of the balloon resources. (`resources/balloons/`) */ export const balloonResourceLocation = browser.runtime.getURL( - 'resources/balloons/' + joinPaths('resources', 'balloons') ); export const defaultBalloonFolderName = 'default'; /** * The location of the default balloon resources. (`resources/balloons/default/`) */ -export const defaultBalloonResourceLocation = - balloonResourceLocation + `${defaultBalloonFolderName}/`; +export const defaultBalloonResourceLocation = joinPaths( + balloonResourceLocation, + defaultBalloonFolderName +); + +type StyleSheetProps = { + id?: string; + name?: string; +}; export default abstract class Balloon { public abstract readonly name: BalloonName; @@ -31,11 +43,31 @@ export default abstract class Balloon { public readonly element: HTMLDivElement = document.createElement('div'); + public get resourceLocation(): string { + return joinPaths(balloonResourceLocation, this.name); + } + constructor() { // Add an event listener to the balloon this.element.addEventListener('click', this.pop.bind(this)); } + protected async importStylesheet(name?: string): Promise; + protected async importStylesheet({ + id, + name, + }: StyleSheetProps): Promise; + protected async importStylesheet( + args?: string | StyleSheetProps + ): Promise { + const { id, name } = + typeof args === 'string' ? { id: undefined, name: args } : (args ?? {}); + await importStylesheet( + `${id ?? this.name}-styles`, + joinPaths(this.resourceLocation, name ?? 'styles.css') + ); + } + /** * @returns Whether the balloon is rising. */ @@ -49,6 +81,13 @@ export default abstract class Balloon { * This will create a new balloon element and add it to the balloon container. */ public rise(): void { + // Import base styles + importStylesheet( + 'balloon-styles', + browser.runtime.getURL( + joinPaths('resources', 'balloons', 'base-styles.css') + ) + ); // Build the balloon element this.build(); // Add the balloon to the container diff --git a/src/balloons/confetti.ts b/src/balloons/confetti.ts index 0efc4771..2a811cec 100644 --- a/src/balloons/confetti.ts +++ b/src/balloons/confetti.ts @@ -1,26 +1,22 @@ import { balloonResourceLocation } from '@/balloon'; -import { importStylesheet, random } from '@/utils'; +import { joinPaths, random } from '@/utils'; import Default from './default'; export default class Confetti extends Default { public static readonly spawn_chance: number = 0.1; // @ts-ignore - public readonly name = 'confetti'; + public get name(): 'confetti' { + return 'confetti'; + } private readonly mask = document.createElement('img'); - constructor() { - super(); - importStylesheet( - 'confetti-styles', - balloonResourceLocation + 'confetti/confetti.css' - ); - } - public build(): void { super.build(); + this.importStylesheet('confetti.css'); + this.element.firstChild?.firstChild?.appendChild(this.mask); - this.mask.src = balloonResourceLocation + this.name + '/mask.png'; + this.mask.src = joinPaths(balloonResourceLocation, this.name, 'mask.png'); this.mask.style.position = 'absolute'; this.mask.style.top = '-10px'; this.mask.style.left = '0'; diff --git a/src/balloons/default.ts b/src/balloons/default.ts index b2258d25..2863608a 100644 --- a/src/balloons/default.ts +++ b/src/balloons/default.ts @@ -1,7 +1,7 @@ import Balloon, { balloonResourceLocation } from '@/balloon'; import storage from '@/managers/storage'; import { BalloonName } from '@/const'; -import { random } from '@/utils'; +import { joinPaths, random } from '@/utils'; export type BuildProps = { size: number; @@ -31,29 +31,55 @@ export type BalloonOptions = { * If not provided, the default sound will be used. */ popSoundUrl: string; -}; - -export default class Default extends Balloon { - public static readonly spawn_chance: number = 0.9; - public readonly name = 'default'; - public readonly options: BalloonOptions = { - dir_name: this.name, - imageUrl: '/icon.png', - popSoundUrl: '/pop.mp3', - }; - + /** + * The size of the balloon. + * + * The first value is the minimum size and the second value is the maximum size. + */ + size: [number, number]; /** * The duration thresholds for the rise animation. * * The first value is the minimum duration and the second value is the maximum duration. */ - public readonly riseDurationThreshold: [number, number] = [10000, 15000]; + riseDurationThreshold: [number, number]; /** * The duration thresholds for the swing animation. * * The first value is the minimum duration and the second value is the maximum duration. */ - public readonly swingDurationThreshold: [number, number] = [2, 4]; + swingDurationThreshold: [number, number]; + /** + * The amount of pixels the balloon should wave back and forth. + * + * First `waveDegrees` to the right, return back to the center, then `waveDegrees` to the left. + */ + swingOffset: number; + /** + * The degrees the balloon will tilt when back ant forth. + */ + waveDegrees: number; +}; + +export default class Default extends Balloon { + public static readonly spawn_chance: number = 0.9; + + public get name(): 'default' { + return 'default'; + } + + public get options(): BalloonOptions { + return { + dir_name: this.name, + imageUrl: this.originalPath('/balloon.svg'), + popSoundUrl: this.originalPath('/pop.mp3'), + size: [50, 75], + riseDurationThreshold: [10000, 15000], + swingDurationThreshold: [2, 4], + swingOffset: 15, + waveDegrees: 8, + }; + } /** * The image element for the balloon image. @@ -70,8 +96,10 @@ export default class Default extends Balloon { * The URL of the balloon image. */ public get balloonImageUrl(): string { - return ( - balloonResourceLocation + this.options.dir_name + this.options.imageUrl + return joinPaths( + balloonResourceLocation, + this.options.dir_name, + this.options.imageUrl ); } @@ -79,33 +107,60 @@ export default class Default extends Balloon { * The URL of the pop sound. */ public get popSoundUrl(): string { - return ( - balloonResourceLocation + this.options.dir_name + this.options.popSoundUrl + return joinPaths( + balloonResourceLocation, + this.options.dir_name, + this.options.popSoundUrl ); } - constructor() { - super(); - // Load the pop sound - this.popSound.src = this.popSoundUrl; - // Load the balloon image - this.balloonImage.src = this.balloonImageUrl; + /** + * Get the path for the resources of the default balloon. + * + * This should only be used in the balloon.options. + * + * @param path The path of the resource. + * @returns The original path of the resource. + */ + protected originalPath(name: string): string { + return joinPaths('..', 'default', name); } public build() { - const size = random(50, 75); + this.importStylesheet({ + id: 'default', + name: this.originalPath('default.css'), + }); + const positionX = random(5, 95); + const size = random(this.options.size[0], this.options.size[1]); const riseDuration = random( - this.riseDurationThreshold[0], - this.riseDurationThreshold[1] + this.options.riseDurationThreshold[0], + this.options.riseDurationThreshold[1] ); const waveDuration = random( - this.swingDurationThreshold[0], - this.swingDurationThreshold[1] + this.options.swingDurationThreshold[0], + this.options.swingDurationThreshold[1] ); + // Load the pop sound + this.popSound.src = this.popSoundUrl; + // Load the balloon image + this.balloonImage.src = this.balloonImageUrl; + this.element.classList.add('balloon'); + // Set css variables + this.element.style.setProperty('--rise-to', -size + 'px'); + this.element.style.setProperty( + '--swing-offset', + this.options.swingOffset + 'px' + ); + this.element.style.setProperty( + '--wave-deg', + this.options.waveDegrees + 'deg' + ); + // Set the balloon's width and height this.element.style.width = size + 'px'; this.element.style.height = this.element.style.width; diff --git a/src/balloons/gold.ts b/src/balloons/gold.ts new file mode 100644 index 00000000..de632190 --- /dev/null +++ b/src/balloons/gold.ts @@ -0,0 +1,19 @@ +import Default, { BalloonOptions } from './default'; + +export default class Gold extends Default { + public static readonly spawn_chance: number = 0.05; + // @ts-ignore + public get name(): 'gold' { + return 'gold'; + } + + public get options(): BalloonOptions { + return { + ...super.options, + imageUrl: '/balloon.svg', + riseDurationThreshold: [15000, 20000], + swingDurationThreshold: [3, 4], + size: [100, 125], + }; + } +} diff --git a/src/balloons/index.ts b/src/balloons/index.ts index 05d885f6..872d0bc0 100644 --- a/src/balloons/index.ts +++ b/src/balloons/index.ts @@ -1,2 +1,3 @@ export { default as Default } from './default'; export { default as Confetti } from './confetti'; +export { default as Gold } from './gold'; diff --git a/src/const.ts b/src/const.ts index 0177b0d1..23f2b9c7 100644 --- a/src/const.ts +++ b/src/const.ts @@ -8,12 +8,14 @@ import * as Balloons from '@/balloons'; type _initialConfig = { popVolume: number; spawnRate: number; + fullScreenVideoSpawn: boolean; } & RemoteConfig; export const initalConfig: _initialConfig = { // Local config popVolume: 70, spawnRate: 1, + fullScreenVideoSpawn: false, // Remote config -> can be overriden by the remote badge: { diff --git a/src/content/spawn-balloon.ts b/src/content/spawn-balloon.ts index fd97baf5..485933eb 100644 --- a/src/content/spawn-balloon.ts +++ b/src/content/spawn-balloon.ts @@ -1,21 +1,30 @@ import browser from 'webextension-polyfill'; import * as balloons from '@/balloons'; -import { getBalloonContainer, importStylesheet, weightedRandom } from '@/utils'; +import { + getBalloonContainer, + importStylesheet, + isFullScreenVideoPlaying, + weightedRandom, +} from '@/utils'; import log from '@/managers/log'; import storage from '@/managers/storage'; (async () => { // Prevent running in popup if (document.body.id === 'pop-a-loon') return; + + const config = await storage.sync.get('config'); + + // Run checks to see if the balloon should spawn + if (!config.fullScreenVideoSpawn && isFullScreenVideoPlaying()) { + log.debug('Full screen video playing, not spawning balloon'); + return; + } + log.setLevel((await storage.local.get('loglevel')) || 'info'); log.groupCollapsed('debug', 'Pop-a-loon: Spawning balloon'); log.time('debug', 'Balloon spawn time'); - importStylesheet( - 'balloon-styles', - browser.runtime.getURL('resources/stylesheets/style.css') - ); - // Add the balloon container to the document const _ = getBalloonContainer(); diff --git a/src/popup/components/forms/LocalSettings.tsx b/src/popup/components/forms/LocalSettings.tsx index b327143f..d103415d 100644 --- a/src/popup/components/forms/LocalSettings.tsx +++ b/src/popup/components/forms/LocalSettings.tsx @@ -3,6 +3,7 @@ import browser, { manifest, type Permissions } from 'webextension-polyfill'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { Form, FormField, @@ -17,6 +18,7 @@ import { Default as DefaultBalloon } from '@/balloons'; import storage from '@/managers/storage'; import log from '@/managers/log'; import { askOriginPermissions } from '@/utils'; +import { initalConfig } from '@/const'; const MIN_POP_VOLUME = 0; const VOLUME_STEP = 20; @@ -28,6 +30,7 @@ const SPAWN_RATE_STEP = 0.1; const formSchema = z.object({ popVolume: z.number().int().min(MIN_POP_VOLUME).max(MAX_POP_VOLUME), spawnRate: z.number().int().min(MIN_SPAWN_RATE).max(MAX_SPAWN_RATE), + fullScreenVideoSpawn: z.boolean(), permissions: z.object({ origins: z.array(z.string()), permissions: z.array(z.string()), @@ -35,8 +38,11 @@ const formSchema = z.object({ }); export default () => { - const [popVolume, setPopVolume] = useState(0); - const [spawnRate, setSpawnRate] = useState(0); + const [popVolume, setPopVolume] = useState(initalConfig.popVolume); + const [spawnRate, setSpawnRate] = useState(initalConfig.spawnRate); + const [fullScreenVideoSpawn, setFullScreenVideoSpawn] = useState( + initalConfig.fullScreenVideoSpawn + ); const [permissions, setPermissions] = useState( {} ); @@ -77,6 +83,23 @@ export default () => { ); }; + const onFullScreenVideoSpawnChange = async ( + fullScreenVideoSpawn: boolean + ) => { + // Save volume to storage + const config = await storage.sync.get('config'); + await storage.sync.set('config', { + ...config, + fullScreenVideoSpawn, + }); + + setFullScreenVideoSpawn(fullScreenVideoSpawn); + log.debug( + 'Spawning in full screen video players:', + (await storage.sync.get('config')).fullScreenVideoSpawn + ); + }; + const onGrantOriginPermissionClick = async () => { await askOriginPermissions(); setPermissions(await browser.permissions.getAll()); @@ -88,6 +111,7 @@ export default () => { // Load volume from storage setPopVolume(config.popVolume); setSpawnRate(config.spawnRate); + setFullScreenVideoSpawn(config.fullScreenVideoSpawn); setPermissions(await browser.permissions.getAll()); }; @@ -168,6 +192,45 @@ export default () => { )} /> + ( + + + Fullscreen video spawn + + + { + onChange(val); + onFullScreenVideoSpawnChange(!!val); + }} + /> + + +

+ Fullscreen video spawn +

+

+ Weither or not to spawn balloons in fullscreen video + players, like youtube. +

+

+ {fullScreenVideoSpawn ? ( + <>Balloons can spawn! + ) : ( + <>Balloons will not spawn. + )} +

+
+
+
+ +
+ )} + /> {/* If the user hasn't granted the host permissions; show the grant permission button */} {!(permissions.origins?.length !== 0) && ( { + const fullscreenElement = document.fullscreenElement; + if (fullscreenElement) { + if (fullscreenElement.tagName.toLowerCase() === 'video') { + return true; + } + const videos = [ + ...fullscreenElement.getElementsByTagName('video'), + ...document.getElementsByTagName('video'), + ]; + if (videos.length > 0) { + return true; + } + } + return false; +}; + +export const joinPaths = (...paths: string[]): string => { + return paths + .map((part, index) => { + if (index === 0) { + return part.trim().replace(/[/]*$/g, ''); + } else { + return part.trim().replace(/(^[/]*|[/]*$)/g, ''); + } + }) + .filter((part) => part.length) + .join('/'); +}; diff --git a/tailwind.config.js b/tailwind.config.js index cb89bead..81fbbb35 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,50 +1,63 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{html,tsx,jsx,ts,js}"], + content: ['./src/**/*.{html,tsx,jsx,ts,js}'], theme: { extend: { colors: { - border: "var(--border)", - input: "var(--input)", - ring: "var(--ring)", - background: "var(--background)", - foreground: "var(--foreground)", + border: 'var(--border)', + input: 'var(--input)', + ring: 'var(--ring)', + background: 'var(--background)', + foreground: 'var(--foreground)', primary: { - DEFAULT: "var(--primary)", - foreground: "var(--primary-foreground)", + DEFAULT: 'var(--primary)', + foreground: 'var(--primary-foreground)', }, secondary: { - DEFAULT: "var(--secondary)", - foreground: "var(--secondary-foreground)", + DEFAULT: 'var(--secondary)', + foreground: 'var(--secondary-foreground)', }, destructive: { - DEFAULT: "var(--destructive)", - foreground: "var(--destructive-foreground)", + DEFAULT: 'var(--destructive)', + foreground: 'var(--destructive-foreground)', }, muted: { - DEFAULT: "var(--muted)", - foreground: "var(--muted-foreground)", + DEFAULT: 'var(--muted)', + foreground: 'var(--muted-foreground)', }, accent: { - DEFAULT: "var(--accent)", - foreground: "var(--accent-foreground)", + DEFAULT: 'var(--accent)', + foreground: 'var(--accent-foreground)', }, popover: { - DEFAULT: "var(--popover)", - foreground: "var(--popover-foreground)", + DEFAULT: 'var(--popover)', + foreground: 'var(--popover-foreground)', }, card: { - DEFAULT: "var(--card)", - foreground: "var(--card-foreground)", + DEFAULT: 'var(--card)', + foreground: 'var(--card-foreground)', }, }, borderRadius: { - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - } + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)', + }, + keyframes: { + 'accordion-down': { + from: { height: '0' }, + to: { height: 'var(--radix-accordion-content-height)' }, + }, + 'accordion-up': { + from: { height: 'var(--radix-accordion-content-height)' }, + to: { height: '0' }, + }, + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out', + }, }, }, - plugins: [], -} - + plugins: [require('tailwindcss-animate')], +}; diff --git a/tests/balloon.test.ts b/tests/balloon.test.ts index 3506f333..3666e5a7 100644 --- a/tests/balloon.test.ts +++ b/tests/balloon.test.ts @@ -1,5 +1,8 @@ import Balloon from '@/balloon'; import { BalloonName } from '@/const'; +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); // Create a concrete subclass of Balloon for testing class TestBalloon extends Balloon { @@ -14,6 +17,8 @@ describe('Balloon', () => { beforeEach(() => { balloon = new TestBalloon(); + + fetchMock.resetMocks(); }); test('isRising should return a boolean', () => { diff --git a/tests/balloons/default.test.ts b/tests/balloons/default.test.ts index f838f2e6..95d27409 100644 --- a/tests/balloons/default.test.ts +++ b/tests/balloons/default.test.ts @@ -1,10 +1,15 @@ import { Default } from '@/balloons'; +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); describe('Default Balloon', () => { let balloon: Default; beforeEach(() => { balloon = new Default(); + + fetchMock.resetMocks(); }); test('name should be "default"', () => { diff --git a/tests/balloons/gold.test.ts b/tests/balloons/gold.test.ts new file mode 100644 index 00000000..d7c378fb --- /dev/null +++ b/tests/balloons/gold.test.ts @@ -0,0 +1,23 @@ +import { Gold } from '@/balloons'; +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); + +describe('Gold Balloon', () => { + let balloon: Gold; + + beforeEach(() => { + balloon = new Gold(); + + fetchMock.resetMocks(); + }); + + test('name should be "gold"', () => { + expect(balloon.name).toBe('gold'); + }); + + test('name should be the same as the class name', () => { + expect(balloon.name).toBe('gold'); + expect(balloon.name).toBe(Gold.name.toLowerCase()); + }); +}); diff --git a/webpack.config.js b/webpack.config.js index 49299f54..094f959b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -82,14 +82,6 @@ module.exports = { patterns: [ // Copy resource files to dist { from: 'resources/', to: 'resources/' }, - // Copy sylesheets to dist but exclude stylesheets from popup folder - { - from: 'src/**/*.css', - globOptions: { - ignore: ['**/popup/**'], - }, - to: 'resources/stylesheets/[name][ext]', - }, // Copy manifest.json to dist { from: `manifest.json`,