mirror of
https://github.com/thedevs-network/kutt-extension.git
synced 2026-02-03 13:53:23 +00:00
Merge pull request #80 from abhijithvijayan/refactor/react-typescript
TypeScript + React Complete Rewrite
This commit is contained in:
12
.babelrc
12
.babelrc
@@ -1,22 +1,30 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
// Latest stable ECMAScript features
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"firefox": "52",
|
||||
"opera": "36"
|
||||
"opera": "36",
|
||||
"edge": "79"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
// Some transforms (such as object-rest-spread)
|
||||
// don't work without it: https://github.com/babel/babel/issues/7215
|
||||
["@babel/plugin-transform-destructuring", { "useBuiltIns": true }],
|
||||
["@babel/plugin-proposal-object-rest-spread", { "useBuiltIns": true }],
|
||||
[
|
||||
// Polyfills the runtime needed for async/await and generators
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"helpers": false,
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,5 @@
|
||||
node_modules/
|
||||
extension/
|
||||
dist/
|
||||
extension/
|
||||
.yarn/
|
||||
.pnp.js
|
||||
@@ -1,5 +1,27 @@
|
||||
{
|
||||
"extends": [
|
||||
"onepass"
|
||||
]
|
||||
}
|
||||
"extends":["onepass"],
|
||||
"rules": {
|
||||
"no-console": 0,
|
||||
"no-extend-native": 0,
|
||||
"react/jsx-filename-extension": [1, { "extensions": [".jsx", "tsx"] }],
|
||||
"react/jsx-props-no-spreading": 0,
|
||||
"jsx-a11y/label-has-associated-control": 0,
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"],
|
||||
"moduleDirectory": ["node_modules", "src/"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -66,3 +66,14 @@ typings/
|
||||
|
||||
## scripts build
|
||||
extension/
|
||||
dist/
|
||||
|
||||
# awesome-ts-loader cache
|
||||
.awcache
|
||||
|
||||
# yarn 2
|
||||
# https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089
|
||||
.yarn/*
|
||||
!.yarn/releases
|
||||
!.yarn/plugins
|
||||
.pnp.*
|
||||
147315
.yarn/releases/yarn-1.21.1.js
vendored
Executable file
147315
.yarn/releases/yarn-1.21.1.js
vendored
Executable file
File diff suppressed because one or more lines are too long
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
@@ -0,0 +1 @@
|
||||
yarnPath: .yarn/releases/yarn-1.21.1.js
|
||||
23
README.md
23
README.md
@@ -1,8 +1,6 @@
|
||||
<div align="center"><img width="150" src="src/assets/logo.png" /></div>
|
||||
<h1 align="center">kutt-extension</h1>
|
||||
<p align="center">Browser extension to shorten long URLs based on <a href="https://kutt.it">Kutt.it</a></p>
|
||||
<h3 align="center">🙋♂️ Made by <a href="https://twitter.com/_abhijithv">@abhijithvijayan</a></h3>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://travis-ci.org/abhijithvijayan/kutt-extension">
|
||||
<img src="https://travis-ci.org/abhijithvijayan/kutt-extension.svg?branch=master" alt="Travis Build" />
|
||||
@@ -23,8 +21,20 @@
|
||||
<img src="https://img.shields.io/github/license/abhijithvijayan/kutt-extension.svg" alt="LICENSE" />
|
||||
</a>
|
||||
</div>
|
||||
<h3 align="center">🙋♂️ Made by <a href="https://twitter.com/_abhijithv">@abhijithvijayan</a></h3>
|
||||
<p align="center">
|
||||
Donate:
|
||||
<a href="https://www.paypal.me/iamabhijithvijayan" target='_blank'><i><b>PayPal</b></i></a>,
|
||||
<a href="https://www.patreon.com/abhijithvijayan" target='_blank'><i><b>Patreon</b></i></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href='https://www.buymeacoffee.com/abhijithvijayan' target='_blank'>
|
||||
<img height='36' style='border:0px;height:36px;' src='https://bmc-cdn.nyc3.digitaloceanspaces.com/BMC-button-images/custom_images/orange_img.png' border='0' alt='Buy Me a Coffee' />
|
||||
</a>
|
||||
</p>
|
||||
<hr />
|
||||
|
||||
## v2 (🚧 [WIP](https://github.com/abhijithvijayan/kutt-extension/tree/refactor/react-typescript))
|
||||
### v2 React + TypeScript (🚧 [WIP](https://github.com/abhijithvijayan/kutt-extension/tree/refactor/react-typescript))
|
||||
|
||||
## Features
|
||||
|
||||
@@ -39,10 +49,9 @@
|
||||
|
||||
## Browser Support
|
||||
|
||||
| [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://addons.mozilla.org/firefox/addon/kutt/) | [](CONTRIBUTING.md#for-opera-users) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) |
|
||||
 |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 49 & later ✔ | 52 & later ✔ | 36 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔
|
||||
| [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://addons.mozilla.org/firefox/addon/kutt/) | [](CONTRIBUTING.md#for-opera-users) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) | [](https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd) |
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 49 & later ✔ | 52 & later ✔ | 36 & later ✔ | 79 & later ✔ | Latest ✔ | Latest ✔ | Latest ✔
|
||||
|
||||
## How to use
|
||||
|
||||
|
||||
124
package.json
124
package.json
@@ -1,15 +1,34 @@
|
||||
{
|
||||
"name": "kutt-extension",
|
||||
"version": "3.2.1",
|
||||
"main": "src/scripts/background.js",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8 <=12"
|
||||
},
|
||||
"author": "abhijithvijayan",
|
||||
"description": "Kutt.it extension for browsers.",
|
||||
"repository": "git+https://github.com/abhijithvijayan/kutt-extension.git",
|
||||
"engines": {
|
||||
"node": ">=8 <=12",
|
||||
"yarn": ">= 1.0.0"
|
||||
},
|
||||
"repository": "https://github.com/abhijithvijayan/kutt-extension.git",
|
||||
"author": "abhijithvijayan <34790378+abhijithvijayan@users.noreply.github.com>",
|
||||
"scripts": {
|
||||
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch",
|
||||
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch",
|
||||
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch",
|
||||
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack",
|
||||
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack",
|
||||
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack",
|
||||
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint:fix": "eslint . --ext .ts --fix"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"npm run lint:fix"
|
||||
]
|
||||
},
|
||||
"keywords": [
|
||||
"url",
|
||||
"shortener",
|
||||
@@ -18,67 +37,60 @@
|
||||
"addon",
|
||||
"kutt"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/abhijithvijayan/kutt-extension/issues"
|
||||
},
|
||||
"homepage": "https://github.com/abhijithvijayan/kutt-extension#readme",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch --mode=development",
|
||||
"dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch --mode=development",
|
||||
"dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch --mode=development",
|
||||
"build:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack --mode=production",
|
||||
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack --mode=production",
|
||||
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack --mode=production",
|
||||
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera"
|
||||
},
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"axios": "^0.19.0",
|
||||
"qrcode": "^1.4.4",
|
||||
"webextension-polyfill": "^0.5.0",
|
||||
"wext-manifest": "^2.0.1"
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"axios": "^0.19.2",
|
||||
"formik": "^2.1.3",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"webextension-polyfill-ts": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"autoprefixer": "^9.7.3",
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
|
||||
"@babel/plugin-transform-destructuring": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"@types/lodash.isequal": "^4.5.5",
|
||||
"@types/react": "^16.9.19",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/webpack": "^4.41.3",
|
||||
"@typescript-eslint/eslint-plugin": "^2.17.0",
|
||||
"@typescript-eslint/parser": "^2.17.0",
|
||||
"autoprefixer": "^9.7.4",
|
||||
"awesome-typescript-loader": "^5.2.1",
|
||||
"babel-eslint": "10.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^5.1.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"cross-env": "^6.0.3",
|
||||
"css-loader": "^3.3.0",
|
||||
"eslint": "^6.7.2",
|
||||
"css-loader": "^3.4.2",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-airbnb": "^18.0.1",
|
||||
"eslint-config-onepass": "1.5.0",
|
||||
"eslint-config-prettier": "^6.7.0",
|
||||
"eslint-config-onepass": "2.1.0",
|
||||
"eslint-config-prettier": "^6.9.0",
|
||||
"eslint-plugin-html": "^6.0.0",
|
||||
"eslint-plugin-import": "^2.19.1",
|
||||
"eslint-plugin-import": "^2.20.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-react": "^7.18.0",
|
||||
"eslint-plugin-react-hooks": "^2.3.0",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^5.0.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^4.0.0-beta.5",
|
||||
"node-sass": "^4.13.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^4.2.1",
|
||||
"lint-staged": "^10.0.3",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"precss": "^4.0.0",
|
||||
"prettier": "^1.19.1",
|
||||
"resolve-url-loader": "^3.1.1",
|
||||
"sass-loader": "^7.3.1",
|
||||
"terser-webpack-plugin": "^1.4.3",
|
||||
"url-loader": "^2.3.0",
|
||||
"webpack": "^4.41.2",
|
||||
"sass-loader": "^8.0.2",
|
||||
"typescript": "^3.7.5",
|
||||
"webpack": "^4.41.5",
|
||||
"webpack-cli": "^3.3.10",
|
||||
"webpack-dev-server": "^3.9.0",
|
||||
"webpack-fix-style-only-entries": "^0.3.1",
|
||||
"write-webpack-plugin": "^1.1.0",
|
||||
"zip-webpack-plugin": "^3.0.0"
|
||||
"webpack-extension-reloader": "^1.1.4",
|
||||
"wext-manifest": "^2.1.0",
|
||||
"write-webpack-plugin": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
5
src/Background/constants.ts
Normal file
5
src/Background/constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const CHECK_API_KEY = 'api.checkApiKey';
|
||||
export const CHECK_API_KEY_TIMEOUT = 8000; // 8secs
|
||||
|
||||
export const SHORTEN_URL = 'api.shortenUrl';
|
||||
export const SHORTEN_URL_TIMEOUT = 20000; // 20secs
|
||||
177
src/Background/index.ts
Normal file
177
src/Background/index.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* @author abhijithvijayan <abhijithvijayan.in>
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { browser } from 'webextension-polyfill-ts';
|
||||
|
||||
import { AxiosPromise } from 'axios';
|
||||
import * as constants from './constants';
|
||||
import api from '../api';
|
||||
|
||||
type ShortenUrlBodyProperties = {
|
||||
target: string;
|
||||
password?: string;
|
||||
customurl?: string;
|
||||
reuse: boolean;
|
||||
domain?: string;
|
||||
};
|
||||
|
||||
export interface ApiBodyProperties extends ShortenUrlBodyProperties {
|
||||
apikey: string;
|
||||
}
|
||||
|
||||
type ShortenLinkResponseProperties = {
|
||||
id: string;
|
||||
address: string;
|
||||
banned: boolean;
|
||||
password: boolean;
|
||||
target: string;
|
||||
visit_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
link: string;
|
||||
};
|
||||
|
||||
export type ApiErroredProperties = {
|
||||
error: true;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type SuccessfulShortenStatusProperties = {
|
||||
error: false;
|
||||
data: ShortenLinkResponseProperties;
|
||||
};
|
||||
|
||||
async function shortenUrl(
|
||||
params: ApiBodyProperties
|
||||
): Promise<SuccessfulShortenStatusProperties | ApiErroredProperties> {
|
||||
try {
|
||||
// extract `apikey` from body
|
||||
const { apikey, ...otherParams } = params;
|
||||
|
||||
const { data }: { data: ShortenLinkResponseProperties } = await api({
|
||||
method: 'POST',
|
||||
timeout: constants.SHORTEN_URL_TIMEOUT,
|
||||
url: `/api/v2/links`,
|
||||
headers: {
|
||||
'X-API-Key': apikey,
|
||||
},
|
||||
data: {
|
||||
...otherParams,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
error: false,
|
||||
data,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Invalid API Key',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Timed out',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Something went wrong',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getUserSettings(apikey: string): AxiosPromise<any> {
|
||||
return api({
|
||||
method: 'GET',
|
||||
url: '/api/v2/users',
|
||||
timeout: constants.CHECK_API_KEY_TIMEOUT,
|
||||
headers: {
|
||||
'X-API-Key': apikey,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type DomainEntryProperties = {
|
||||
address: string;
|
||||
banned: boolean;
|
||||
created_at: string;
|
||||
id: string;
|
||||
homepage: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type UserSettingsResponseProperties = {
|
||||
apikey: string;
|
||||
email: string;
|
||||
domains: DomainEntryProperties[];
|
||||
};
|
||||
|
||||
export type SuccessfulApiKeyCheckProperties = {
|
||||
error: false;
|
||||
data: UserSettingsResponseProperties;
|
||||
};
|
||||
|
||||
async function checkApiKey(apikey: string): Promise<SuccessfulApiKeyCheckProperties | ApiErroredProperties> {
|
||||
try {
|
||||
const { data }: { data: UserSettingsResponseProperties } = await getUserSettings(apikey);
|
||||
|
||||
return {
|
||||
error: false,
|
||||
data,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.response) {
|
||||
if (err.response.status === 401) {
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Invalid API Key',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Something went wrong.',
|
||||
};
|
||||
}
|
||||
|
||||
if (err.code === 'ECONNABORTED') {
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Timed out',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: true,
|
||||
message: 'Error: Please check your internet connection',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for messages from UI
|
||||
*/
|
||||
browser.runtime.onMessage.addListener((request, sender): void | Promise<any> => {
|
||||
console.log('message received', request);
|
||||
|
||||
// eslint-disable-next-line default-case
|
||||
switch (request.action) {
|
||||
case constants.CHECK_API_KEY: {
|
||||
return checkApiKey(request.params.apikey);
|
||||
}
|
||||
|
||||
case constants.SHORTEN_URL: {
|
||||
return shortenUrl(request.params);
|
||||
}
|
||||
}
|
||||
});
|
||||
45
src/Options/Options.tsx
Normal file
45
src/Options/Options.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { getExtensionSettings } from '../util/settings';
|
||||
import BodyWrapper from '../components/BodyWrapper';
|
||||
import Loader from '../components/Loader';
|
||||
import OptionsForm, { OptionsFormValuesProperties } from './OptionsForm';
|
||||
|
||||
const Options: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [defaultValues, setDefaultValues] = useState<OptionsFormValuesProperties>({
|
||||
apikey: '',
|
||||
autocopy: true,
|
||||
history: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function getSavedSettings(): Promise<void> {
|
||||
const { settings = {} } = await getExtensionSettings();
|
||||
|
||||
// inject existing keys (if field doesn't exist, use default)
|
||||
const defaultFormValues: OptionsFormValuesProperties = {
|
||||
apikey: settings.apikey || defaultValues.apikey,
|
||||
autocopy: Object.prototype.hasOwnProperty.call(settings, 'autocopy')
|
||||
? settings.autocopy
|
||||
: defaultValues.autocopy,
|
||||
history: Object.prototype.hasOwnProperty.call(settings, 'history')
|
||||
? settings.history
|
||||
: defaultValues.history,
|
||||
};
|
||||
|
||||
setDefaultValues(defaultFormValues);
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
getSavedSettings();
|
||||
}, [defaultValues.apikey, defaultValues.autocopy, defaultValues.history]);
|
||||
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<div id="options">{!loading ? <OptionsForm defaultValues={defaultValues} /> : <Loader />}</div>
|
||||
</BodyWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Options;
|
||||
115
src/Options/OptionsForm.tsx
Normal file
115
src/Options/OptionsForm.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { withFormik, Field, Form, FormikHelpers, FormikProps, FormikErrors } from 'formik';
|
||||
|
||||
import AutoSave from '../util/autoSave';
|
||||
import messageUtil from '../util/mesageUtil';
|
||||
import { CHECK_API_KEY } from '../Background/constants';
|
||||
import { TextField, CheckBox } from '../components/Input';
|
||||
import { updateExtensionSettings } from '../util/settings';
|
||||
import { SuccessfulApiKeyCheckProperties, ApiErroredProperties } from '../Background';
|
||||
|
||||
export type OptionsFormValuesProperties = {
|
||||
apikey: string;
|
||||
autocopy: boolean;
|
||||
history: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onSave = (values: OptionsFormValuesProperties): Promise<any> => {
|
||||
// should always return a Promise
|
||||
return updateExtensionSettings(values); // update local settings
|
||||
};
|
||||
|
||||
// Note: The default key-value pairs are not saved to storage without any first interaction
|
||||
const InnerForm: React.FC<FormikProps<OptionsFormValuesProperties>> = props => {
|
||||
// ToDo: Replace `Saving` text with Spinning Loader
|
||||
const { isSubmitting, handleSubmit } = props;
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} autoComplete="off" id="options__form">
|
||||
<div>
|
||||
<Field name="apikey" type="password" component={TextField} label="API Key" />
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
Validate
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Field name="autocopy" component={CheckBox} label="Auto Copy URL to Clipboard" />
|
||||
</div>
|
||||
<div>
|
||||
<Field name="history" component={CheckBox} label="Keep URLs History" />
|
||||
</div>
|
||||
<AutoSave
|
||||
onSave={onSave}
|
||||
render={({ isSaving }: { isSaving: boolean }): string | null => {
|
||||
return isSaving ? 'Saving' : null;
|
||||
}}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
// The type of props `OptionsForm` receives
|
||||
type OptionsFormProperties = {
|
||||
defaultValues: OptionsFormValuesProperties;
|
||||
};
|
||||
|
||||
// Wrap our form with the withFormik HoC
|
||||
const OptionsForm = withFormik<OptionsFormProperties, OptionsFormValuesProperties>({
|
||||
// Transform outer props into form values
|
||||
mapPropsToValues: ({ defaultValues: { apikey, autocopy, history } }) => {
|
||||
return {
|
||||
apikey,
|
||||
autocopy,
|
||||
history,
|
||||
};
|
||||
},
|
||||
|
||||
validate: (values: OptionsFormValuesProperties) => {
|
||||
const errors: FormikErrors<OptionsFormValuesProperties> = {};
|
||||
|
||||
if (!values.apikey) {
|
||||
errors.apikey = 'API key missing';
|
||||
}
|
||||
// ToDo: restore before on production
|
||||
// else if (values.apikey && values.apikey.trim().length < 40) {
|
||||
// errors.apikey = 'API key must be 40 characters';
|
||||
// } else if (values.apikey && values.apikey.trim().length > 40) {
|
||||
// errors.apikey = 'API key cannot exceed 40 characters';
|
||||
// }
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
// for API Key validation only
|
||||
handleSubmit: async (
|
||||
values: OptionsFormValuesProperties,
|
||||
{ setSubmitting }: FormikHelpers<OptionsFormValuesProperties>
|
||||
) => {
|
||||
const response: SuccessfulApiKeyCheckProperties | ApiErroredProperties = await messageUtil.send(CHECK_API_KEY, {
|
||||
apikey: values.apikey.trim(),
|
||||
});
|
||||
|
||||
if (!response.error) {
|
||||
// ToDo: show valid api key status
|
||||
console.log('Valid API Key');
|
||||
|
||||
const { domains, email } = response.data;
|
||||
// Store user account information
|
||||
await updateExtensionSettings({ user: { domains, email } });
|
||||
} else {
|
||||
// ---- errored ---- //
|
||||
// Delete `user` field from settings
|
||||
await updateExtensionSettings({ user: null });
|
||||
|
||||
console.log(response.message);
|
||||
}
|
||||
|
||||
// enable validate button
|
||||
setSubmitting(false);
|
||||
},
|
||||
|
||||
displayName: 'OptionsForm',
|
||||
})(InnerForm);
|
||||
|
||||
export default OptionsForm;
|
||||
8
src/Options/index.tsx
Normal file
8
src/Options/index.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
import Options from './Options';
|
||||
|
||||
ReactDOM.render(<Options />, document.getElementById('options-root'));
|
||||
38
src/Options/styles.scss
Normal file
38
src/Options/styles.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
background-color: $grey-white;
|
||||
}
|
||||
|
||||
#options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 20% 10%;
|
||||
font-size: 1.125em;
|
||||
|
||||
#options__form {
|
||||
button {
|
||||
padding: 7px 22px;
|
||||
margin: 15px auto;
|
||||
cursor: pointer;
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
div {
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
|
||||
.error {
|
||||
font-size: 11px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Popup/Header.tsx
Normal file
23
src/Popup/Header.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import Icon from '../components/Icon';
|
||||
import { openExtOptionsPage } from '../util/tabs';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<header id="header">
|
||||
<div className="logo__holder">
|
||||
<img src="assets/logo.png" alt="logo" style={{ width: '22px', height: '22px' }} />
|
||||
</div>
|
||||
<div className="action__buttons--holder">
|
||||
<button type="button" className="icon" onClick={openExtOptionsPage}>
|
||||
<Icon name="settings" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
123
src/Popup/Popup.tsx
Normal file
123
src/Popup/Popup.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { UserSettingsResponseProperties } from '../Background';
|
||||
import { getExtensionSettings } from '../util/settings';
|
||||
import BodyWrapper from '../components/BodyWrapper';
|
||||
import Loader from '../components/Loader';
|
||||
import PopupForm from './PopupForm';
|
||||
import PopupHeader from './Header';
|
||||
import PopupBody, { ProcessedRequestProperties } from './PopupBody';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
type DomainOptionsProperties = {
|
||||
option: string;
|
||||
value: string;
|
||||
id: string;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export type ProcessRequestProperties = React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
error: boolean | null;
|
||||
message: string;
|
||||
}>
|
||||
>;
|
||||
|
||||
export type UserConfigProperties = {
|
||||
apikey: string;
|
||||
domainOptions: DomainOptionsProperties[];
|
||||
};
|
||||
|
||||
const Popup: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [userConfig, setUserConfig] = useState<UserConfigProperties>({
|
||||
apikey: '',
|
||||
domainOptions: [],
|
||||
});
|
||||
const [requestProcessed, setRequestProcessed] = useState<ProcessedRequestProperties>({ error: null, message: '' });
|
||||
|
||||
useEffect((): void => {
|
||||
async function getUserSettings(): Promise<void> {
|
||||
// ToDo: type
|
||||
const { settings = {} } = await getExtensionSettings();
|
||||
// ToDo: change kutt.it entry to custom host(if exist)
|
||||
const defaultOptions: DomainOptionsProperties[] = [
|
||||
{
|
||||
id: '',
|
||||
option: '-- Choose Domain --',
|
||||
value: '',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'default',
|
||||
option: 'kutt.it',
|
||||
value: 'https://kutt.it',
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
// No API Key set
|
||||
if (!Object.prototype.hasOwnProperty.call(settings, 'apikey') || settings.apikey === '') {
|
||||
setRequestProcessed({ error: true, message: 'Extension requires an API Key to work' });
|
||||
setLoading(false);
|
||||
|
||||
// ToDo: Open options page after slight delay
|
||||
return;
|
||||
}
|
||||
|
||||
// `user` & `apikey` fields exist on storage
|
||||
if (Object.prototype.hasOwnProperty.call(settings, 'user') && settings.user) {
|
||||
const { user }: { user: UserSettingsResponseProperties } = settings;
|
||||
|
||||
let optionsArray: DomainOptionsProperties[] = user.domains.map(({ id, address, homepage, banned }) => {
|
||||
return {
|
||||
id,
|
||||
option: homepage,
|
||||
value: address,
|
||||
disabled: banned,
|
||||
};
|
||||
});
|
||||
|
||||
// merge to beginning of array
|
||||
optionsArray = defaultOptions.concat(optionsArray);
|
||||
|
||||
// update domain list
|
||||
setUserConfig({ apikey: settings.apikey, domainOptions: optionsArray });
|
||||
} else {
|
||||
// no `user` but `apikey` exist on storage
|
||||
setUserConfig({ apikey: settings.apikey, domainOptions: defaultOptions });
|
||||
}
|
||||
|
||||
// ToDo: handle init operations(if any)
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
getUserSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BodyWrapper>
|
||||
<div id="popup">
|
||||
{!loading ? (
|
||||
<>
|
||||
<PopupHeader />
|
||||
{(requestProcessed.error !== null && (
|
||||
<PopupBody requestProcessed={requestProcessed} setRequestProcessed={setRequestProcessed} />
|
||||
)) || (
|
||||
<PopupForm
|
||||
defaultDomainId="default"
|
||||
userConfig={userConfig}
|
||||
setRequestProcessed={setRequestProcessed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</div>
|
||||
</BodyWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popup;
|
||||
35
src/Popup/PopupBody.tsx
Normal file
35
src/Popup/PopupBody.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ProcessRequestProperties } from './Popup';
|
||||
|
||||
export type ProcessedRequestProperties = {
|
||||
error: boolean | null;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type PopupBodyProperties = {
|
||||
requestProcessed: ProcessedRequestProperties;
|
||||
setRequestProcessed: ProcessRequestProperties;
|
||||
};
|
||||
|
||||
const PopupBody: React.FC<PopupBodyProperties> = ({ requestProcessed: { message, error }, setRequestProcessed }) => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{!error ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(): void => {
|
||||
return setRequestProcessed({ error: null, message: '' });
|
||||
}}
|
||||
>
|
||||
Go Back
|
||||
</button>
|
||||
) : null}
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupBody;
|
||||
154
src/Popup/PopupForm.tsx
Normal file
154
src/Popup/PopupForm.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { withFormik, Field, Form, FormikBag, FormikProps, FormikErrors } from 'formik';
|
||||
|
||||
import Loader from '../components/Loader';
|
||||
import messageUtil from '../util/mesageUtil';
|
||||
import { UserConfigProperties, ProcessRequestProperties } from './Popup';
|
||||
import { getCurrentTab } from '../util/tabs';
|
||||
|
||||
import { SHORTEN_URL } from '../Background/constants';
|
||||
import { SelectField, TextField } from '../components/Input';
|
||||
import { ApiBodyProperties, SuccessfulShortenStatusProperties, ApiErroredProperties } from '../Background';
|
||||
|
||||
type PopupFormValuesProperties = {
|
||||
password: string;
|
||||
customurl: string;
|
||||
domain: string;
|
||||
};
|
||||
|
||||
const InnerForm: React.FC<PopupFormProperties & FormikProps<PopupFormValuesProperties>> = props => {
|
||||
const {
|
||||
isSubmitting,
|
||||
handleSubmit,
|
||||
userConfig: { domainOptions },
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubmitting ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<Form onSubmit={handleSubmit} autoComplete="off" id="popup__form">
|
||||
<div>
|
||||
<Field
|
||||
name="domain"
|
||||
type="text"
|
||||
component={SelectField}
|
||||
label="Domain"
|
||||
options={domainOptions}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Field name="customurl" type="text" component={TextField} label="Custom URL" />
|
||||
</div>
|
||||
<div>
|
||||
<Field name="password" type="password" component={TextField} label="Password" />
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={isSubmitting}>
|
||||
Create
|
||||
</button>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// The type of props `PopupForm` receives
|
||||
type PopupFormProperties = {
|
||||
defaultDomainId: string;
|
||||
userConfig: UserConfigProperties;
|
||||
setRequestProcessed: ProcessRequestProperties;
|
||||
};
|
||||
|
||||
// Wrap our form with the withFormik HoC
|
||||
const PopupForm = withFormik<PopupFormProperties, PopupFormValuesProperties>({
|
||||
// Transform outer props into default form values
|
||||
mapPropsToValues: ({
|
||||
defaultDomainId,
|
||||
userConfig: { domainOptions },
|
||||
}: PopupFormProperties): PopupFormValuesProperties => {
|
||||
// find default item to select in options menu
|
||||
const defaultItem = domainOptions.find(({ id }) => {
|
||||
return id === defaultDomainId;
|
||||
});
|
||||
|
||||
return {
|
||||
password: '',
|
||||
customurl: '',
|
||||
domain: defaultItem && defaultItem.value ? defaultItem.value.trim() : '', // empty string will map to disabled entry
|
||||
};
|
||||
},
|
||||
|
||||
// Custom sync validation
|
||||
validate: (values: PopupFormValuesProperties): FormikErrors<PopupFormValuesProperties> => {
|
||||
const errors: FormikErrors<PopupFormValuesProperties> = {};
|
||||
// ToDo: Remove special symbols from password & customurl fields
|
||||
|
||||
// password validation
|
||||
if (values.password && values.password.trim().length < 3) {
|
||||
errors.password = 'Password must be atleast 3 characters';
|
||||
}
|
||||
// custom url validation
|
||||
if (values.customurl && values.customurl.trim().length < 3) {
|
||||
errors.customurl = 'Custom URL must be atleast 3 characters';
|
||||
}
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
handleSubmit: async (
|
||||
values: PopupFormValuesProperties,
|
||||
{
|
||||
setSubmitting,
|
||||
props: {
|
||||
setRequestProcessed,
|
||||
userConfig: { apikey },
|
||||
},
|
||||
}: FormikBag<PopupFormProperties, PopupFormValuesProperties>
|
||||
) => {
|
||||
// Get target link to shorten
|
||||
const tabs = await getCurrentTab();
|
||||
const target: string | null = (tabs.length > 0 && tabs[0].url) || null;
|
||||
|
||||
if (!target || !target.startsWith('http')) {
|
||||
// No valid target
|
||||
return setRequestProcessed({ error: true, message: 'Not a valid URL' });
|
||||
}
|
||||
|
||||
const { customurl, password, domain } = values;
|
||||
const apiBody: ApiBodyProperties = {
|
||||
apikey,
|
||||
target,
|
||||
...(customurl.trim() !== '' && { customurl: customurl.trim() }), // add this key only if field is not empty
|
||||
...(password.trim() !== '' && { password: password.trim() }),
|
||||
reuse: false,
|
||||
...(domain.trim() !== '' && { domain: domain.trim() }),
|
||||
};
|
||||
|
||||
// shorten url in the background
|
||||
const response: SuccessfulShortenStatusProperties | ApiErroredProperties = await messageUtil.send(
|
||||
SHORTEN_URL,
|
||||
apiBody
|
||||
);
|
||||
|
||||
// re-enable submit button
|
||||
setSubmitting(false);
|
||||
|
||||
if (!response.error) {
|
||||
const {
|
||||
data: { link },
|
||||
} = response;
|
||||
|
||||
// show shortened url
|
||||
setRequestProcessed({ error: false, message: link });
|
||||
} else {
|
||||
// errored
|
||||
setRequestProcessed({ error: true, message: response.message });
|
||||
}
|
||||
},
|
||||
|
||||
displayName: 'PopupForm',
|
||||
})(InnerForm);
|
||||
|
||||
export default PopupForm;
|
||||
6
src/Popup/index.tsx
Normal file
6
src/Popup/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Popup from './Popup';
|
||||
|
||||
ReactDOM.render(<Popup />, document.getElementById('popup-root'));
|
||||
64
src/Popup/styles.scss
Normal file
64
src/Popup/styles.scss
Normal file
@@ -0,0 +1,64 @@
|
||||
@import "../styles/fonts";
|
||||
@import "../styles/reset";
|
||||
@import "../styles/variables";
|
||||
|
||||
body {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
#popup {
|
||||
min-height: 300px;
|
||||
min-width: 250px;
|
||||
font-size: 1.125em;
|
||||
text-align: center;
|
||||
|
||||
#popup__form {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
div {
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
|
||||
.error {
|
||||
font-size: 11px;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 7px 22px;
|
||||
margin: 15px auto;
|
||||
cursor: pointer;
|
||||
background-color: lightblue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.logo__holder {
|
||||
img {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.action__buttons--holder {
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
border: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 100%;
|
||||
background-color: transparent !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src/api/index.ts
Normal file
6
src/api/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default axios.create({
|
||||
// ToDo: get from local storage
|
||||
baseURL: 'https://kutt.it',
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" data-prefix="far" data-icon="copy" class="svg-inline--fa fa-copy fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#888" d="M433.941 65.941l-51.882-51.882A48 48 0 0 0 348.118 0H176c-26.51 0-48 21.49-48 48v48H48c-26.51 0-48 21.49-48 48v320c0 26.51 21.49 48 48 48h224c26.51 0 48-21.49 48-48v-48h80c26.51 0 48-21.49 48-48V99.882a48 48 0 0 0-14.059-33.941zM266 464H54a6 6 0 0 1-6-6V150a6 6 0 0 1 6-6h74v224c0 26.51 21.49 48 48 48h96v42a6 6 0 0 1-6 6zm128-96H182a6 6 0 0 1-6-6V54a6 6 0 0 1 6-6h106v88c0 13.255 10.745 24 24 24h88v202a6 6 0 0 1-6 6zm6-256h-64V48h9.632c1.591 0 3.117.632 4.243 1.757l48.368 48.368a6 6 0 0 1 1.757 4.243V112z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 710 B |
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="far" data-icon="trash-alt" class="svg-inline--fa fa-trash-alt fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#888" d="M268 416h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12zM432 80h-82.41l-34-56.7A48 48 0 0 0 274.41 0H173.59a48 48 0 0 0-41.16 23.3L98.41 80H16A16 16 0 0 0 0 96v16a16 16 0 0 0 16 16h16v336a48 48 0 0 0 48 48h288a48 48 0 0 0 48-48V128h16a16 16 0 0 0 16-16V96a16 16 0 0 0-16-16zM171.84 50.91A6 6 0 0 1 177 48h94a6 6 0 0 1 5.15 2.91L293.61 80H154.39zM368 464H80V128h288zm-212-48h24a12 12 0 0 0 12-12V188a12 12 0 0 0-12-12h-24a12 12 0 0 0-12 12v216a12 12 0 0 0 12 12z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 731 B |
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="history" class="svg-inline--fa fa-history fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#444" d="M504 255.531c.253 136.64-111.18 248.372-247.82 248.468-59.015.042-113.223-20.53-155.822-54.911-11.077-8.94-11.905-25.541-1.839-35.607l11.267-11.267c8.609-8.609 22.353-9.551 31.891-1.984C173.062 425.135 212.781 440 256 440c101.705 0 184-82.311 184-184 0-101.705-82.311-184-184-184-48.814 0-93.149 18.969-126.068 49.932l50.754 50.754c10.08 10.08 2.941 27.314-11.313 27.314H24c-8.837 0-16-7.163-16-16V38.627c0-14.254 17.234-21.393 27.314-11.314l49.372 49.372C129.209 34.136 189.552 8 256 8c136.81 0 247.747 110.78 248 247.531zm-180.912 78.784l9.823-12.63c8.138-10.463 6.253-25.542-4.21-33.679L288 256.349V152c0-13.255-10.745-24-24-24h-16c-13.255 0-24 10.745-24 24v135.651l65.409 50.874c10.463 8.137 25.541 6.253 33.679-4.21z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 947 B |
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" data-prefix="fas" data-icon="qrcode" class="svg-inline--fa fa-qrcode fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="#888" d="M0 224h192V32H0v192zM64 96h64v64H64V96zm192-64v192h192V32H256zm128 128h-64V96h64v64zM0 480h192V288H0v192zm64-128h64v64H64v-64zm352-64h32v128h-96v-32h-32v96h-64V288h96v32h64v-32zm0 160h32v32h-32v-32zm-64 0h32v32h-32v-32z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 425 B |
@@ -1 +0,0 @@
|
||||
<svg aria-hidden="true" data-prefix="fas" data-icon="cog" class="svg-inline--fa fa-cog fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#444" d="M444.788 291.1l42.616 24.599c4.867 2.809 7.126 8.618 5.459 13.985-11.07 35.642-29.97 67.842-54.689 94.586a12.016 12.016 0 0 1-14.832 2.254l-42.584-24.595a191.577 191.577 0 0 1-60.759 35.13v49.182a12.01 12.01 0 0 1-9.377 11.718c-34.956 7.85-72.499 8.256-109.219.007-5.49-1.233-9.403-6.096-9.403-11.723v-49.184a191.555 191.555 0 0 1-60.759-35.13l-42.584 24.595a12.016 12.016 0 0 1-14.832-2.254c-24.718-26.744-43.619-58.944-54.689-94.586-1.667-5.366.592-11.175 5.459-13.985L67.212 291.1a193.48 193.48 0 0 1 0-70.199l-42.616-24.599c-4.867-2.809-7.126-8.618-5.459-13.985 11.07-35.642 29.97-67.842 54.689-94.586a12.016 12.016 0 0 1 14.832-2.254l42.584 24.595a191.577 191.577 0 0 1 60.759-35.13V25.759a12.01 12.01 0 0 1 9.377-11.718c34.956-7.85 72.499-8.256 109.219-.007 5.49 1.233 9.403 6.096 9.403 11.723v49.184a191.555 191.555 0 0 1 60.759 35.13l42.584-24.595a12.016 12.016 0 0 1 14.832 2.254c24.718 26.744 43.619 58.944 54.689 94.586 1.667 5.366-.592 11.175-5.459 13.985L444.788 220.9a193.485 193.485 0 0 1 0 70.2zM336 256c0-44.112-35.888-80-80-80s-80 35.888-80 80 35.888 80 80 80 80-35.888 80-80z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
18
src/components/BodyWrapper/index.tsx
Normal file
18
src/components/BodyWrapper/index.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
type WrapperProperties = {
|
||||
children: React.ReactChild;
|
||||
};
|
||||
|
||||
const BodyWrapper: React.FC<WrapperProperties> = ({ children }) => {
|
||||
// ToDo: get from props
|
||||
const isLoading = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>{isLoading ? 'Loading...' : children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BodyWrapper;
|
||||
27
src/components/Icon/Icon.tsx
Normal file
27
src/components/Icon/Icon.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
|
||||
import Settings from './Settings';
|
||||
import Spinner from './Spinner';
|
||||
|
||||
const icons = {
|
||||
settings: Settings,
|
||||
spinner: Spinner,
|
||||
};
|
||||
|
||||
export type Icons = keyof typeof icons;
|
||||
|
||||
type Props = {
|
||||
name: Icons;
|
||||
stroke?: string;
|
||||
fill?: string;
|
||||
hoverFill?: string;
|
||||
hoverStroke?: string;
|
||||
strokeWidth?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Icon: React.FC<Props> = ({ name, ...rest }) => {
|
||||
return <div {...rest}>{React.createElement(icons[name])}</div>;
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
17
src/components/Icon/Settings.tsx
Normal file
17
src/components/Icon/Settings.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="-2 -2 24 24"
|
||||
width="32"
|
||||
height="32"
|
||||
preserveAspectRatio="xMinYMin"
|
||||
className="cog_svg__jam cog_svg__jam-cog"
|
||||
>
|
||||
<path d="M20 8.163A2.106 2.106 0 0018.926 10c0 .789.433 1.476 1.074 1.837l-.717 2.406a2.105 2.105 0 00-2.218 3.058l-2.062 1.602A2.104 2.104 0 0011.633 20l-3.29-.008a2.104 2.104 0 00-3.362-1.094l-2.06-1.615A2.105 2.105 0 00.715 14.24L0 11.825A2.106 2.106 0 001.051 10C1.051 9.22.63 8.54 0 8.175L.715 5.76a2.105 2.105 0 002.207-3.043L4.98 1.102A2.104 2.104 0 008.342.008L11.634 0a2.104 2.104 0 003.37 1.097l2.06 1.603a2.105 2.105 0 002.218 3.058L20 8.162zM14.823 3.68c0-.063.002-.125.005-.188l-.08-.062a4.103 4.103 0 01-4.308-1.428l-.904.002a4.1 4.1 0 01-4.29 1.43l-.095.076A4.108 4.108 0 012.279 7.6a4.1 4.1 0 01.772 2.399c0 .882-.28 1.715-.772 2.4a4.108 4.108 0 012.872 4.09l.096.075a4.104 4.104 0 014.289 1.43l.904.002a4.1 4.1 0 014.307-1.428l.08-.062A4.108 4.108 0 0117.7 12.4a4.102 4.102 0 01-.773-2.4c0-.882.281-1.716.773-2.4a4.108 4.108 0 01-2.876-3.919zM10 14a4 4 0 110-8 4 4 0 010 8zm0-2a2 2 0 100-4 2 2 0 000 4z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Settings);
|
||||
34
src/components/Icon/Spinner.tsx
Normal file
34
src/components/Icon/Spinner.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
const Spinner: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
id="spinner"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
className="feather feather-loader"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M12 2L12 6" />
|
||||
<path d="M12 18L12 22" />
|
||||
<path d="M4.93 4.93L7.76 7.76" />
|
||||
<path d="M16.24 16.24L19.07 19.07" />
|
||||
<path d="M2 12L6 12" />
|
||||
<path d="M18 12L22 12" />
|
||||
<path d="M4.93 19.07L7.76 16.24" />
|
||||
<path d="M16.24 7.76L19.07 4.93" />
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Spinner);
|
||||
1
src/components/Icon/index.ts
Normal file
1
src/components/Icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Icon';
|
||||
13
src/components/Icon/styles.scss
Normal file
13
src/components/Icon/styles.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
84
src/components/Input/index.tsx
Normal file
84
src/components/Input/index.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import React from 'react';
|
||||
import { FieldProps } from 'formik';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
type SelectFieldProperties = {
|
||||
options: SelectFieldOptionPropeties[];
|
||||
label: string;
|
||||
};
|
||||
|
||||
type SelectFieldOptionPropeties = {
|
||||
option: string;
|
||||
value: string;
|
||||
disabled?: boolean | undefined;
|
||||
};
|
||||
|
||||
export const SelectField: React.FC<SelectFieldProperties & FieldProps> = ({
|
||||
options,
|
||||
label,
|
||||
field,
|
||||
form: { touched, errors },
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>{label}</label>
|
||||
<div style={{ padding: '0px' }}>
|
||||
<select {...field} {...props}>
|
||||
{options.map(({ option, value, disabled = false }: SelectFieldOptionPropeties, index: number) => {
|
||||
return (
|
||||
<option value={value} disabled={disabled} key={index}>
|
||||
{option}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{touched[field.name] && errors[field.name] && <div className="error">{errors[field.name]}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type TextFieldProperties = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const TextField: React.FC<TextFieldProperties & FieldProps> = ({
|
||||
label,
|
||||
field,
|
||||
form: { touched, errors },
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>{label}</label>
|
||||
<input {...field} {...props} />
|
||||
|
||||
{touched[field.name] && errors[field.name] && <div className="error">{errors[field.name]}</div>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type CheckBoxProperties = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const CheckBox: React.FC<CheckBoxProperties & FieldProps> = ({ label, field, form, ...props }) => {
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={field.name}>{label}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={field.value}
|
||||
onChange={(): void => {
|
||||
form.setFieldValue(field.name, !field.value);
|
||||
}}
|
||||
{...field}
|
||||
{...props}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
24
src/components/Input/styles.scss
Normal file
24
src/components/Input/styles.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
input {
|
||||
height: 35px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
border-radius: 100px;
|
||||
border: 5px solid rgb(245, 245, 245);
|
||||
}
|
||||
|
||||
select {
|
||||
height: 35px;
|
||||
padding: 10px 15px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin: 0px;
|
||||
border-radius: 100px;
|
||||
background-color: rgb(245, 245, 245);
|
||||
|
||||
option {
|
||||
background-color: rgb(245, 245, 245);
|
||||
}
|
||||
}
|
||||
14
src/components/Loader/index.tsx
Normal file
14
src/components/Loader/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import './styles.scss';
|
||||
import Icon from '../Icon';
|
||||
|
||||
const Loader: React.FC = props => {
|
||||
return (
|
||||
<div id="loader" {...props}>
|
||||
<Icon name="spinner" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
10
src/components/Loader/styles.scss
Normal file
10
src/components/Loader/styles.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
#loader {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>History: Kutt</title>
|
||||
<link rel="stylesheet" href="css/history.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="history">
|
||||
<div class="container history__content--holder">
|
||||
<div class="table__content--holder">
|
||||
<div class="history__head--holder">
|
||||
<h2>
|
||||
Recent shortened links. (last 15 results)
|
||||
</h2>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
id="home__button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>Dashboard</a
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
id="rate__button"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>Rate 5 stars</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table__content--wrapper" id="URL_table">
|
||||
<thead class="table__content--head">
|
||||
<tr class="table__head--holder">
|
||||
<th class="table__head--longURL">
|
||||
Original URL
|
||||
</th>
|
||||
<th class="table__head--shortURL">
|
||||
Short URL
|
||||
</th>
|
||||
<th class="table__head--clearAll">
|
||||
<ul class="table__list--clearAll">
|
||||
<li class="table__listItem--clear">
|
||||
<button
|
||||
class="table__clearAll--btn"
|
||||
id="table__clearAll--btn"
|
||||
>
|
||||
Clear All
|
||||
<img
|
||||
class="selectDisable icon__img"
|
||||
src="assets/delete.svg"
|
||||
alt="Clear All"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="table__content--body" id="delegation__element">
|
||||
<!-- Inject children here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="js/history.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -41,7 +41,7 @@ const manifestInput = {
|
||||
|
||||
background: {
|
||||
'__chrome|opera__persistent': false,
|
||||
scripts: ['js/background.js'],
|
||||
scripts: ['js/background.bundle.js'],
|
||||
},
|
||||
|
||||
__chrome__minimum_chrome_version: '49',
|
||||
|
||||
149
src/options.html
149
src/options.html
@@ -1,149 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Options: Kutt</title>
|
||||
<link rel="stylesheet" href="css/options.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="options">
|
||||
<div class="container options__content--holder">
|
||||
<div class="head__content--holder text-center">
|
||||
<img class="head__content--logo" src="assets/logo.png" />
|
||||
<a
|
||||
class="head__content--title"
|
||||
href="https://kutt.it"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>Kutt</a
|
||||
>
|
||||
</div>
|
||||
<div class="form__content--holder">
|
||||
<form class="form__content">
|
||||
<label class="api__key--label"
|
||||
>API Key:
|
||||
<a
|
||||
class="api__label--text"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
href="https://kutt.it/login"
|
||||
>(Get one)
|
||||
<span class="api__label--tooltiptext text-center">
|
||||
Generate key from Kutt.it Website<br />(Settings Page)
|
||||
</span>
|
||||
</a>
|
||||
</label>
|
||||
<input
|
||||
class="api__key--holder"
|
||||
id="api__key--value"
|
||||
type="password"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<div>
|
||||
<label class="password--label"
|
||||
>Set Password
|
||||
<span class="password__label--optional" style="font-size: 16px;"
|
||||
>?
|
||||
<span class="password__label--tooltiptext text-center">
|
||||
Set Password for the Shortened URLs.<br />
|
||||
(20 Char. Max)
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="switch"
|
||||
id="password__label--switch"
|
||||
for="password__label--checkbox"
|
||||
>
|
||||
<input type="checkbox" id="password__label--checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-2 d-none" id="pwd__holder">
|
||||
<input
|
||||
class="password--holder"
|
||||
id="password--value"
|
||||
type="password"
|
||||
maxlength="20"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<span class="view__password--eye" id="view__password--eye">
|
||||
SHOW
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<label class="copy--label">Auto-copy URL to clipboard</label>
|
||||
<label
|
||||
class="switch"
|
||||
id="autocopy__label--switch"
|
||||
for="autocopy__label--checkbox"
|
||||
>
|
||||
<input type="checkbox" id="autocopy__label--checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="copy--label">Keep URL History</label>
|
||||
<label
|
||||
class="switch"
|
||||
id="history__label--switch"
|
||||
for="history__label--checkbox"
|
||||
>
|
||||
<input type="checkbox" id="history__label--checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dev__mode--container">
|
||||
<label class="customhost__mode--label"
|
||||
>Custom Host
|
||||
<span class="customhost__label--optional"
|
||||
>(Advanced)
|
||||
<span class="customhost__label--tooltiptext text-center">
|
||||
Use extension for self - hosted Kutt.<br />
|
||||
Paste the self hosted domain in the field.
|
||||
(eg: https://mykutt.it)
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
class="switch"
|
||||
id="customhost__label--switch"
|
||||
for="customhost__label--checkbox"
|
||||
>
|
||||
<input type="checkbox" id="customhost__label--checkbox" />
|
||||
<div class="slider round"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-2 d-none" id="customhost__holder">
|
||||
<input
|
||||
class="customhost__mode--holder text-center"
|
||||
id="customhost__mode--value"
|
||||
type="text"
|
||||
placeholder="https://mykutt.it"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<button class="button__submit" id="button__submit" type="button">
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="footer__text--holder text-center">
|
||||
Made with ❤️ on
|
||||
<a
|
||||
class="github__repo--link"
|
||||
href="https://github.com/abhijithvijayan/kutt-extension"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>GitHub</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="js/options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,94 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Kutt</title>
|
||||
<link rel="stylesheet" href="css/popup.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section id="home">
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<div class="main__logo--holder">
|
||||
<img
|
||||
style="width: 22px; height: 22px;"
|
||||
class="main__logo selectDisable"
|
||||
src="assets/logo.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="main__list--holder">
|
||||
<a
|
||||
class="list__button"
|
||||
href="options.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
title="Options"
|
||||
>
|
||||
<img
|
||||
style="width: 16px; height: 16px;"
|
||||
class="button__icon selectDisable"
|
||||
src="assets/settings.svg"
|
||||
/></a>
|
||||
|
||||
<a
|
||||
class="list__button"
|
||||
href="history.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
title="History"
|
||||
>
|
||||
<img
|
||||
style="width: 16px; height: 16px;"
|
||||
class="button__icon selectDisable"
|
||||
src="assets/history.svg"
|
||||
/></a>
|
||||
</div>
|
||||
</header>
|
||||
<div class="content__holder">
|
||||
<div class="url__content--holder text-center">
|
||||
<div class="url__content--url">
|
||||
<div class="v-none" id="copy__alert">Copied to clipboard!</div>
|
||||
<h4 id="url__content-inner">Shortening...</h4>
|
||||
</div>
|
||||
<ul class="buttons__content--holder d-none">
|
||||
<li
|
||||
class="copy__content--holder"
|
||||
id="button__copy--holder"
|
||||
title="Copy"
|
||||
>
|
||||
<img
|
||||
style="width: 13px; height: 13px;"
|
||||
id="button__copy"
|
||||
class="selectDisable"
|
||||
src="assets/copy.svg"
|
||||
alt="copy"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="qrbtn__content--holder"
|
||||
id="button__qrcode--holder"
|
||||
title="QR code"
|
||||
>
|
||||
<img
|
||||
style="width: 13px; height: 13px;"
|
||||
id="button__qrcode"
|
||||
class="selectDisable"
|
||||
src="assets/qrcode.svg"
|
||||
alt="QR Code"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="qrcode__content--holder selectDisable text-center d-none">
|
||||
<img id="qr_code" src="#" alt="QRCode" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
import { KUTT_IT_DEFAULT_DOMAIN } from './constants';
|
||||
|
||||
// Shorten url
|
||||
const shortenUrl = async (API_KEY, urlToShorten, password) => {
|
||||
let API_HOST = KUTT_IT_DEFAULT_DOMAIN;
|
||||
|
||||
try {
|
||||
const { host, userOptions } = await browser.storage.local.get(['host', 'userOptions']);
|
||||
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
if (userOptions.hasOwnProperty('devMode') && userOptions.devMode) {
|
||||
API_HOST = host;
|
||||
}
|
||||
// else use default host
|
||||
} catch (err) {
|
||||
API_HOST = KUTT_IT_DEFAULT_DOMAIN;
|
||||
}
|
||||
|
||||
// shorten function
|
||||
try {
|
||||
const {
|
||||
data: { shortUrl },
|
||||
} = await axios({
|
||||
method: 'POST',
|
||||
timeout: 20000,
|
||||
url: `${API_HOST}/api/url/submit`,
|
||||
headers: {
|
||||
'X-API-Key': API_KEY,
|
||||
},
|
||||
data: {
|
||||
target: urlToShorten,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
return shortUrl;
|
||||
} catch (e) {
|
||||
// time out
|
||||
if (e.code === 'ECONNABORTED') {
|
||||
return 504;
|
||||
}
|
||||
|
||||
// return status code
|
||||
if (e.response) {
|
||||
return e.response.status;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Calling function
|
||||
browser.runtime.onMessage.addListener(async (request, sender, response) => {
|
||||
// shorten request
|
||||
if (request.msg === 'shorten') {
|
||||
const { API_KEY, pageUrl, password } = request;
|
||||
|
||||
return shortenUrl(API_KEY, pageUrl, password);
|
||||
}
|
||||
|
||||
// store urls to history
|
||||
if (request.msg === 'store') {
|
||||
const { curURLCollection } = request;
|
||||
const { curURLPair } = request;
|
||||
|
||||
// find & remove duplicates
|
||||
const noDuplicateArray = curURLCollection.filter(el => {
|
||||
return el.longUrl !== curURLPair.longUrl;
|
||||
});
|
||||
|
||||
const count = noDuplicateArray.length;
|
||||
|
||||
// delete first pair if size exceeds 15
|
||||
if (count >= 15) {
|
||||
noDuplicateArray.shift();
|
||||
}
|
||||
|
||||
// push to the array
|
||||
noDuplicateArray.push(curURLPair);
|
||||
|
||||
// save to local storage
|
||||
await browser.storage.local.set({
|
||||
URL_array: noDuplicateArray,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
/* eslint-disable no-multi-assign */
|
||||
const $ = document.querySelector.bind(document);
|
||||
const $$ = document.querySelectorAll.bind(document);
|
||||
|
||||
Node.prototype.on = window.on = function(name, fn) {
|
||||
this.addEventListener(name, fn);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-proto
|
||||
NodeList.prototype.__proto__ = Array.prototype;
|
||||
|
||||
NodeList.prototype.on = NodeList.prototype.addEventListener = function(name, fn) {
|
||||
this.forEach(function(elem, i) {
|
||||
elem.on(name, fn);
|
||||
});
|
||||
};
|
||||
|
||||
export { $, $$ };
|
||||
@@ -1,87 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export const KUTT_IT_DEFAULT_DOMAIN = 'https://kutt.it';
|
||||
export const FIREFOX = 'firefox';
|
||||
export const OPERA = 'opera';
|
||||
export const CHROME = 'chrome';
|
||||
|
||||
// popup page
|
||||
export const QRCODE_IMAGE_NODE = '#qr_code';
|
||||
export const URL_HOLDER = '#url__content-inner';
|
||||
export const BUTTONS_GROUP = '.buttons__content--holder';
|
||||
export const COPY_BUTTON = '#button__copy--holder';
|
||||
export const COPIED_ALERT_HOLDER = '#copy__alert';
|
||||
export const QRCODE_HOLDER = '.qrcode__content--holder';
|
||||
export const QRCODE_BUTTON = '#button__qrcode--holder';
|
||||
export const QR_EXTERNAL_API_URL = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=';
|
||||
|
||||
// options page
|
||||
export const PASSWORD_HOLDER = '#pwd__holder';
|
||||
export const CUSTOM_HOST_URL_HOLDER = '#customhost__holder';
|
||||
export const SAVE_BUTTON = '#button__submit';
|
||||
export const PASSWORD_INPUT = '#password--value';
|
||||
export const CUSTOM_HOST_URL_INPUT = '#customhost__mode--value';
|
||||
export const API_KEY_INPUT = '#api__key--value';
|
||||
export const PASSWORD_VIEW_TOGGLER = '#view__password--eye';
|
||||
export const PASSWORD_INPUT_TOGGLE_SWITCH = '#password__label--switch';
|
||||
export const CUSTOM_HOST_INPUT_TOGGLE_SWITCH = '#customhost__label--switch';
|
||||
export const PASSWORD_OPTION_CHECKBOX = '#password__label--checkbox';
|
||||
export const CUSTOM_HOST_OPTION_CHECKBOX = '#customhost__label--checkbox';
|
||||
export const SAVE_HISTORY_OPTION_CHECKBOX = '#history__label--checkbox';
|
||||
export const AUTOCOPY_OPTION_CHECKBOX = '#autocopy__label--checkbox';
|
||||
|
||||
// history page
|
||||
export const CLEAR_HISTORY_BUTTON = '#table__clearAll--btn';
|
||||
export const ALERT_COPIED_HOLDER = '#flash_copy';
|
||||
export const HISTORY_VIEW_TABLE = '.table__content--holder';
|
||||
export const RATE_NOW_BUTTON = '#rate__button';
|
||||
export const DASHBOARD_BUTTON = '#home__button';
|
||||
export const HISTORY_VIEW_TABLE_PARENT_NODE = '#delegation__element';
|
||||
export const CHROME_STORE_LINK =
|
||||
'https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd/reviews';
|
||||
export const FIREFOX_STORE_LINK = 'https://addons.mozilla.org/en-US/firefox/addon/kutt/reviews/';
|
||||
|
||||
export const COPY_BUTTON_ID = 'copy';
|
||||
export const QRCODE_BUTTON_ID = 'qrcode';
|
||||
export const QRCODE_POPUP_CLOSE_BUTTON_ID = 'close__btn';
|
||||
|
||||
export const NO_URLS_TO_SHOW = '<h2 class="py-2 table-inner">No Shortened URLs</h2>';
|
||||
export const COPIED_TO_CLIPBOARD = '<div class="table_body--flashCopy" id="flash_copy">Copied to clipboard!</div>';
|
||||
export const FAILED_TO_COPY = '<div class="table_body--flashCopy" id="flash_copy">Error while Copying!!</div>';
|
||||
|
||||
export const QRCODE_POPUP_NODE = '#qrcode__template';
|
||||
export const QRCODE_POPUP_NODE_TEMPLATE = `
|
||||
<div class="table__qrcodePopup--div" id="qrcode__template">
|
||||
<div class="table__qrcode--popup">
|
||||
<div class="table__qrcode--holder">
|
||||
<img id="table__qrcode" src="%qrcodeLink%" alt="QRCode" />
|
||||
</div>
|
||||
<div class="table__closebtn--holder">
|
||||
<button type="button" class="table__closebtn--inner" id="close__btn-%num%">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export const HISTORY_TABLE_ITEM_HTML = `
|
||||
<tr class="table__body--holder" id="table__body-%num%">
|
||||
<td class="table__body--original">
|
||||
<a href="%longLink%" class="table__body--originalURL" target="_blank" rel="noopener noreferrer nofollow">%longLink%</a>
|
||||
</td>
|
||||
<td class="table__body--shortened" id="table__shortened-%num%">
|
||||
<div class="table__body--shortenBody">
|
||||
<a href="%shortLink%" id="shortUrl-%num%" class="table__body--shortenURL" target="_blank" rel="noopener noreferrer nofollow">%shortLink%</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="table__body--functionBtns">
|
||||
<div class="table__body--btnHolder" id="btns-%num%">
|
||||
<button type="button" class="table__body--copy" id="copy-%num%" title="Copy">
|
||||
<img class="selectDisable icon__img" src="assets/copy.svg" alt="copy" />
|
||||
</button>
|
||||
<button type="button" class="table__body--qrcode" id="qrcode-%num%" title="QR Code">
|
||||
<img class="selectDisable icon__img" src="assets/qrcode.svg" alt="QR Code" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
@@ -1,237 +0,0 @@
|
||||
/* eslint-disable prefer-destructuring */
|
||||
/* eslint-disable camelcase */
|
||||
import browser from 'webextension-polyfill';
|
||||
import qr from 'qrcode';
|
||||
|
||||
import {
|
||||
HISTORY_TABLE_ITEM_HTML,
|
||||
CLEAR_HISTORY_BUTTON,
|
||||
HISTORY_VIEW_TABLE,
|
||||
RATE_NOW_BUTTON,
|
||||
DASHBOARD_BUTTON,
|
||||
HISTORY_VIEW_TABLE_PARENT_NODE,
|
||||
CHROME_STORE_LINK,
|
||||
FIREFOX_STORE_LINK,
|
||||
QR_EXTERNAL_API_URL,
|
||||
KUTT_IT_DEFAULT_DOMAIN,
|
||||
FIREFOX,
|
||||
OPERA,
|
||||
CHROME,
|
||||
ALERT_COPIED_HOLDER,
|
||||
NO_URLS_TO_SHOW,
|
||||
COPIED_TO_CLIPBOARD,
|
||||
FAILED_TO_COPY,
|
||||
QRCODE_POPUP_NODE_TEMPLATE,
|
||||
QRCODE_POPUP_NODE,
|
||||
COPY_BUTTON_ID,
|
||||
QRCODE_BUTTON_ID,
|
||||
QRCODE_POPUP_CLOSE_BUTTON_ID,
|
||||
} from './constants';
|
||||
import { $ } from './bling';
|
||||
|
||||
/**
|
||||
* Identify Browser
|
||||
*/
|
||||
const getBrowserInfo = () => {
|
||||
// Chrome 1+
|
||||
const isChrome = !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
|
||||
|
||||
// Firefox 1.0+
|
||||
const isFirefox = typeof InstallTrigger !== 'undefined';
|
||||
|
||||
// Opera 8.0+
|
||||
// eslint-disable-next-line no-undef
|
||||
const isOpera = (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||
|
||||
if (isFirefox) {
|
||||
return FIREFOX;
|
||||
}
|
||||
|
||||
if (isOpera) {
|
||||
return OPERA;
|
||||
}
|
||||
|
||||
return CHROME;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Store Link
|
||||
*/
|
||||
const updateRatingButton = () => {
|
||||
const browserName = getBrowserInfo();
|
||||
|
||||
switch (browserName) {
|
||||
case CHROME:
|
||||
case OPERA: {
|
||||
$(RATE_NOW_BUTTON).setAttribute('href', CHROME_STORE_LINK);
|
||||
break;
|
||||
}
|
||||
|
||||
case FIREFOX: {
|
||||
$(RATE_NOW_BUTTON).setAttribute('href', FIREFOX_STORE_LINK);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update Home Page URL
|
||||
*/
|
||||
|
||||
document.on('DOMContentLoaded', async () => {
|
||||
let updatedHTML;
|
||||
|
||||
const {
|
||||
userOptions: { keepHistory, devMode },
|
||||
URL_array,
|
||||
host,
|
||||
} = await browser.storage.local.get(['userOptions', 'URL_array', 'host']);
|
||||
|
||||
if (keepHistory) {
|
||||
const count = URL_array.length;
|
||||
|
||||
// update DOM
|
||||
if (count > 0) {
|
||||
let pass = 0;
|
||||
|
||||
for (const el of URL_array) {
|
||||
// Regular Expression Based Implementation
|
||||
updatedHTML = HISTORY_TABLE_ITEM_HTML.replace(/%longLink%/g, el.longUrl);
|
||||
pass += 1;
|
||||
updatedHTML = updatedHTML.replace(/%num%/g, pass);
|
||||
updatedHTML = updatedHTML.replace(/%shortLink%/g, el.shortUrl);
|
||||
// inject to DOM
|
||||
$(HISTORY_VIEW_TABLE_PARENT_NODE).insertAdjacentHTML('afterbegin', updatedHTML);
|
||||
}
|
||||
} else {
|
||||
$(CLEAR_HISTORY_BUTTON).style.display = 'none';
|
||||
$(HISTORY_VIEW_TABLE_PARENT_NODE).insertAdjacentHTML('afterbegin', NO_URLS_TO_SHOW);
|
||||
}
|
||||
|
||||
// update review link
|
||||
updateRatingButton();
|
||||
|
||||
// update home page url
|
||||
const hostHomeUrl = devMode ? host : KUTT_IT_DEFAULT_DOMAIN;
|
||||
$(DASHBOARD_BUTTON).setAttribute('href', hostHomeUrl);
|
||||
} else {
|
||||
alert('Enable History from Options Page');
|
||||
|
||||
// open options page in new tab
|
||||
browser.runtime.openOptionsPage();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Clear all history
|
||||
*/
|
||||
$(CLEAR_HISTORY_BUTTON).on('click', async () => {
|
||||
await browser.storage.local.set({
|
||||
URL_array: [],
|
||||
});
|
||||
|
||||
$(HISTORY_VIEW_TABLE_PARENT_NODE).parentNode.removeChild($(HISTORY_VIEW_TABLE_PARENT_NODE));
|
||||
$(CLEAR_HISTORY_BUTTON).style.display = 'none';
|
||||
$(HISTORY_VIEW_TABLE).insertAdjacentHTML('beforeend', NO_URLS_TO_SHOW);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle Buttons Click Actions
|
||||
*/
|
||||
const buttonAction = async (type, id) => {
|
||||
const flashCopyAlert = flashHTML => {
|
||||
$(`#table__shortened-${id}`).insertAdjacentHTML('afterbegin', flashHTML);
|
||||
|
||||
setTimeout(() => {
|
||||
$(ALERT_COPIED_HOLDER).parentNode.removeChild($(ALERT_COPIED_HOLDER));
|
||||
}, 1300);
|
||||
};
|
||||
|
||||
// copy button
|
||||
if (type === COPY_BUTTON_ID) {
|
||||
const shortLink = $(`#shortUrl-${id}`).textContent;
|
||||
|
||||
try {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = shortLink;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
|
||||
flashCopyAlert(COPIED_TO_CLIPBOARD);
|
||||
} catch (error) {
|
||||
flashCopyAlert(FAILED_TO_COPY);
|
||||
}
|
||||
}
|
||||
// generate QRCode
|
||||
else if (type === QRCODE_BUTTON_ID) {
|
||||
let updatedHTML;
|
||||
|
||||
// 1. get short link
|
||||
const shortUrl = $(`#shortUrl-${id}`).textContent;
|
||||
|
||||
// 2. generate qrcode
|
||||
try {
|
||||
const qrcodeURL = await qr.toDataURL(shortUrl);
|
||||
|
||||
// 3. display popup menu with link
|
||||
updatedHTML = QRCODE_POPUP_NODE_TEMPLATE.replace('%qrcodeLink%', qrcodeURL);
|
||||
updatedHTML = updatedHTML.replace('%num%', id);
|
||||
|
||||
$(`#btns-${id}`).insertAdjacentHTML('afterend', updatedHTML);
|
||||
} catch (err) {
|
||||
// fetch qrcode from http://goqr.me
|
||||
updatedHTML = QRCODE_POPUP_NODE_TEMPLATE.replace('%qrcodeLink%', `${QR_EXTERNAL_API_URL}${shortUrl}`);
|
||||
$(`#btns-${id}`).insertAdjacentHTML('afterend', updatedHTML);
|
||||
}
|
||||
} else if (type === QRCODE_POPUP_CLOSE_BUTTON_ID) {
|
||||
$(QRCODE_POPUP_NODE).parentNode.removeChild($(QRCODE_POPUP_NODE));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* get the delegation id (child node)
|
||||
*/
|
||||
const getButtonDetails = e => {
|
||||
let splitId;
|
||||
let type;
|
||||
let id;
|
||||
const eventId = e.target.id;
|
||||
|
||||
if (eventId) {
|
||||
splitId = eventId.split('-');
|
||||
type = splitId[0];
|
||||
id = parseInt(splitId[1]);
|
||||
// perform action
|
||||
buttonAction(type, id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Button Action (qrcode / copy)
|
||||
*/
|
||||
$(HISTORY_VIEW_TABLE_PARENT_NODE).on('click', getButtonDetails);
|
||||
|
||||
/**
|
||||
* prevent enter key press
|
||||
*/
|
||||
document.on('keypress', e => {
|
||||
const keyCode = e.which || e.keyCode;
|
||||
|
||||
if (keyCode === 13) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
@@ -1,173 +0,0 @@
|
||||
/* eslint-disable no-use-before-define */
|
||||
/* eslint-disable camelcase */
|
||||
import browser from 'webextension-polyfill';
|
||||
|
||||
import {
|
||||
PASSWORD_HOLDER,
|
||||
CUSTOM_HOST_URL_HOLDER,
|
||||
SAVE_BUTTON,
|
||||
PASSWORD_INPUT,
|
||||
CUSTOM_HOST_URL_INPUT,
|
||||
API_KEY_INPUT,
|
||||
PASSWORD_VIEW_TOGGLER,
|
||||
PASSWORD_INPUT_TOGGLE_SWITCH,
|
||||
CUSTOM_HOST_INPUT_TOGGLE_SWITCH,
|
||||
PASSWORD_OPTION_CHECKBOX,
|
||||
CUSTOM_HOST_OPTION_CHECKBOX,
|
||||
SAVE_HISTORY_OPTION_CHECKBOX,
|
||||
AUTOCOPY_OPTION_CHECKBOX,
|
||||
} from './constants';
|
||||
import { $ } from './bling';
|
||||
|
||||
document.on('DOMContentLoaded', async () => {
|
||||
// get values from localstorage
|
||||
let { key, pwd, userOptions, host } = await browser.storage.local.get(['key', 'pwd', 'userOptions', 'host']);
|
||||
|
||||
// don't use toString() as it will fail for `undefined`
|
||||
const API_KEY = `${key}`;
|
||||
|
||||
if (API_KEY === 'undefined') {
|
||||
$(API_KEY_INPUT).value = '';
|
||||
} else {
|
||||
$(API_KEY_INPUT).value = API_KEY;
|
||||
|
||||
// password holder
|
||||
$(PASSWORD_OPTION_CHECKBOX).checked = userOptions.pwdForUrls;
|
||||
|
||||
// if disabled -> delete saved password
|
||||
if (!userOptions.pwdForUrls) {
|
||||
pwd = '';
|
||||
}
|
||||
|
||||
$(PASSWORD_INPUT).value = pwd;
|
||||
toggleInputVisibility(userOptions.pwdForUrls, PASSWORD_HOLDER);
|
||||
|
||||
// dev mode holder
|
||||
$(CUSTOM_HOST_OPTION_CHECKBOX).checked = userOptions.devMode;
|
||||
|
||||
// if disabled -> reset to default host
|
||||
if (!userOptions.devMode) {
|
||||
host = '';
|
||||
}
|
||||
|
||||
$(CUSTOM_HOST_URL_INPUT).value = host;
|
||||
toggleInputVisibility(userOptions.devMode, CUSTOM_HOST_URL_HOLDER);
|
||||
}
|
||||
|
||||
$(AUTOCOPY_OPTION_CHECKBOX).checked = userOptions.autoCopy;
|
||||
$(SAVE_HISTORY_OPTION_CHECKBOX).checked = userOptions.keepHistory;
|
||||
});
|
||||
|
||||
// Store Data and Alert message
|
||||
const saveData = async () => {
|
||||
let password = $(PASSWORD_INPUT).value;
|
||||
let API_HOST = $(CUSTOM_HOST_URL_INPUT).value;
|
||||
const API_KEY = $(API_KEY_INPUT).value;
|
||||
|
||||
let devMode = $(CUSTOM_HOST_OPTION_CHECKBOX).checked;
|
||||
let pwdForUrls = $(PASSWORD_OPTION_CHECKBOX).checked;
|
||||
const autoCopy = $(AUTOCOPY_OPTION_CHECKBOX).checked;
|
||||
const keepHistory = $(SAVE_HISTORY_OPTION_CHECKBOX).checked;
|
||||
|
||||
if (password === '') {
|
||||
pwdForUrls = false;
|
||||
}
|
||||
|
||||
if (!pwdForUrls) {
|
||||
password = '';
|
||||
}
|
||||
|
||||
if (API_HOST === '') {
|
||||
devMode = false;
|
||||
} else if (API_HOST.endsWith('/')) {
|
||||
API_HOST = API_HOST.slice(0, -1);
|
||||
}
|
||||
|
||||
if (!devMode) {
|
||||
API_HOST = '';
|
||||
}
|
||||
|
||||
const userOptions = {
|
||||
pwdForUrls,
|
||||
autoCopy,
|
||||
devMode,
|
||||
keepHistory,
|
||||
};
|
||||
|
||||
// store value locally
|
||||
await browser.storage.local.set({
|
||||
key: API_KEY,
|
||||
pwd: password,
|
||||
host: API_HOST,
|
||||
URL_array: [],
|
||||
userOptions,
|
||||
});
|
||||
|
||||
$(SAVE_BUTTON).textContent = 'Saved';
|
||||
|
||||
setTimeout(async () => {
|
||||
$(SAVE_BUTTON).textContent = 'Save';
|
||||
|
||||
// close current tab
|
||||
const tabInfo = await browser.tabs.getCurrent();
|
||||
browser.tabs.remove(tabInfo.id);
|
||||
}, 1250);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle submit button click
|
||||
*/
|
||||
$(SAVE_BUTTON).on('click', saveData);
|
||||
|
||||
/**
|
||||
* Handle enter-key press
|
||||
*/
|
||||
document.on('keypress', e => {
|
||||
if (e.keyCode === 13) {
|
||||
saveData();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle Password View
|
||||
*/
|
||||
$(PASSWORD_VIEW_TOGGLER).on('click', () => {
|
||||
const element = $(PASSWORD_INPUT);
|
||||
|
||||
if (element.type === 'password') {
|
||||
element.type = 'text';
|
||||
$(PASSWORD_VIEW_TOGGLER).textContent = 'HIDE';
|
||||
} else {
|
||||
element.type = 'password';
|
||||
$(PASSWORD_VIEW_TOGGLER).textContent = 'SHOW';
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggle Element Visibility
|
||||
*/
|
||||
function toggleInputVisibility(checked, el) {
|
||||
if (checked) {
|
||||
$(el).classList.remove('d-none');
|
||||
} else {
|
||||
$(el).classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Password Enable/Disable Switch
|
||||
*/
|
||||
$(PASSWORD_INPUT_TOGGLE_SWITCH).on('click', () => {
|
||||
const { checked } = $(PASSWORD_OPTION_CHECKBOX);
|
||||
|
||||
toggleInputVisibility(checked, PASSWORD_HOLDER);
|
||||
});
|
||||
|
||||
/**
|
||||
* Customhost Mode Enable/Disable Switch
|
||||
*/
|
||||
$(CUSTOM_HOST_INPUT_TOGGLE_SWITCH).on('click', () => {
|
||||
const { checked } = $(CUSTOM_HOST_OPTION_CHECKBOX);
|
||||
|
||||
toggleInputVisibility(checked, CUSTOM_HOST_URL_HOLDER);
|
||||
});
|
||||
@@ -1,252 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
import browser from 'webextension-polyfill';
|
||||
import qr from 'qrcode';
|
||||
|
||||
import {
|
||||
QRCODE_IMAGE_NODE,
|
||||
URL_HOLDER,
|
||||
BUTTONS_GROUP,
|
||||
COPY_BUTTON,
|
||||
COPIED_ALERT_HOLDER,
|
||||
QRCODE_BUTTON,
|
||||
QR_EXTERNAL_API_URL,
|
||||
QRCODE_HOLDER,
|
||||
} from './constants';
|
||||
import { $ } from './bling';
|
||||
|
||||
let shortUrl;
|
||||
let longUrl;
|
||||
let API_KEY;
|
||||
let password;
|
||||
let validUrl = '';
|
||||
|
||||
/**
|
||||
* DOM Message Update function
|
||||
*/
|
||||
const updateDOMContent = value => {
|
||||
$(URL_HOLDER).textContent = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Trigger Opening Options Page
|
||||
*/
|
||||
const openOptionsPage = () => {
|
||||
setTimeout(() => {
|
||||
browser.runtime.openOptionsPage();
|
||||
}, 900);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show / Hide Components
|
||||
*
|
||||
* @param {String} element ID or class
|
||||
*/
|
||||
const toggleContentVisibility = element => {
|
||||
$(element).classList.toggle('d-none');
|
||||
};
|
||||
|
||||
/**
|
||||
* Show / Hide copied text alert
|
||||
*/
|
||||
const toggleCopyAlert = () => {
|
||||
$(COPIED_ALERT_HOLDER).classList.toggle('v-none');
|
||||
};
|
||||
|
||||
/**
|
||||
* QR Code generator
|
||||
*
|
||||
* @param {String} url
|
||||
*/
|
||||
const generateQRCode = async sourceUrl => {
|
||||
try {
|
||||
$(QRCODE_IMAGE_NODE).src = await qr.toDataURL(sourceUrl);
|
||||
} catch (err) {
|
||||
// fetch qrcode from http://goqr.me api
|
||||
$(QRCODE_IMAGE_NODE).src = `${QR_EXTERNAL_API_URL}${sourceUrl}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy Link to Clipboard
|
||||
*/
|
||||
const copyLinkToClipboard = () => {
|
||||
// https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
|
||||
try {
|
||||
$(COPIED_ALERT_HOLDER).textContent = 'Copied to clipboard!';
|
||||
const el = document.createElement('textarea');
|
||||
el.value = shortUrl;
|
||||
el.setAttribute('readonly', '');
|
||||
el.style.position = 'absolute';
|
||||
el.style.left = '-9999px';
|
||||
document.body.appendChild(el);
|
||||
const selected = document.getSelection().rangeCount > 0 ? document.getSelection().getRangeAt(0) : false;
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
|
||||
if (selected) {
|
||||
document.getSelection().removeAllRanges();
|
||||
document.getSelection().addRange(selected);
|
||||
}
|
||||
|
||||
toggleCopyAlert();
|
||||
setTimeout(() => {
|
||||
toggleCopyAlert();
|
||||
}, 1300);
|
||||
} catch (error) {
|
||||
$(COPIED_ALERT_HOLDER).textContent = 'Error while Copying!';
|
||||
|
||||
toggleCopyAlert();
|
||||
setTimeout(() => {
|
||||
toggleCopyAlert();
|
||||
}, 1300);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Add URL to localstorage array
|
||||
*/
|
||||
const addToHistory = async curURLPair => {
|
||||
const { URL_array } = await browser.storage.local.get(['URL_array']);
|
||||
|
||||
// store to localstorage
|
||||
await browser.runtime.sendMessage({
|
||||
msg: 'store',
|
||||
curURLPair,
|
||||
curURLCollection: URL_array,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle User Preferred Actions (autoCopy/keepHistory)
|
||||
*/
|
||||
const doUserSetActions = async () => {
|
||||
const { userOptions } = await browser.storage.local.get(['userOptions']);
|
||||
const { keepHistory, autoCopy } = userOptions;
|
||||
|
||||
if (autoCopy) {
|
||||
setTimeout(() => {
|
||||
copyLinkToClipboard();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (keepHistory) {
|
||||
const curURLPair = {
|
||||
longUrl,
|
||||
shortUrl,
|
||||
};
|
||||
|
||||
addToHistory(curURLPair);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle copying on button click
|
||||
*/
|
||||
$(COPY_BUTTON).on('click', () => {
|
||||
return copyLinkToClipboard();
|
||||
});
|
||||
|
||||
/**
|
||||
* Show / Hide QRCode on button click
|
||||
*/
|
||||
$(QRCODE_BUTTON).on('click', () => {
|
||||
toggleContentVisibility(QRCODE_HOLDER);
|
||||
});
|
||||
|
||||
/**
|
||||
* Driver Function
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const tabs = await browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
|
||||
// extract page url
|
||||
longUrl = tabs.length && tabs[0].url;
|
||||
|
||||
// validate url
|
||||
if (longUrl) {
|
||||
validUrl = longUrl.startsWith('http');
|
||||
}
|
||||
|
||||
// Get API Key / Password from localstorage
|
||||
const { key, pwd } = await browser.storage.local.get(['key', 'pwd']);
|
||||
|
||||
API_KEY = key;
|
||||
password = pwd;
|
||||
|
||||
if (validUrl && API_KEY !== '' && API_KEY !== undefined) {
|
||||
/**
|
||||
* Initialize url shortening (send message to background.js)
|
||||
*/
|
||||
const response = await browser.runtime.sendMessage({
|
||||
msg: 'shorten',
|
||||
API_KEY,
|
||||
pageUrl: longUrl,
|
||||
password,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
if (!isNaN(response)) {
|
||||
// status codes
|
||||
switch (response) {
|
||||
case 429:
|
||||
updateDOMContent('API Limit Exceeded!');
|
||||
break;
|
||||
case 401:
|
||||
updateDOMContent('Invalid API Key');
|
||||
openOptionsPage();
|
||||
break;
|
||||
case 504:
|
||||
updateDOMContent('Time-out!');
|
||||
break;
|
||||
default:
|
||||
updateDOMContent('Some error occured');
|
||||
break;
|
||||
}
|
||||
}
|
||||
// got valid response
|
||||
else if (response) {
|
||||
shortUrl = response;
|
||||
// show shortened kutt url
|
||||
updateDOMContent(shortUrl);
|
||||
// Show action buttons
|
||||
toggleContentVisibility(BUTTONS_GROUP);
|
||||
// Generate QR Code
|
||||
generateQRCode(shortUrl);
|
||||
// perform user-set actions
|
||||
doUserSetActions();
|
||||
}
|
||||
// all test-cases fail
|
||||
else {
|
||||
updateDOMContent('Invalid Response!');
|
||||
}
|
||||
}
|
||||
// no API key set
|
||||
else if (API_KEY === '' || API_KEY === undefined) {
|
||||
updateDOMContent('Set API Key in Options!');
|
||||
|
||||
const defaultOptions = {
|
||||
pwdForUrls: false,
|
||||
autoCopy: false,
|
||||
keepHistory: true,
|
||||
devMode: false,
|
||||
};
|
||||
|
||||
// set defaults
|
||||
await browser.storage.local.set({
|
||||
userOptions: defaultOptions,
|
||||
URL_array: [],
|
||||
});
|
||||
|
||||
openOptionsPage();
|
||||
}
|
||||
// invalid url
|
||||
else if (!validUrl) {
|
||||
updateDOMContent('Not a Valid URL!!');
|
||||
} else {
|
||||
updateDOMContent('Some error occured');
|
||||
}
|
||||
});
|
||||
0
src/styles/_fonts.scss
Normal file
0
src/styles/_fonts.scss
Normal file
43
src/styles/_reset.scss
Normal file
43
src/styles/_reset.scss
Normal file
@@ -0,0 +1,43 @@
|
||||
// forked from Normalize.css, Reboot.css, Sanitize.css, and Untouched.css
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input {
|
||||
word-spacing: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
text-rendering: auto;
|
||||
cursor: text;
|
||||
margin: 0em;
|
||||
padding: 1px 0px;
|
||||
border-width: 2px;
|
||||
}
|
||||
24
src/styles/_variables.scss
Normal file
24
src/styles/_variables.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
// **** colors ****
|
||||
$black: #111111;
|
||||
$light-black: #0f0f0f;
|
||||
$grey-white: #f3f3f3;
|
||||
$white: #ffffff;
|
||||
|
||||
// **** fonts ****
|
||||
$font-nunito: "Nunito", sans-serif;
|
||||
|
||||
// font weights
|
||||
$thin: 100;
|
||||
$exlight: 200;
|
||||
$light: 300;
|
||||
$regular: 400;
|
||||
$medium: 500;
|
||||
$semibold: 600;
|
||||
$bold: 700;
|
||||
$exbold: 800;
|
||||
$exblack: 900;
|
||||
|
||||
// **** other variables ****
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito:400,600");
|
||||
@@ -1,31 +0,0 @@
|
||||
// Manually forked from Normalize.css, Reboot.css, Sanitize.css, and Untouched.css
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
*:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
a:link {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
// Colors
|
||||
$color-black: #111111;
|
||||
$color-light-black: #0f0f0f;
|
||||
$color-shadow-black: rgba(50, 50, 50, 0.1);
|
||||
$color-shadow-light: rgba(100, 100, 100, 0.1);
|
||||
$color-null-black: rgba(0, 0, 0, 0);
|
||||
$color-grey-white: #f3f3f3;
|
||||
$color-white: #ffffff;
|
||||
$color-light: #fcfcfc;
|
||||
$color-white-less: #cccccc;
|
||||
$color-border-white: #f5f5f5;
|
||||
$color-less-white: #dedede;
|
||||
$color-light-grey: #555;
|
||||
$color-medium-grey: #444444;
|
||||
$color-dark-grey: #333;
|
||||
$color-light-azure: rgba(66, 165, 245, 0.5);
|
||||
$color-light-blue: #4d5bfa;
|
||||
$color-grad-light-blue: #42a5f5;
|
||||
$color-grad-dark-blue: #2979ff;
|
||||
|
||||
$font-nunito: "Nunito", sans-serif;
|
||||
|
||||
$regular: 400;
|
||||
$bold: 600;
|
||||
|
||||
.d-none {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.v-none {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.my-2 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding: 1em 24px;
|
||||
}
|
||||
|
||||
.table-inner {
|
||||
border-radius: 0 0 12px 12px;
|
||||
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.selectDisable {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: $font-nunito;
|
||||
}
|
||||
|
||||
.icon__img {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* toggle switch */
|
||||
.switch {
|
||||
height: 23px;
|
||||
width: 49px;
|
||||
margin-right: 10px;
|
||||
float: right;
|
||||
position: relative;
|
||||
input {
|
||||
display: none;
|
||||
&:checked + .slider {
|
||||
background-color: $color-grad-dark-blue;
|
||||
}
|
||||
&:checked + .slider:before {
|
||||
transform: translateX(26px);
|
||||
}
|
||||
}
|
||||
.slider {
|
||||
background-color: $color-white-less;
|
||||
bottom: 0;
|
||||
cursor: pointer;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
transition: 0.4s;
|
||||
&::before {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
background-color: $color-white;
|
||||
bottom: 4px;
|
||||
content: "";
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
transition: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: 34px;
|
||||
&::before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
@@ -1,334 +0,0 @@
|
||||
@import "base/fonts";
|
||||
@import "base/reset";
|
||||
@import "base/variables";
|
||||
|
||||
body {
|
||||
color: $color-black;
|
||||
background-color: $color-grey-white;
|
||||
}
|
||||
|
||||
button > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#history {
|
||||
.history__content--holder {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 auto;
|
||||
|
||||
.table__content--holder {
|
||||
width: 1200px;
|
||||
max-width: 95%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 40px 0px 120px;
|
||||
|
||||
.history__head--holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
margin: 0.83em 0;
|
||||
}
|
||||
|
||||
a {
|
||||
border-bottom: 1px solid;
|
||||
margin-right: 10px;
|
||||
padding-bottom: 1px;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.table__content--wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: white;
|
||||
box-shadow: rgba(50, 50, 50, 0.2) 0px 6px 30px;
|
||||
flex: 1 1 auto;
|
||||
border-radius: 12px;
|
||||
|
||||
.table__content--head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgb(241, 241, 241);
|
||||
border-top-right-radius: 12px;
|
||||
border-top-left-radius: 12px;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.table__head--holder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1 1 auto;
|
||||
padding: 0px 24px;
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
|
||||
th {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0px;
|
||||
}
|
||||
|
||||
.table__head--longURL {
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 2 2 0px;
|
||||
font-size: 16px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.table__head--shortURL {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex: 1 1 0px;
|
||||
font-size: 16px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.table__head--clearAll {
|
||||
display: flex;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
padding: 16px 0px;
|
||||
|
||||
.table__list--clearAll {
|
||||
.table__listItem--clear {
|
||||
.table__clearAll--btn {
|
||||
color: #111;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 26px;
|
||||
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
|
||||
background-color: rgb(222, 222, 222);
|
||||
cursor: pointer;
|
||||
width: auto;
|
||||
margin: 0px 2px 0px 12px;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
outline: none;
|
||||
transition: all 0.2s ease-out 0s;
|
||||
padding: 0px 12px;
|
||||
border-radius: 100px;
|
||||
|
||||
img {
|
||||
margin: 1px 0px 3px 6px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table__content--body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.table__body--holder {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1 1 auto;
|
||||
padding: 0px 24px;
|
||||
border-bottom: 1px solid rgb(234, 234, 234);
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 0px;
|
||||
}
|
||||
|
||||
.table__body--original {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex: 2 2 0px;
|
||||
position: relative;
|
||||
|
||||
.table__body--originalURL {
|
||||
color: rgb(33, 150, 243);
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent;
|
||||
transition: all 0.2s ease-out 0s;
|
||||
font-size: 16px;
|
||||
line-height: 1.45;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(to left, white, white, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.table__body--shortened {
|
||||
white-space: nowrap;
|
||||
flex: 1 1 23px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
height: 100%;
|
||||
width: 56px;
|
||||
background: linear-gradient(to left, white, white, transparent);
|
||||
}
|
||||
|
||||
.table_body--flashCopy {
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
color: green;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.table__body--shortenBody {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.table__body--shortenURL {
|
||||
color: rgb(33, 150, 243);
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dotted transparent;
|
||||
transition: all 0.2s ease-out 0s;
|
||||
font-size: 16px;
|
||||
line-height: 1.45;
|
||||
|
||||
&:hover {
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table__body--functionBtns {
|
||||
.table__body--btnHolder {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
.table__body--qrcode,
|
||||
.table__body--copy {
|
||||
margin: 0px 2px 0px 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
box-shadow: rgba(100, 100, 100, 0.1) 0px 2px 4px;
|
||||
background-color: rgb(222, 222, 222);
|
||||
cursor: pointer;
|
||||
margin: 0px 12px 0px 2px;
|
||||
padding: 0px;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
outline: none;
|
||||
border-radius: 100%;
|
||||
transition: all 0.2s ease-out 0s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table__qrcodePopup--div {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(50, 50, 50, 0.8);
|
||||
z-index: 1000;
|
||||
|
||||
.table__qrcode--popup {
|
||||
text-align: center;
|
||||
background-color: white;
|
||||
padding: 48px 64px;
|
||||
border-radius: 8px;
|
||||
|
||||
.table__qrcode--holder {
|
||||
#table__qrcode {
|
||||
height: 196px;
|
||||
width: 196px;
|
||||
}
|
||||
}
|
||||
|
||||
.table__closebtn--holder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
|
||||
.table__closebtn--inner {
|
||||
margin: 0px 16px;
|
||||
position: relative;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
word-break: keep-all;
|
||||
cursor: pointer;
|
||||
color: black;
|
||||
box-shadow: rgba(160, 160, 160, 0.5) 0px 5px 6px;
|
||||
padding: 0px 32px;
|
||||
border-width: initial;
|
||||
border-style: none;
|
||||
border-color: initial;
|
||||
border-image: initial;
|
||||
border-radius: 100px;
|
||||
transition: all 0.4s ease-out 0s;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
rgb(224, 224, 224),
|
||||
rgb(189, 189, 189)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
@import "base/fonts";
|
||||
@import "base/reset";
|
||||
@import "base/variables";
|
||||
|
||||
body {
|
||||
color: $color-black;
|
||||
background-color: $color-grey-white;
|
||||
}
|
||||
|
||||
#options {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
|
||||
.options__content--holder {
|
||||
padding-top: 5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.head__content--holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.head__content--logo {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.head__content--title {
|
||||
color: $color-black;
|
||||
font-weight: $bold;
|
||||
padding: 0;
|
||||
margin: 0 0 0 0.4em;
|
||||
font-size: 34px;
|
||||
}
|
||||
}
|
||||
|
||||
.form__content--holder {
|
||||
margin-top: 3em;
|
||||
|
||||
.form__content {
|
||||
padding: 0px 100px 40px;
|
||||
width: 600px;
|
||||
|
||||
.api__key--label,
|
||||
.password--label,
|
||||
.copy--label,
|
||||
.customhost__mode--label {
|
||||
font-size: 16px;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.8em;
|
||||
}
|
||||
|
||||
.password--label,
|
||||
.api__key--label,
|
||||
.customhost__mode--label {
|
||||
.password__label--optional,
|
||||
.customhost__label--optional,
|
||||
.api__label--text {
|
||||
font-size: 13px;
|
||||
letter-spacing: 1px;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black;
|
||||
|
||||
&:hover .password__label--tooltiptext,
|
||||
&:hover .api__label--tooltiptext,
|
||||
&:hover .customhost__label--tooltiptext {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.password__label--tooltiptext,
|
||||
.api__label--tooltiptext,
|
||||
.customhost__label--tooltiptext {
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
font-size: 13px;
|
||||
width: 180px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 1px;
|
||||
background-color: $color-light-grey;
|
||||
color: $color-white;
|
||||
padding: 5px 0;
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
bottom: 125%;
|
||||
left: 50%;
|
||||
margin-left: -90px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: -5px;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: $color-light-grey transparent transparent
|
||||
transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.api__label--text {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.api__key--holder,
|
||||
.password--holder,
|
||||
.customhost__mode--holder {
|
||||
font-family: $font-nunito;
|
||||
width: 100%;
|
||||
border-radius: 100px;
|
||||
background-color: $color-white;
|
||||
border: none;
|
||||
box-shadow: $color-shadow-black 0px 10px 35px;
|
||||
color: $color-medium-grey;
|
||||
box-sizing: border-box;
|
||||
border-color: currentcolor currentcolor $color-border-white;
|
||||
border-style: none none solid;
|
||||
border-width: medium medium 4px;
|
||||
border-image: none 100% / 1 / 0 stretch;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
.api__key--holder {
|
||||
margin-bottom: 3em;
|
||||
padding: 12px 25px 12px 25px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.password--holder,
|
||||
.customhost__mode--holder {
|
||||
font-size: 20px;
|
||||
padding: 8px 25px 8px 25px;
|
||||
}
|
||||
|
||||
.view__password--eye {
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
margin-top: -36px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.saved__alert {
|
||||
margin-top: 2em;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.button__submit {
|
||||
font-family: $font-nunito;
|
||||
font-size: 18px;
|
||||
display: block;
|
||||
color: $color-white;
|
||||
width: 100%;
|
||||
background: $color-null-black
|
||||
linear-gradient(
|
||||
to right,
|
||||
$color-grad-light-blue,
|
||||
$color-grad-dark-blue
|
||||
)
|
||||
repeat scroll 0% 0%;
|
||||
box-shadow: $color-light-azure 0px 5px 6px;
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
padding: 12px;
|
||||
margin: 2em 0 1em 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: $color-null-black
|
||||
linear-gradient(
|
||||
to right,
|
||||
$color-grad-dark-blue,
|
||||
$color-light-blue
|
||||
)
|
||||
repeat scroll 0% 0%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer__text--holder {
|
||||
font-size: 12px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
letter-spacing: 1px;
|
||||
|
||||
.github__repo--link {
|
||||
border-bottom: 1px dotted $color-black;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
@import "base/fonts";
|
||||
@import "base/reset";
|
||||
@import "base/variables";
|
||||
|
||||
body {
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
#home {
|
||||
min-width: 340px;
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
line-height: 1;
|
||||
|
||||
.main__list--holder {
|
||||
.list__button {
|
||||
display: inline-block;
|
||||
margin-right: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content__holder {
|
||||
.url__content--holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 1.5em 0;
|
||||
|
||||
.url__content--url {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
#url__content-inner {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
color: $color-dark-grey;
|
||||
font-weight: $bold;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
#copy__alert {
|
||||
position: absolute;
|
||||
margin-top: 3.6em;
|
||||
top: 0px;
|
||||
color: green;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons__content--holder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 0 0 0 16px;
|
||||
|
||||
.copy__content--holder,
|
||||
.qrbtn__content--holder {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
margin: 0 6px;
|
||||
border-radius: 100%;
|
||||
box-shadow: $color-shadow-light 0px 2px 4px;
|
||||
background-color: $color-less-white;
|
||||
|
||||
#button__copy,
|
||||
#button__qrcode {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/util/autoSave.ts
Normal file
59
src/util/autoSave.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { connect, FormikContextType } from 'formik';
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import isEqual from 'lodash.isequal';
|
||||
|
||||
interface FormikPartProperties {
|
||||
formik: FormikContextType<any>;
|
||||
}
|
||||
|
||||
interface OuterProperties {
|
||||
onSave: (values: any) => Promise<any>;
|
||||
render: any;
|
||||
}
|
||||
|
||||
const usePrevious = (value: any): any => {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
ref.current = value;
|
||||
});
|
||||
|
||||
return ref.current;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns boolean
|
||||
*
|
||||
* Wrapping with formik HOC can give access to form values.
|
||||
* It calls the passed callback with form values as arguments.
|
||||
*
|
||||
* Ref: https://github.com/jaredpalmer/formik/issues/172#issuecomment-528192124
|
||||
*/
|
||||
const AutoSave = ({ formik: { values }, onSave, render }: OuterProperties & FormikPartProperties): any => {
|
||||
const previousValues = usePrevious(values);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function callback(value: any): any {
|
||||
// promise fulfilled
|
||||
setIsSaving(false);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
if (previousValues && Object.keys(previousValues).length && !isEqual(previousValues, values)) {
|
||||
// values are updated
|
||||
setIsSaving(true);
|
||||
// invoke passed promise callback
|
||||
onSave(values).then(callback, callback);
|
||||
}
|
||||
}
|
||||
|
||||
save();
|
||||
}, [onSave, previousValues, values]);
|
||||
|
||||
return render({ isSaving });
|
||||
};
|
||||
|
||||
export default connect<OuterProperties, any>(AutoSave);
|
||||
16
src/util/mesageUtil.ts
Normal file
16
src/util/mesageUtil.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { browser } from 'webextension-polyfill-ts';
|
||||
|
||||
const messageUtil = {
|
||||
send(name: string, params?: any): Promise<any> {
|
||||
const data = {
|
||||
action: name,
|
||||
params,
|
||||
};
|
||||
|
||||
return browser.runtime.sendMessage(data);
|
||||
},
|
||||
};
|
||||
|
||||
export default messageUtil;
|
||||
33
src/util/settings.ts
Normal file
33
src/util/settings.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { browser, Storage } from 'webextension-polyfill-ts';
|
||||
|
||||
import { DomainEntryProperties } from '../Background';
|
||||
|
||||
// Core Extensions settings props
|
||||
type ExtensionSettingsProperties = {
|
||||
apikey: string;
|
||||
autocopy: boolean;
|
||||
history: boolean;
|
||||
user?: {
|
||||
email?: string;
|
||||
domains?: DomainEntryProperties[];
|
||||
} | null;
|
||||
};
|
||||
|
||||
// update extension settings in browser storage
|
||||
export function saveExtensionSettings(settings: Storage.StorageAreaSetItemsType): Promise<void> {
|
||||
return browser.storage.local.set({
|
||||
settings,
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getExtensionSettings(): Promise<{ [s: string]: any }> {
|
||||
return browser.storage.local.get('settings');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export async function updateExtensionSettings(newFields: { [s: string]: any }): Promise<void> {
|
||||
const { settings = {} } = await getExtensionSettings();
|
||||
|
||||
return saveExtensionSettings({ ...settings, ...newFields });
|
||||
}
|
||||
12
src/util/tabs.ts
Normal file
12
src/util/tabs.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { browser, Tabs } from 'webextension-polyfill-ts';
|
||||
|
||||
export function openExtOptionsPage(): Promise<void> {
|
||||
return browser.runtime.openOptionsPage();
|
||||
}
|
||||
|
||||
export function getCurrentTab(): Promise<Tabs.Tab[]> {
|
||||
return browser.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
});
|
||||
}
|
||||
54
tsconfig.json
Normal file
54
tsconfig.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"outDir": "dist",
|
||||
/* for manifest/index.js */
|
||||
"allowJs": true,
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"jsx": "react",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
"noEmit": true, /* Do not emit outputs. */
|
||||
"noEmitOnError": true,
|
||||
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true,
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Additional Checks */
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"useDefineForClassFields": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"pretty": true,
|
||||
"newLine": "lf",
|
||||
"stripInternal": true,
|
||||
"noUnusedLocals": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*", "src/manifest/index.js"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"awesomeTypescriptLoaderOptions": {
|
||||
"useBabel": true,
|
||||
"babelCore": "@babel/core", // needed for Babel v7,
|
||||
"useCache": true, // Use internal file cache to improve warm-up time.
|
||||
}
|
||||
}
|
||||
11
views/options.html
Normal file
11
views/options.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=500" />
|
||||
<title>Options: Kutt</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="options-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
11
views/popup.html
Normal file
11
views/popup.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=500" />
|
||||
<title>Kutt Extension</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="popup-root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,20 +1,38 @@
|
||||
/* eslint-disable global-require, import/no-extraneous-dependencies */
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const WriteWebpackPlugin = require('write-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const ZipPlugin = require('zip-webpack-plugin');
|
||||
const wextManifest = require('wext-manifest');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const wextManifest = require('wext-manifest');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const WriteWebpackPlugin = require('write-webpack-plugin');
|
||||
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
||||
const { CheckerPlugin } = require('awesome-typescript-loader');
|
||||
const ExtensionReloader = require('webpack-extension-reloader');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
|
||||
const manifestInput = require('./src/manifest');
|
||||
|
||||
const targetBrowser = process.env.TARGET_BROWSER;
|
||||
const sourcePath = path.join(__dirname, 'src');
|
||||
const viewsPath = path.join(__dirname, 'views');
|
||||
const destPath = path.join(__dirname, 'extension');
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const manifest = wextManifest[targetBrowser](manifestInput);
|
||||
|
||||
const extensionReloader =
|
||||
nodeEnv === 'development'
|
||||
? new ExtensionReloader({
|
||||
port: 9128,
|
||||
reloadPage: true,
|
||||
entries: {
|
||||
// TODO: reload manifest on update
|
||||
background: 'background',
|
||||
extensionPage: ['popup', 'options'],
|
||||
},
|
||||
})
|
||||
: () => {
|
||||
this.apply = () => {};
|
||||
};
|
||||
|
||||
const getExtensionFileType = () => {
|
||||
if (targetBrowser === 'opera') {
|
||||
return 'crx';
|
||||
@@ -22,139 +40,98 @@ const getExtensionFileType = () => {
|
||||
if (targetBrowser === 'firefox') {
|
||||
return 'xpi';
|
||||
}
|
||||
|
||||
return 'zip';
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
return {
|
||||
entry: {
|
||||
options: ['./src/scripts/options.js'],
|
||||
popup: ['./src/scripts/popup.js'],
|
||||
history: ['./src/scripts/history.js'],
|
||||
background: ['./src/scripts/background.js'],
|
||||
styles: ['./src/styles/popup.scss', './src/styles/history.scss', './src/styles/options.scss'],
|
||||
module.exports = {
|
||||
mode: nodeEnv,
|
||||
|
||||
entry: {
|
||||
background: path.join(sourcePath, 'Background', 'index.ts'),
|
||||
options: path.join(sourcePath, 'Options', 'index.tsx'),
|
||||
popup: path.join(sourcePath, 'Popup', 'index.tsx'),
|
||||
},
|
||||
|
||||
output: {
|
||||
filename: 'js/[name].bundle.js',
|
||||
path: path.join(destPath, targetBrowser),
|
||||
},
|
||||
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js', '.json'],
|
||||
alias: {
|
||||
'webextension-polyfill-ts': path.resolve(path.join(__dirname, 'node_modules', 'webextension-polyfill-ts')),
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'extension', targetBrowser),
|
||||
filename: 'js/[name].js',
|
||||
publicPath: '',
|
||||
},
|
||||
node: {
|
||||
fs: 'empty',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
loader: 'babel-loader',
|
||||
},
|
||||
{
|
||||
test: /\.(html)$/,
|
||||
use: {
|
||||
loader: 'html-loader',
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts|tsx)?$/,
|
||||
loader: 'awesome-typescript-loader',
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.(sa|sc|c)ss$/,
|
||||
use: [
|
||||
{
|
||||
loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS
|
||||
},
|
||||
{
|
||||
loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack
|
||||
options: {
|
||||
attrs: [':data-src'],
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
loader: 'url-loader',
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].css',
|
||||
context: './src/styles/',
|
||||
outputPath: 'css/',
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader', // For autoprefixer
|
||||
options: {
|
||||
ident: 'postcss',
|
||||
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||
plugins: [require('autoprefixer')()],
|
||||
},
|
||||
{
|
||||
loader: 'extract-loader',
|
||||
},
|
||||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'resolve-url-loader',
|
||||
},
|
||||
{
|
||||
loader: 'postcss-loader',
|
||||
options: {
|
||||
plugins() {
|
||||
return [require('precss'), require('autoprefixer')];
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new FixStyleOnlyEntriesPlugin({ silent: true }),
|
||||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: [
|
||||
path.join(process.cwd(), `extension/${targetBrowser}`),
|
||||
path.join(process.cwd(), `extension/${targetBrowser}.${getExtensionFileType()}`),
|
||||
},
|
||||
'resolve-url-loader', // Rewrites relative paths in url() statements
|
||||
'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS
|
||||
],
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
new CopyWebpackPlugin([{ from: 'src/assets', to: 'assets' }]),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/options.html',
|
||||
inject: false,
|
||||
filename: 'options.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/popup.html',
|
||||
inject: false,
|
||||
filename: 'popup.html',
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: 'src/history.html',
|
||||
inject: false,
|
||||
filename: 'history.html',
|
||||
}),
|
||||
new WriteWebpackPlugin([{ name: manifest.name, data: Buffer.from(manifest.content) }]),
|
||||
},
|
||||
],
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new OptimizeCssAssetsPlugin({
|
||||
assetNameRegExp: /\.css$/g,
|
||||
cssProcessor: require('cssnano'),
|
||||
cssProcessorOptions: {
|
||||
map: false,
|
||||
},
|
||||
cssProcessorPluginOptions: {
|
||||
preset: ['default', { discardComments: { removeAll: true } }],
|
||||
},
|
||||
canPrint: true,
|
||||
}),
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
}),
|
||||
new ZipPlugin({
|
||||
path: path.resolve(__dirname, 'extension'),
|
||||
extension: `${getExtensionFileType()}`,
|
||||
filename: `${targetBrowser}`,
|
||||
}),
|
||||
},
|
||||
|
||||
plugins: [
|
||||
// for awesome-typescript-loader
|
||||
new CheckerPlugin(),
|
||||
// environment variables
|
||||
new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']),
|
||||
// delete previous build files
|
||||
new CleanWebpackPlugin({
|
||||
cleanOnceBeforeBuildPatterns: [
|
||||
path.join(process.cwd(), `extension/${targetBrowser}`),
|
||||
path.join(process.cwd(), `extension/${targetBrowser}.${getExtensionFileType()}`),
|
||||
],
|
||||
},
|
||||
devServer: {
|
||||
port: 3000,
|
||||
contentBase: './extension',
|
||||
},
|
||||
};
|
||||
cleanStaleWebpackAssets: false,
|
||||
verbose: true,
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'popup.html'),
|
||||
inject: 'body',
|
||||
filename: 'popup.html',
|
||||
chunks: ['popup'],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(viewsPath, 'options.html'),
|
||||
inject: 'body',
|
||||
filename: 'options.html',
|
||||
chunks: ['options'],
|
||||
}),
|
||||
// write css file(s) to build folder
|
||||
new MiniCssExtractPlugin({ filename: 'css/[name].css' }),
|
||||
// copy static assets
|
||||
new CopyWebpackPlugin([{ from: path.join(sourcePath, 'assets'), to: 'assets' }]),
|
||||
// write manifest.json
|
||||
new WriteWebpackPlugin([{ name: manifest.name, data: Buffer.from(manifest.content) }]),
|
||||
// plugin to enable browser reloading in development mode
|
||||
extensionReloader,
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user