refactor: add vuex, split code in services and refactor content script controller
35
package-lock.json
generated
@@ -6728,6 +6728,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/devtools-api": {
|
||||
"version": "6.0.0-beta.14",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.0.0-beta.14.tgz",
|
||||
"integrity": "sha512-44fPrrN1cqcs6bFkT0C+yxTM6PZXLbR+ESh1U1j8UD22yO04gXvxH62HApMjLbS3WqJO/iCNC+CYT+evPQh2EQ=="
|
||||
},
|
||||
"@vue/eslint-config-prettier": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-6.0.0.tgz",
|
||||
@@ -10727,15 +10732,15 @@
|
||||
}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.10.0.tgz",
|
||||
"integrity": "sha512-xdr6e4t/L2moRAeEQ9HKgge/hFq+w9v5Dj+BA54nTAzSFdUyKLiSOdZaRQjCHMY0Pk2WaQBFH9QiWG60xiC+6A==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-7.0.0.tgz",
|
||||
"integrity": "sha512-SFcM8ZMdVRUQKrAryl6Q5razrJtloATUOKTZoK/8yvQig1c1L3VRoMLL9AXiV3VNsKXolkk6yeMIiqGf33/Iew==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-utils": "^2.1.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.3.2",
|
||||
"vue-eslint-parser": "^7.6.0"
|
||||
"vue-eslint-parser": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-utils": {
|
||||
@@ -17861,6 +17866,14 @@
|
||||
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
|
||||
"dev": true
|
||||
},
|
||||
"pinia": {
|
||||
"version": "2.0.0-beta.3",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.0-beta.3.tgz",
|
||||
"integrity": "sha512-4ygKhe9FrYD69tJ7nSdgHm9Ldb0aM/Nzyb8Qz/RZuzOyOr85jWHNmCAhCytWy0l9C4/ypGJYCEJ3vuZfyWjcZA==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.0.0-beta.14"
|
||||
}
|
||||
},
|
||||
"pinkie": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
|
||||
@@ -22127,9 +22140,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"vue-jest": {
|
||||
"version": "5.0.0-alpha.9",
|
||||
"resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-5.0.0-alpha.9.tgz",
|
||||
"integrity": "sha512-OEegZLEc17ELrDhkGOpU9ZX2U8o7aOuy+pHsHhrvBP3tWiuQi+GJnJOaCNimCej41ugZDKc9KI5LuSB8l24U5Q==",
|
||||
"version": "5.0.0-alpha.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-jest/-/vue-jest-5.0.0-alpha.10.tgz",
|
||||
"integrity": "sha512-iN62cTi4AL0UsgxEyVeJtHG6qXEv+8Ci2wX1vP3b/dAZvyBRmqy5aJHQrP6VCEuio+HgHQ1LAZ+ccM2pouBmlg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.2.0",
|
||||
@@ -22218,6 +22231,14 @@
|
||||
"highlight.js": "^10.3.2"
|
||||
}
|
||||
},
|
||||
"vuex": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-4.0.2.tgz",
|
||||
"integrity": "sha512-M6r8uxELjZIK8kTKDGgZTYX/ahzblnzC4isU1tpmEuOIIKmV+TRdc+H4s8ds2NuZ7wpUTdGRzJRtoj+lI+pc0Q==",
|
||||
"requires": {
|
||||
"@vue/devtools-api": "^6.0.0-beta.11"
|
||||
}
|
||||
},
|
||||
"w3c-hr-time": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
"autoprefixer": "9",
|
||||
"core-js": "3.6.5",
|
||||
"css-selector-generator": "3.0.1",
|
||||
"pinia": "2.0.0-beta.3",
|
||||
"postcss": "7",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@2.0.2",
|
||||
"vue": "3.0.6",
|
||||
"vue-tippy": "6.0.0-alpha.30",
|
||||
"vue3-highlightjs": "1.0.5"
|
||||
"vue3-highlightjs": "1.0.5",
|
||||
"vuex": "4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "5.12.0",
|
||||
@@ -33,7 +35,7 @@
|
||||
"babel-eslint": "10.1.0",
|
||||
"eslint": "6.7.2",
|
||||
"eslint-plugin-prettier": "3.1.3",
|
||||
"eslint-plugin-vue": "7.0.0-0",
|
||||
"eslint-plugin-vue": "7.0.0",
|
||||
"jest": "26.6.3",
|
||||
"jest-vue-preprocessor": "1.7.1",
|
||||
"node-sass": "5.0.0",
|
||||
@@ -44,6 +46,6 @@
|
||||
"typescript": "3.9.3",
|
||||
"vue-cli-plugin-browser-extension": "0.25.1",
|
||||
"vue-cli-plugin-tailwind": "2.0.6",
|
||||
"vue-jest": "5.0.0-0"
|
||||
"vue-jest": "^5.0.0-0"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 908 B After Width: | Height: | Size: 908 B |
4
public/icons/light/duplicate.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 6.75C0 5.7835 0.783502 5 1.75 5H3.25C3.66421 5 4 5.33579 4 5.75C4 6.16421 3.66421 6.5 3.25 6.5H1.75C1.61193 6.5 1.5 6.61193 1.5 6.75V14.25C1.5 14.3881 1.61193 14.5 1.75 14.5H9.25C9.38807 14.5 9.5 14.3881 9.5 14.25V12.75C9.5 12.3358 9.83579 12 10.25 12C10.6642 12 11 12.3358 11 12.75V14.25C11 15.2165 10.2165 16 9.25 16H1.75C0.783502 16 0 15.2165 0 14.25V6.75Z" fill="#1F2D3D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 1.75C5 0.783502 5.7835 0 6.75 0H14.25C15.2165 0 16 0.783502 16 1.75V9.25C16 10.2165 15.2165 11 14.25 11H6.75C5.7835 11 5 10.2165 5 9.25V1.75ZM6.75 1.5C6.61193 1.5 6.5 1.61193 6.5 1.75V9.25C6.5 9.38807 6.61193 9.5 6.75 9.5H14.25C14.3881 9.5 14.5 9.38807 14.5 9.25V1.75C14.5 1.61193 14.3881 1.5 14.25 1.5H6.75Z" fill="#1F2D3D"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 912 B |
@@ -1,12 +1,3 @@
|
||||
/* a11y-dark theme */
|
||||
/* Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css */
|
||||
/* @author: ericwbailey */
|
||||
|
||||
|
||||
/* code.hljs {
|
||||
background: #8b8787 !important;
|
||||
} */
|
||||
|
||||
.hljs-line {
|
||||
color: '#ADAACC';
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
<svg width="27" height="25" viewBox="0 0 27 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.69329 17.1234L3.52904 15.108C3.78327 15.2135 4.06378 15.2727 4.36364 15.2727H6.54545V17.4545H4.36364C3.77196 17.4545 3.2078 17.3368 2.69329 17.1234ZM10.9091 17.4545V15.2727H13.0909C13.3908 15.2727 13.6713 15.2135 13.9255 15.108L14.7613 17.1234C14.2467 17.3368 13.6826 17.4545 13.0909 17.4545H10.9091ZM17.4545 6.54545H15.2727V4.36364C15.2727 4.06378 15.2135 3.78327 15.108 3.52904L17.1234 2.69329C17.3368 3.2078 17.4545 3.77196 17.4545 4.36364V6.54545ZM6.54545 0H4.36364C3.77196 0 3.2078 0.117758 2.69329 0.331115L3.52904 2.34652C3.78327 2.24109 4.06378 2.18182 4.36364 2.18182H6.54545V0ZM0 10.9091H2.18182V13.0909C2.18182 13.3908 2.24109 13.6713 2.34652 13.9255L0.331115 14.7613C0.117758 14.2467 0 13.6826 0 13.0909V10.9091ZM0 6.54545H2.18182V4.36364C2.18182 4.06378 2.24109 3.78327 2.34652 3.52904L0.331115 2.69329C0.117758 3.2078 0 3.77196 0 4.36364V6.54545ZM10.9091 0V2.18182H13.0909C13.3908 2.18182 13.6713 2.24109 13.9255 2.34652L14.7613 0.331115C14.2467 0.117758 13.6826 0 13.0909 0H10.9091ZM17.4545 10.9091H15.2727V13.0909C15.2727 13.3908 15.2135 13.6713 15.108 13.9255L17.1234 14.7613C17.3368 14.2467 17.4545 13.6826 17.4545 13.0909V10.9091Z" fill="#F9FAFC"/>
|
||||
<rect x="6.54541" y="8" width="18.9091" height="14.5455" fill="#2E2E2E"/>
|
||||
<path d="M10.9091 12.3637L9.65836 12.9891C8.64201 13.4973 8 14.536 8 15.6724V21.0001C8 22.6569 9.34315 24.0001 11 24.0001H22.4545C24.1114 24.0001 25.4545 22.6569 25.4545 21.0001V15.6724C25.4545 14.536 24.8125 13.4973 23.7962 12.9891L22.5455 12.3637" stroke="#F9FAFC" stroke-width="2"/>
|
||||
<path d="M20.3637 13.091V12.4546C20.3637 10.7977 19.0205 9.45459 17.3637 9.45459H16.0909C14.4341 9.45459 13.0909 10.7977 13.0909 12.4546V13.091" stroke="#F9FAFC" stroke-width="2"/>
|
||||
<circle cx="16.7273" cy="18.1818" r="2.63636" stroke="#F9FAFC" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="35" height="34" viewBox="0 0 35 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.125C0 2.67525 1.17525 1.5 2.625 1.5H21.375C22.8247 1.5 24 2.67525 24 4.125V19.875C24 21.3247 22.8247 22.5 21.375 22.5H2.625C1.17525 22.5 0 21.3247 0 19.875V4.125ZM2.625 3.75C2.41789 3.75 2.25 3.91789 2.25 4.125V6.75H5.25V3.75H2.625ZM7.5 3.75V6.75H10.5V3.75H7.5ZM12.75 3.75V6.75H21.75V4.125C21.75 3.91789 21.5821 3.75 21.375 3.75H12.75ZM21.75 9H2.25V19.875C2.25 20.0821 2.41789 20.25 2.625 20.25H21.375C21.5821 20.25 21.75 20.0821 21.75 19.875V9Z" fill="#1F2D3D"/>
|
||||
<rect x="8" y="11" width="26" height="20" fill="#EFF2F7"/>
|
||||
<path d="M14 17L11.6584 18.1708C10.642 18.679 10 19.7178 10 20.8541V30C10 31.6569 11.3431 33 13 33H31C32.6569 33 34 31.6569 34 30V20.8541C34 19.7178 33.358 18.679 32.3416 18.1708L30 17" stroke="#1F2D3D" stroke-width="2"/>
|
||||
<path d="M27 18V16C27 14.3431 25.6569 13 24 13H20C18.3431 13 17 14.3431 17 16V18" stroke="#1F2D3D" stroke-width="2"/>
|
||||
<circle cx="22" cy="25" r="4" stroke="#1F2D3D" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 3.75C16 3.4798 15.8547 3.23048 15.6195 3.09735C15.3844 2.96422 15.0958 2.96786 14.8641 3.10688L11 5.42536V4.75C11 3.7835 10.2165 3 9.25 3H1.75C0.783502 3 0 3.7835 0 4.75V11.25C0 12.2165 0.783502 13 1.75 13H9.25C10.2165 13 11 12.2165 11 11.25V10.5746L14.8641 12.8931C15.0958 13.0321 15.3844 13.0358 15.6195 12.9026C15.8547 12.7695 16 12.5202 16 12.25V3.75ZM11 8.82536L14.5 10.9254V5.07464L11 7.17464V8.82536ZM9.5 6.75V4.75C9.5 4.61193 9.38807 4.5 9.25 4.5H1.75C1.61193 4.5 1.5 4.61193 1.5 4.75V11.25C1.5 11.3881 1.61193 11.5 1.75 11.5H9.25C9.38807 11.5 9.5 11.3881 9.5 11.25V9.25V6.75Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 757 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.625 4.5C5.625 3.87868 6.12868 3.375 6.75 3.375H11.25C11.8713 3.375 12.375 3.87868 12.375 4.5V22.5C12.375 23.1213 11.8713 23.625 11.25 23.625H6.75C6.12868 23.625 5.625 23.1213 5.625 22.5V4.5ZM7.875 5.625V21.375H10.125V5.625H7.875Z" fill="#1F2D3D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.625 4.5C14.625 3.87868 15.1287 3.375 15.75 3.375H20.25C20.8713 3.375 21.375 3.87868 21.375 4.5V22.5C21.375 23.1213 20.8713 23.625 20.25 23.625H15.75C15.1287 23.625 14.625 23.1213 14.625 22.5V4.5ZM16.875 5.625V21.375H19.125V5.625H16.875Z" fill="#1F2D3D"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 711 B |
@@ -1,14 +0,0 @@
|
||||
<svg width="26" height="25" viewBox="0 0 26 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 2.99997C0 1.94561 0.854729 1.09088 1.90909 1.09088H15.5454C16.5998 1.09088 17.4545 1.94561 17.4545 2.99997V14.4545C17.4545 15.5089 16.5998 16.3636 15.5454 16.3636H1.90909C0.854728 16.3636 0 15.5089 0 14.4545V2.99997ZM1.90909 2.72724C1.75847 2.72724 1.63636 2.84935 1.63636 2.99997V4.90906H3.81818V2.72724H1.90909ZM5.45454 2.72724V4.90906H7.63636V2.72724H5.45454ZM9.27272 2.72724V4.90906H15.8182V2.99997C15.8182 2.84935 15.6961 2.72724 15.5454 2.72724H9.27272ZM15.8182 6.54542H1.63636V14.4545C1.63636 14.6051 1.75847 14.7272 1.90909 14.7272H15.5454C15.6961 14.7272 15.8182 14.6051 15.8182 14.4545V6.54542Z" fill="#F9FAFC"/>
|
||||
</g>
|
||||
<rect x="5.81812" y="8" width="18.9091" height="14.5454" fill="#2E2E2E"/>
|
||||
<path d="M10.1822 12.3637L8.93143 12.9891C7.91508 13.4972 7.27307 14.536 7.27307 15.6724V21.0001C7.27307 22.6569 8.61622 24.0001 10.2731 24.0001H21.7276C23.3845 24.0001 24.7276 22.6569 24.7276 21.0001V15.6724C24.7276 14.536 24.0856 13.4972 23.0692 12.9891L21.8185 12.3637" stroke="#F9FAFC" stroke-width="2"/>
|
||||
<path d="M19.6369 13.091V12.4546C19.6369 10.7977 18.2937 9.45459 16.6369 9.45459H15.3641C13.7073 9.45459 12.3641 10.7977 12.3641 12.4546V13.091" stroke="#F9FAFC" stroke-width="2"/>
|
||||
<circle cx="16.0005" cy="18.1818" r="2.63636" stroke="#F9FAFC" stroke-width="2"/>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="17.4545" height="17.4545" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +0,0 @@
|
||||
<svg width="36" height="34" viewBox="0 0 36 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.70327 23.5447L4.85243 20.7735C5.202 20.9185 5.5877 21 6 21H9V24H6C5.18645 24 4.41072 23.8381 3.70327 23.5447ZM15 24V21H18C18.4123 21 18.798 20.9185 19.1476 20.7735L20.2967 23.5447C19.5893 23.8381 18.8135 24 18 24H15ZM24 9H21V6C21 5.5877 20.9185 5.202 20.7735 4.85244L23.5447 3.70327C23.8381 4.41072 24 5.18645 24 6V9ZM9 0H6C5.18645 0 4.41072 0.161917 3.70327 0.455283L4.85244 3.22646C5.202 3.0815 5.5877 3 6 3H9V0ZM0 15H3V18C3 18.4123 3.0815 18.798 3.22646 19.1476L0.455283 20.2967C0.161917 19.5893 0 18.8135 0 18V15ZM0 9H3V6C3 5.5877 3.0815 5.202 3.22646 4.85243L0.455283 3.70327C0.161917 4.41072 0 5.18645 0 6V9ZM15 0V3H18C18.4123 3 18.798 3.0815 19.1476 3.22646L20.2967 0.455283C19.5893 0.161917 18.8135 0 18 0H15ZM24 15H21V18C21 18.4123 20.9185 18.798 20.7735 19.1476L23.5447 20.2967C23.8381 19.5893 24 18.8135 24 18V15Z" fill="#1F2D3D"/>
|
||||
<rect x="9" y="11" width="26" height="20" fill="#EFF2F7"/>
|
||||
<path d="M15 17L12.6584 18.1708C11.642 18.679 11 19.7178 11 20.8541V30C11 31.6569 12.3431 33 14 33H32C33.6569 33 35 31.6569 35 30V20.8541C35 19.7178 34.358 18.679 33.3416 18.1708L31 17" stroke="#1F2D3D" stroke-width="2"/>
|
||||
<path d="M28 18V16C28 14.3431 26.6569 13 25 13H21C19.3431 13 18 14.3431 18 16V18" stroke="#1F2D3D" stroke-width="2"/>
|
||||
<circle cx="23" cy="25" r="4" stroke="#1F2D3D" stroke-width="2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,163 +0,0 @@
|
||||
.shake {
|
||||
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
10%, 90% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
}
|
||||
|
||||
20%, 80% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
}
|
||||
|
||||
30%, 50%, 70% {
|
||||
transform: translate3d(-4px, 0, 0);
|
||||
}
|
||||
|
||||
40%, 60% {
|
||||
transform: translate3d(4px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s ease infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
.flash-once {
|
||||
animation: flash-once 3.5s ease 1;
|
||||
}
|
||||
|
||||
@keyframes fade-up {
|
||||
0% {
|
||||
transform: translate3d(0, 10px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fade-in .3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation-name: spin;
|
||||
animation-duration: 2000ms;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {transform:rotate(0deg);}
|
||||
to {transform:rotate(360deg);}
|
||||
}
|
||||
|
||||
|
||||
.bounceIn {
|
||||
animation-name: bounceIn;
|
||||
transform-origin: center bottom;
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: 1;
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0%, 20%, 40%, 60%, 80%, 100% {
|
||||
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
|
||||
}
|
||||
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(.8, .8, .8);
|
||||
transform: scale3d(.8, .8, .8);
|
||||
}
|
||||
|
||||
20% {
|
||||
-webkit-transform: scale3d(1.1, 1.1, 1.1);
|
||||
transform: scale3d(1.1, 1.1, 1.1);
|
||||
}
|
||||
|
||||
40% {
|
||||
-webkit-transform: scale3d(.9, .9, .9);
|
||||
transform: scale3d(.9, .9, .9);
|
||||
}
|
||||
|
||||
60% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1.03, 1.03, 1.03);
|
||||
transform: scale3d(1.03, 1.03, 1.03);
|
||||
}
|
||||
|
||||
80% {
|
||||
-webkit-transform: scale3d(.97, .97, .97);
|
||||
transform: scale3d(.97, .97, .97);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale3d(1, 1, 1);
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0%, 20% {
|
||||
color: rgba(0,0,0,0);
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0,0,0,0),
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
40% {
|
||||
color: #8492A6;
|
||||
text-shadow:
|
||||
.25em 0 0 rgba(0,0,0,0),
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
60% {
|
||||
text-shadow:
|
||||
.25em 0 0 #8492A6,
|
||||
.5em 0 0 rgba(0,0,0,0);}
|
||||
80%, 100% {
|
||||
text-shadow:
|
||||
.25em 0 0 #8492A6,
|
||||
.5em 0 0 #8492A6;}}
|
||||
|
||||
@keyframes recording {
|
||||
0% {
|
||||
box-shadow: 0px 0px 5px 0px rgba(173,0,0,.3);
|
||||
}
|
||||
65% {
|
||||
box-shadow: 0px 0px 5px 5px rgba(173,0,0,.3);
|
||||
}
|
||||
90% {
|
||||
box-shadow: 0px 0px 5px 5px rgba(173,0,0,0);
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
button {
|
||||
&.btn {
|
||||
display: inline-block;
|
||||
font-weight: 300;
|
||||
line-height: 1.25;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
letter-spacing: 1px;
|
||||
transition: all .15s ease;
|
||||
|
||||
&.btn-sm {
|
||||
padding: .4rem .8rem;
|
||||
font-size: .8rem;
|
||||
border-radius: .2rem
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: #fff;
|
||||
background-color: $brand-primary;
|
||||
border-color: $brand-primary;
|
||||
}
|
||||
|
||||
&.btn-outline-primary {
|
||||
color: $brand-primary;
|
||||
background-color: transparent;
|
||||
border-color: $brand-primary
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
color: #fff;
|
||||
background-color: $brand-danger;
|
||||
border-color: $brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
@mixin header () {
|
||||
background: $gray-lightest;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: 0 $spacer;
|
||||
font-weight: 500;
|
||||
}
|
||||
@mixin footer() {
|
||||
background: $gray-lightest;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 $spacer;
|
||||
font-weight: 500;
|
||||
border-top: 1px solid $gray-light;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// drop down
|
||||
.drop-down-enter, .drop-down-leave-to {
|
||||
transform: translateX(0) translateY(-20px);
|
||||
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.drop-down-enter-active, .drop-down-leave-active {
|
||||
transition: all .15s
|
||||
}
|
||||
|
||||
// move left
|
||||
.move-left-enter, .move-left-leave-to {
|
||||
transform: translateY(0) translateX(-80px);
|
||||
transition-timing-function: cubic-bezier(.74,.04,.26,1.05);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.move-left-enter-active, .move-left-leave-active {
|
||||
transition: all .15s
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.text-muted {
|
||||
color: $gray;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// spacing
|
||||
$spacer: 12px;
|
||||
|
||||
// colors
|
||||
$white: #fff;
|
||||
$black: #1f2d3d;
|
||||
$red: #FF4949;
|
||||
$orange: #F19B45;
|
||||
$yellow: #FFC82C;
|
||||
$green: #13CE66;
|
||||
|
||||
$blue: #45C8F1;
|
||||
$blue-light: #EBFAFF;
|
||||
$blue-lightest: #F0F8FF;
|
||||
$teal: #205E71;
|
||||
$pink: #FF659D;
|
||||
|
||||
$gray-dark: #3C4858;
|
||||
$gray: #8492A6;
|
||||
$gray-light: #E0E6ED;
|
||||
$gray-lighter: #EFF2F7;
|
||||
$gray-lightest: #F9FAFC;
|
||||
|
||||
$brand-primary: $blue;
|
||||
$brand-success: $green;
|
||||
$brand-info: $teal;
|
||||
$brand-warning: $orange;
|
||||
$brand-danger: $red;
|
||||
$brand-inverse: $gray-dark;
|
||||
$brand-accent: $pink;
|
||||
|
||||
// typography
|
||||
|
||||
$text-muted: $gray;
|
||||
|
||||
// layout
|
||||
|
||||
$max-content-height: 400px;
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* GitHub Gist Theme
|
||||
* Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
background: white;
|
||||
padding: 0.5em;
|
||||
color: #333333;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-meta {
|
||||
color: #969896;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-strong,
|
||||
.hljs-emphasis,
|
||||
.hljs-quote {
|
||||
color: #df5000;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.hljs-selector-tag,
|
||||
.hljs-type {
|
||||
color: #a71d5d;
|
||||
}
|
||||
|
||||
.hljs-literal,
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-attribute {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name {
|
||||
color: #63a35c;
|
||||
}
|
||||
|
||||
.hljs-tag {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-attr,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo {
|
||||
color: #795da3;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
@import "variables";
|
||||
@import "buttons";
|
||||
@import "typography";
|
||||
@import "transitions";
|
||||
@import "animations";
|
||||
@import "github-gist.css";
|
||||
@import "mixins";
|
||||
|
||||
//reset
|
||||
|
||||
html {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
color: $gray-dark;
|
||||
width: 350px;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -101,7 +101,7 @@ class RecordingController {
|
||||
console.debug('recording stored')
|
||||
})
|
||||
|
||||
this.toggleSelectorHelper()
|
||||
// this.toggleOverlay()
|
||||
}
|
||||
|
||||
pause() {
|
||||
@@ -275,12 +275,11 @@ class RecordingController {
|
||||
|
||||
injectScript() {
|
||||
chrome.tabs.executeScript({ file: 'js/content-script.js', allFrames: false }, () => {
|
||||
this.toggleSelectorHelper(true)
|
||||
this.toggleOverlay(true)
|
||||
})
|
||||
}
|
||||
|
||||
toggleSelectorHelper(value = false) {
|
||||
console.debug('toggling overlay')
|
||||
toggleOverlay(value = false) {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, tabs => {
|
||||
chrome.tabs.sendMessage(tabs[0].id, {
|
||||
action: uiActions.TOGGLE_OVERLAY,
|
||||
@@ -1,4 +1,4 @@
|
||||
import UIController from '../screenshot-controller'
|
||||
import UIController from '../shooter'
|
||||
|
||||
// this test NEEDS to come first because of shitty JSDOM.
|
||||
// See https://github.com/facebook/jest/issues/1224
|
||||
|
||||
@@ -1,307 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import { finder } from '@medv/finder'
|
||||
|
||||
import {
|
||||
uiActions,
|
||||
isDarkMode,
|
||||
controlMessages,
|
||||
eventsToRecord,
|
||||
overlaySelectors,
|
||||
} from '@/services/constants'
|
||||
|
||||
import UIController from './screenshot-controller'
|
||||
import OverlayApp from './OverlayApp.vue'
|
||||
import Selector from './Selector.vue'
|
||||
|
||||
export default class EventRecorder {
|
||||
constructor() {
|
||||
this._boundedMessageListener = null
|
||||
this._eventLog = []
|
||||
this._previousEvent = null
|
||||
this._dataAttribute = null
|
||||
this._darkMode = false
|
||||
this._uiController = null
|
||||
this._screenShotMode = false
|
||||
this._isTopFrame = window.location === window.parent.location
|
||||
this._isRecordingClicks = true
|
||||
|
||||
this.mouseOverEvent = null
|
||||
this.overlayApp = null
|
||||
this.overlayContainer = null
|
||||
|
||||
this.selectorContainer = null
|
||||
this.selectorApp = null
|
||||
}
|
||||
|
||||
boot() {
|
||||
// We need to check the existence of chrome for testing purposes
|
||||
if (chrome.storage && chrome.storage.local) {
|
||||
chrome.storage.local.get(['options'], ({ options }) => {
|
||||
this._darkMode = options && options.extension ? options.extension.darkMode : isDarkMode()
|
||||
|
||||
const { dataAttribute } = options ? options.code : {}
|
||||
if (dataAttribute) {
|
||||
this._dataAttribute = dataAttribute
|
||||
}
|
||||
this._initializeRecorder()
|
||||
})
|
||||
} else {
|
||||
this._initializeRecorder()
|
||||
}
|
||||
|
||||
chrome.storage.onChanged.addListener(({ options = null }) => {
|
||||
if (options) {
|
||||
this._darkMode = options.newValue.extension.darkMode
|
||||
this.overlayApp.darkMode = options.newValue.extension.darkMode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_initializeRecorder() {
|
||||
const events = Object.values(eventsToRecord)
|
||||
if (!window.pptRecorderAddedControlListeners) {
|
||||
this._addAllListeners(events)
|
||||
this._boundedMessageListener =
|
||||
this._boundedMessageListener || this._handleBackgroundMessage.bind(this)
|
||||
chrome.runtime.onMessage.addListener(this._boundedMessageListener)
|
||||
window.pptRecorderAddedControlListeners = true
|
||||
}
|
||||
|
||||
if (
|
||||
!window.document.pptRecorderAddedControlListeners &&
|
||||
chrome.runtime &&
|
||||
chrome.runtime.onMessage
|
||||
) {
|
||||
window.document.pptRecorderAddedControlListeners = true
|
||||
}
|
||||
|
||||
if (this._isTopFrame) {
|
||||
this._sendMessage({ control: controlMessages.EVENT_RECORDER_STARTED })
|
||||
this._sendMessage({
|
||||
control: controlMessages.GET_CURRENT_URL,
|
||||
href: window.location.href,
|
||||
})
|
||||
this._sendMessage({
|
||||
control: controlMessages.GET_VIEWPORT_SIZE,
|
||||
coordinates: { width: window.innerWidth, height: window.innerHeight },
|
||||
})
|
||||
console.debug('Headless Recorder in-page EventRecorder started')
|
||||
}
|
||||
}
|
||||
|
||||
_handleBackgroundMessage(msg) {
|
||||
console.debug('content-script: message from background', msg)
|
||||
if (msg && msg.action) {
|
||||
switch (msg.action) {
|
||||
case uiActions.TOGGLE_SCREENSHOT_MODE:
|
||||
this._handleScreenshotMode(false)
|
||||
break
|
||||
|
||||
case uiActions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
|
||||
this._handleScreenshotMode(true)
|
||||
break
|
||||
|
||||
case uiActions.CLOSE_SCREENSHOT_MODE:
|
||||
this._cancelScreenshotMode()
|
||||
break
|
||||
|
||||
case uiActions.TOGGLE_OVERLAY:
|
||||
msg.value ? this._attachOverlay() : this._dettachOverlay()
|
||||
break
|
||||
|
||||
case uiActions.PAUSE:
|
||||
this.overlayApp.isPaused = true
|
||||
break
|
||||
|
||||
case uiActions.UN_PAUSE:
|
||||
this.overlayApp.isPaused = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_attachOverlay() {
|
||||
console.debug('attach overlay')
|
||||
|
||||
if (this.overlayContainer) {
|
||||
return
|
||||
}
|
||||
|
||||
this.overlayContainer = document.createElement('div')
|
||||
this.overlayContainer.id = overlaySelectors.OVERLAY_ID
|
||||
document.body.appendChild(this.overlayContainer)
|
||||
|
||||
this.selectorContainer = document.createElement('div')
|
||||
this.selectorContainer.id = overlaySelectors.OVERLAY_ID + 1
|
||||
document.body.appendChild(this.selectorContainer)
|
||||
|
||||
this.selectorApp = createApp(Selector).mount('#' + overlaySelectors.OVERLAY_ID + 1)
|
||||
this.overlayApp = createApp(OverlayApp).mount('#' + overlaySelectors.OVERLAY_ID)
|
||||
|
||||
this.overlayApp.darkMode = this._darkMode
|
||||
|
||||
this.mouseOverEvent = e => {
|
||||
const selector = this._getSelector(e)
|
||||
this.overlayApp.currentSelector = selector.includes('#' + overlaySelectors.OVERLAY_ID)
|
||||
? ''
|
||||
: selector
|
||||
|
||||
if (
|
||||
this.overlayApp.currentSelector &&
|
||||
(!this._screenShotMode || this._uiController._isClipped)
|
||||
) {
|
||||
this.selectorApp.move(e, [overlaySelectors.OVERLAY_ID])
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener('mouseover', this.mouseOverEvent)
|
||||
}
|
||||
|
||||
_dettachOverlay() {
|
||||
console.debug('dettach overlay')
|
||||
document.body.removeChild(this.overlayContainer)
|
||||
document.body.removeChild(this.selectorContainer)
|
||||
|
||||
this.overlayContainer = null
|
||||
this.overlayApp = null
|
||||
this.selectorContainer = null
|
||||
this.selectorApp = null
|
||||
|
||||
window.document.removeEventListener('mouseover', this.mouseOverEvent)
|
||||
}
|
||||
|
||||
_addAllListeners(events) {
|
||||
const boundedRecordEvent = this._recordEvent.bind(this)
|
||||
events.forEach(type => window.addEventListener(type, boundedRecordEvent, true))
|
||||
}
|
||||
|
||||
_sendMessage(msg) {
|
||||
// filter messages based on enabled / disabled features
|
||||
if (msg.action === 'click' && !this._isRecordingClicks) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// poor man's way of detecting whether this script was injected by an actual extension, or is loaded for
|
||||
// testing purposes
|
||||
if (chrome.runtime && chrome.runtime.onMessage) {
|
||||
chrome.runtime.sendMessage(msg)
|
||||
} else {
|
||||
this._eventLog.push(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('caught error', err)
|
||||
}
|
||||
}
|
||||
|
||||
_recordEvent(e) {
|
||||
if (this._previousEvent && this._previousEvent.timeStamp === e.timeStamp) {
|
||||
return
|
||||
}
|
||||
this._previousEvent = e
|
||||
|
||||
// we explicitly catch any errors and swallow them, as none node-type events are also ingested.
|
||||
// for these events we cannot generate selectors, which is OK
|
||||
try {
|
||||
const selector = this._getSelector(e)
|
||||
|
||||
if (selector.includes('#' + overlaySelectors.OVERLAY_ID)) {
|
||||
return
|
||||
}
|
||||
|
||||
// HERE
|
||||
this.overlayApp.showBorder = true
|
||||
|
||||
this._sendMessage({
|
||||
selector,
|
||||
value: e.target.value,
|
||||
tagName: e.target.tagName,
|
||||
action: e.type,
|
||||
keyCode: e.keyCode ? e.keyCode : null,
|
||||
href: e.target.href ? e.target.href : null,
|
||||
coordinates: EventRecorder._getCoordinates(e),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
_getEventLog() {
|
||||
return this._eventLog
|
||||
}
|
||||
|
||||
_clearEventLog() {
|
||||
this._eventLog = []
|
||||
}
|
||||
|
||||
_handleScreenshotMode(isClipped) {
|
||||
this._disableClickRecording()
|
||||
this._uiController = new UIController({ isClipped })
|
||||
this._screenShotMode = !this._screenShotMode
|
||||
document.body.classList.add(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
|
||||
console.debug('screenshot mode:', this._screenShotMode)
|
||||
|
||||
this._screenShotMode
|
||||
? this._uiController.startScreenshotMode()
|
||||
: this._uiController.stopScreenshotMode()
|
||||
|
||||
this._uiController.on('click', event => {
|
||||
this._screenShotMode = false
|
||||
this.overlayApp.isScreenShotMode = false
|
||||
document.body.classList.add(overlaySelectors.FLASH_CLASS)
|
||||
document.body.classList.remove(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
setTimeout(() => document.body.classList.remove(overlaySelectors.FLASH_CLASS), 1000)
|
||||
|
||||
this._sendMessage({ control: controlMessages.GET_SCREENSHOT, value: event.clip })
|
||||
this._enableClickRecording()
|
||||
})
|
||||
}
|
||||
|
||||
_cancelScreenshotMode() {
|
||||
if (!this._screenShotMode) {
|
||||
return
|
||||
}
|
||||
this._screenShotMode = false
|
||||
this.overlayApp.isScreenShotMode = false
|
||||
document.body.classList.remove(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
this._uiController.stopScreenshotMode()
|
||||
this._enableClickRecording()
|
||||
}
|
||||
|
||||
_disableClickRecording() {
|
||||
this._isRecordingClicks = false
|
||||
}
|
||||
|
||||
_enableClickRecording() {
|
||||
this._isRecordingClicks = true
|
||||
}
|
||||
|
||||
_getSelector(e) {
|
||||
if (this._dataAttribute && e.target.getAttribute(this._dataAttribute)) {
|
||||
return `[${this._dataAttribute}="${e.target.getAttribute(this._dataAttribute)}"]`
|
||||
}
|
||||
|
||||
if (e.target.id) {
|
||||
return `#${e.target.id}`
|
||||
}
|
||||
|
||||
const selector = finder(e.target, {
|
||||
seedMinLength: 5,
|
||||
optimizedMinLength: e.target.id ? 2 : 10,
|
||||
attr: name => name === this._dataAttribute,
|
||||
})
|
||||
|
||||
return selector
|
||||
}
|
||||
|
||||
static _getCoordinates(evt) {
|
||||
const eventsWithCoordinates = {
|
||||
mouseup: true,
|
||||
mousedown: true,
|
||||
mousemove: true,
|
||||
mouseover: true,
|
||||
}
|
||||
return eventsWithCoordinates[evt.type] ? { x: evt.clientX, y: evt.clientY } : null
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,14 @@
|
||||
import EventRecorder from './event-recorder'
|
||||
window.eventRecorder = new EventRecorder()
|
||||
window.eventRecorder.boot()
|
||||
import store from '@/store'
|
||||
|
||||
import Overlay from '@/services/overlay'
|
||||
import Recorder from '@/services/recorder'
|
||||
import Shooter from '@/services/shooter'
|
||||
import Controller from '@/services/controller'
|
||||
|
||||
window.headlessRecorder = new Controller({
|
||||
shooter: new Shooter(),
|
||||
overlay: new Overlay({ store }),
|
||||
recorder: new Recorder({ store }),
|
||||
})
|
||||
|
||||
window.headlessRecorder.init()
|
||||
|
||||
@@ -62,6 +62,10 @@
|
||||
"icons/dark/screen.svg",
|
||||
"icons/light/screen.svg",
|
||||
"icons/dark/clip.svg",
|
||||
"icons/light/clip.svg"
|
||||
"icons/light/clip.svg",
|
||||
"icons/dark/sync.svg",
|
||||
"icons/light/sync.svg",
|
||||
"icons/dark/duplicate.svg",
|
||||
"icons/light/duplicate.svg"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
|
||||
<script>
|
||||
import { version } from '../../package.json'
|
||||
import { defaults as code } from '@/services/code-generator'
|
||||
import { defaults as code } from '@/services/code-generator/code-generator'
|
||||
import { isDarkMode } from '@/services/constants'
|
||||
|
||||
import Button from '@/components/Button'
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<Button class="mr-2 w-34" @click="copyCode" v-show="code">
|
||||
<img
|
||||
v-show="!isCopying"
|
||||
src="@/assets/icons/duplicate.svg"
|
||||
src="/icons/dark/duplicate.svg"
|
||||
class="mr-1"
|
||||
alt="copy code to clipboard"
|
||||
/>
|
||||
@@ -52,8 +52,8 @@
|
||||
|
||||
<script>
|
||||
import { uiActions, isDarkMode } from '@/services/constants'
|
||||
import PuppeteerCodeGenerator from '@/services/puppeteer-code-generator'
|
||||
import PlaywrightCodeGenerator from '@/services/playwright-code-generator'
|
||||
import PuppeteerCodeGenerator from '@/services/code-generator/puppeteer-code-generator'
|
||||
import PlaywrightCodeGenerator from '@/services/code-generator/playwright-code-generator'
|
||||
|
||||
import Footer from '@/components/Footer.vue'
|
||||
import Header from '@/components/Header.vue'
|
||||
|
||||
0
src/services/browser.js
Normal file
@@ -1,5 +1,5 @@
|
||||
import { headlessActions, eventsToRecord } from '@/services/constants'
|
||||
import Block from '@/services/block'
|
||||
import Block from '@/services/code-generator/block'
|
||||
import { headlessActions, eventsToRecord } from '@/services/code-generator/constants'
|
||||
|
||||
export const defaults = {
|
||||
wrapAsync: true,
|
||||
25
src/services/code-generator/constants.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export const headlessActions = {
|
||||
GOTO: 'GOTO',
|
||||
VIEWPORT: 'VIEWPORT',
|
||||
WAITFORSELECTOR: 'WAITFORSELECTOR',
|
||||
NAVIGATION: 'NAVIGATION',
|
||||
NAVIGATION_PROMISE: 'NAVIGATION_PROMISE',
|
||||
FRAME_SET: 'FRAME_SET',
|
||||
SCREENSHOT: 'SCREENSHOT',
|
||||
}
|
||||
|
||||
export const eventsToRecord = {
|
||||
CLICK: 'click',
|
||||
DBLCLICK: 'dblclick',
|
||||
CHANGE: 'change',
|
||||
KEYDOWN: 'keydown',
|
||||
SELECT: 'select',
|
||||
SUBMIT: 'submit',
|
||||
LOAD: 'load',
|
||||
UNLOAD: 'unload',
|
||||
}
|
||||
|
||||
export const headlessTypes = {
|
||||
PUPPETEER: 'puppeteer',
|
||||
PLAYWRIGHT: 'playwright',
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import Block from '@/services/block'
|
||||
import CodeGenerator from '@/services/code-generator'
|
||||
import { headlessActions } from '@/services/constants'
|
||||
import Block from '@/services/code-generator/block'
|
||||
import { headlessActions } from '@/services/code-generator/constants'
|
||||
import CodeGenerator from '@/services/code-generator/code-generator'
|
||||
|
||||
const importPlaywright = `const { chromium } = require('playwright');\n`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { headlessActions } from '@/services/constants'
|
||||
import Block from '@/services/block'
|
||||
import CodeGenerator from '@/services/code-generator'
|
||||
import Block from '@/services/code-generator/block'
|
||||
import { headlessActions } from '@/services/code-generator/constants'
|
||||
import CodeGenerator from '@/services/code-generator/code-generator'
|
||||
|
||||
const importPuppeteer = `const puppeteer = require('puppeteer');\n`
|
||||
|
||||
95
src/services/controller/index.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import { wathEffect } from 'vue'
|
||||
import { uiActions, isDarkMode, controlMessages } from '@/services/constants'
|
||||
|
||||
import store from '@/store'
|
||||
import storage from '@/services/storage'
|
||||
import Shooter from '@/services/shooter'
|
||||
|
||||
export default class HeadlessController {
|
||||
constructor({ overlay, recorder }) {
|
||||
this.backgroundListener = null
|
||||
|
||||
this.shooter = null
|
||||
this.overlay = overlay
|
||||
this.recorder = recorder
|
||||
}
|
||||
|
||||
async init() {
|
||||
const { options } = await storage.get(['options'])
|
||||
|
||||
const darkMode = options && options.extension ? options.extension.darkMode : isDarkMode()
|
||||
const { dataAttribute } = options ? options.code : {}
|
||||
|
||||
store.commit('setDarkMode', darkMode)
|
||||
store.commit('setDataAttribute', dataAttribute)
|
||||
|
||||
this.recorder.init(() => this.listenBackgroundMessages())
|
||||
|
||||
this.overlay.init()
|
||||
}
|
||||
|
||||
listenBackgroundMessages() {
|
||||
this.backgroundListener = this.backgroundListener || this.handleBackgroundMessages.bind(this)
|
||||
chrome.runtime.onMessage.addListener(this.backgroundListener)
|
||||
}
|
||||
|
||||
handleBackgroundMessages(msg) {
|
||||
if (msg && msg.action) {
|
||||
switch (msg.action) {
|
||||
case uiActions.TOGGLE_SCREENSHOT_MODE:
|
||||
this.handleScreenshot(false)
|
||||
break
|
||||
|
||||
case uiActions.TOGGLE_SCREENSHOT_CLIPPED_MODE:
|
||||
this.handleScreenshot(true)
|
||||
break
|
||||
|
||||
case uiActions.CLOSE_SCREENSHOT_MODE:
|
||||
this.cancelScreenshot()
|
||||
break
|
||||
|
||||
case uiActions.TOGGLE_OVERLAY:
|
||||
msg.value ? this.overlay.mount() : this.overlay.unmount()
|
||||
break
|
||||
|
||||
case uiActions.PAUSE:
|
||||
store.commit('togglePause')
|
||||
break
|
||||
|
||||
case uiActions.UN_PAUSE:
|
||||
store.commit('togglePause')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleScreenshot(isClipped) {
|
||||
this.recorder.disableClickRecording()
|
||||
this.shooter = new Shooter({ isClipped })
|
||||
|
||||
this.shooter.addCameraIcon()
|
||||
|
||||
store.state.screenshotMode
|
||||
? this.shooter.startScreenshotMode()
|
||||
: this.shooter.stopScreenshotMode()
|
||||
|
||||
this.shooter.on('click', ({ clip }) => {
|
||||
store.commit('stopScreenshotMode')
|
||||
|
||||
this.shooter.showScreenshotEffect()
|
||||
this.recorder._sendMessage({ control: controlMessages.GET_SCREENSHOT, value: clip })
|
||||
this.recorder.enableClickRecording()
|
||||
})
|
||||
}
|
||||
|
||||
cancelScreenshot() {
|
||||
if (!store.state.screenshotMode) {
|
||||
return
|
||||
}
|
||||
|
||||
store.commit('stopScreenshotMode')
|
||||
this.shooter.removeCameraIcon()
|
||||
this.shooter.stopScreenshotMode()
|
||||
this.recorder.enableClickRecording()
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,58 @@
|
||||
<template>
|
||||
<nav v-show="!isScreenShotMode" :class="{ recorded: showBorder, dark: darkMode }">
|
||||
<div class="rec" v-show="!isPaused">
|
||||
<span class="dot"></span>
|
||||
REC
|
||||
</div>
|
||||
<button title="stop" @click="stop" v-tippy="{ content: 'Stop Recording' }">
|
||||
<div class="stop"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
title="pause"
|
||||
@click="pause"
|
||||
v-tippy="{ content: isPaused ? 'Resume Recording' : 'Pause Recording' }"
|
||||
>
|
||||
<img v-show="isPaused" width="27" height="27" :src="getIcon('play')" alt="play" />
|
||||
<img v-show="!isPaused" width="27" height="27" :src="getIcon('pause')" alt="pause" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button
|
||||
class="btn-big"
|
||||
@click.prevent="fullScreenshot"
|
||||
v-tippy="{ content: 'Full Screenshot' }"
|
||||
>
|
||||
<img width="27" height="27" :src="getIcon('screen')" alt="full page sreenshot" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-big"
|
||||
@click.prevent="clippedScreenshot"
|
||||
v-tippy="{ content: 'Clipped Screenshot' }"
|
||||
>
|
||||
<img width="27" height="27" :src="getIcon('clip')" alt="clipped sreenshot" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<span>
|
||||
{{ currentSelector }}
|
||||
</span>
|
||||
<nav v-show="!screenshotMode" :class="{ recorded: hasRecorded, dark: darkMode }">
|
||||
<template v-if="isStopped">
|
||||
<div>
|
||||
<p>Recording finished!</p>
|
||||
<p>You can copy the code to clipboard right away!</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button>
|
||||
<img width="16" height="16" :src="getIcon('sync')" alt="full page sreenshot" />
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
<Button>
|
||||
<img width="16" height="16" :src="getIcon('sync')" alt="full page sreenshot" />
|
||||
Restart Recording
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="rec" v-show="!isPaused">
|
||||
<span class="dot"></span>
|
||||
REC
|
||||
</div>
|
||||
<button title="stop" @click="stop" v-tippy="{ content: 'Stop Recording' }">
|
||||
<div class="stop"></div>
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
title="pause"
|
||||
@click="pause"
|
||||
v-tippy="{ content: isPaused ? 'Resume Recording' : 'Pause Recording' }"
|
||||
>
|
||||
<img v-show="isPaused" width="27" height="27" :src="getIcon('play')" alt="play" />
|
||||
<img v-show="!isPaused" width="27" height="27" :src="getIcon('pause')" alt="pause" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<button
|
||||
class="btn-big"
|
||||
@click.prevent="fullScreenshot"
|
||||
v-tippy="{ content: 'Full Screenshot' }"
|
||||
>
|
||||
<img width="27" height="27" :src="getIcon('screen')" alt="full page sreenshot" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-big"
|
||||
@click.prevent="clippedScreenshot"
|
||||
v-tippy="{ content: 'Clipped Screenshot' }"
|
||||
>
|
||||
<img width="27" height="27" :src="getIcon('clip')" alt="clipped sreenshot" />
|
||||
</button>
|
||||
<div class="separator"></div>
|
||||
<span>
|
||||
{{ currentSelector }}
|
||||
</span>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
@@ -43,30 +61,23 @@ import { directive } from 'vue-tippy'
|
||||
import { controlMessages } from '@/services/constants'
|
||||
import 'tippy.js/dist/tippy.css'
|
||||
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
import Button from '@/components/Button'
|
||||
|
||||
export default {
|
||||
name: 'Overlay',
|
||||
|
||||
components: { Button },
|
||||
directives: { tippy: directive },
|
||||
|
||||
data() {
|
||||
return {
|
||||
darkMode: false,
|
||||
showBorder: false,
|
||||
isPaused: false,
|
||||
isScreenShotMode: false,
|
||||
currentSelector: '',
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
showBorder(newVal, oldVal) {
|
||||
if (oldVal === newVal) {
|
||||
return
|
||||
}
|
||||
if (newVal) {
|
||||
setTimeout(() => (this.showBorder = false), 250)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['isPaused', 'isStopped', 'screenshotMode', 'darkMode', 'hasRecorded'])
|
||||
},
|
||||
|
||||
mounted() {
|
||||
@@ -74,6 +85,7 @@ export default {
|
||||
if (e.code !== 'Escape') {
|
||||
return
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
control: controlMessages.OVERLAY_ABORT_SCREENSHOT,
|
||||
})
|
||||
@@ -82,7 +94,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
stop() {
|
||||
chrome.runtime.sendMessage({ control: controlMessages.OVERLAY_STOP })
|
||||
this.$store.commit('stop')
|
||||
},
|
||||
|
||||
getIcon(icon) {
|
||||
@@ -90,22 +102,15 @@ export default {
|
||||
},
|
||||
|
||||
pause() {
|
||||
this.isPaused = !this.isPaused
|
||||
chrome.runtime.sendMessage({ control: controlMessages.OVERLAY_PAUSE })
|
||||
this.$store.commit('pause')
|
||||
},
|
||||
|
||||
fullScreenshot() {
|
||||
this.isScreenShotMode = true
|
||||
chrome.runtime.sendMessage({
|
||||
control: controlMessages.OVERLAY_FULL_SCREENSHOT,
|
||||
})
|
||||
this.$store.commit('startScreenshotMode', false)
|
||||
},
|
||||
|
||||
clippedScreenshot() {
|
||||
this.isScreenShotMode = true
|
||||
chrome.runtime.sendMessage({
|
||||
control: controlMessages.OVERLAY_CLIPPED_SCREENSHOT,
|
||||
})
|
||||
this.$store.commit('startScreenshotMode', true)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -16,10 +16,6 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
// mounted() {
|
||||
// document.body.addEventListener('click', this._boundeMouseUp, false)
|
||||
// },
|
||||
|
||||
methods: {
|
||||
move(e, skippedSelectors = []) {
|
||||
if (this.element === e.target) {
|
||||
77
src/services/overlay/index.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import getSelector from '@/services/selector'
|
||||
import { overlaySelectors } from '@/services/constants'
|
||||
|
||||
import Selector from './Selector.vue'
|
||||
import OverlayApp from './Overlay.vue'
|
||||
|
||||
export default class Overlay {
|
||||
constructor({ store }) {
|
||||
this.overlayApp = null
|
||||
this.selectorApp = null
|
||||
|
||||
this.overlayContainer = null
|
||||
this.selectorContainer = null
|
||||
|
||||
this.mouseOverEvent = null
|
||||
|
||||
this.store = store
|
||||
}
|
||||
|
||||
init() {
|
||||
// TODO: look for dark mode (and default settings)
|
||||
}
|
||||
|
||||
mount() {
|
||||
if (this.overlayContainer) {
|
||||
return
|
||||
}
|
||||
|
||||
this.overlayContainer = document.createElement('div')
|
||||
this.overlayContainer.id = overlaySelectors.OVERLAY_ID
|
||||
document.body.appendChild(this.overlayContainer)
|
||||
|
||||
this.selectorContainer = document.createElement('div')
|
||||
this.selectorContainer.id = overlaySelectors.OVERLAY_ID + 1
|
||||
document.body.appendChild(this.selectorContainer)
|
||||
|
||||
this.selectorApp = createApp(Selector)
|
||||
.use(this.store)
|
||||
.mount('#' + overlaySelectors.OVERLAY_ID + 1)
|
||||
|
||||
this.overlayApp = createApp(OverlayApp)
|
||||
.use(this.store)
|
||||
.mount('#' + overlaySelectors.OVERLAY_ID)
|
||||
|
||||
this.overlayApp.darkMode = this._darkMode
|
||||
|
||||
this.mouseOverEvent = e => {
|
||||
const selector = getSelector(e)
|
||||
this.overlayApp.currentSelector = selector.includes('#' + overlaySelectors.OVERLAY_ID)
|
||||
? ''
|
||||
: selector
|
||||
|
||||
if (
|
||||
this.overlayApp.currentSelector &&
|
||||
(!this._screenShotMode || this._uiController._isClipped)
|
||||
) {
|
||||
this.selectorApp.move(e, [overlaySelectors.OVERLAY_ID])
|
||||
}
|
||||
}
|
||||
|
||||
window.document.addEventListener('mouseover', this.mouseOverEvent)
|
||||
}
|
||||
|
||||
unmount() {
|
||||
document.body.removeChild(this.overlayContainer)
|
||||
document.body.removeChild(this.selectorContainer)
|
||||
|
||||
this.overlayContainer = null
|
||||
this.overlayApp = null
|
||||
this.selectorContainer = null
|
||||
this.selectorApp = null
|
||||
|
||||
window.document.removeEventListener('mouseover', this.mouseOverEvent)
|
||||
}
|
||||
}
|
||||
119
src/services/recorder/index.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import getSelector from '@/services/selector'
|
||||
import { controlMessages, eventsToRecord, overlaySelectors } from '@/services/constants'
|
||||
|
||||
export default class Recorder {
|
||||
constructor({ store }) {
|
||||
// this._boundedMessageListener = null
|
||||
this._eventLog = []
|
||||
this._previousEvent = null
|
||||
|
||||
this._isTopFrame = window.location === window.parent.location
|
||||
this._isRecordingClicks = true
|
||||
|
||||
this.store = store
|
||||
}
|
||||
|
||||
init(cb) {
|
||||
const events = Object.values(eventsToRecord)
|
||||
|
||||
if (!window.pptRecorderAddedControlListeners) {
|
||||
this._addAllListeners(events)
|
||||
cb && cb()
|
||||
window.pptRecorderAddedControlListeners = true
|
||||
}
|
||||
|
||||
if (!window.document.pptRecorderAddedControlListeners && chrome.runtime?.onMessage) {
|
||||
window.document.pptRecorderAddedControlListeners = true
|
||||
}
|
||||
|
||||
if (this._isTopFrame) {
|
||||
this._sendMessage({ control: controlMessages.EVENT_RECORDER_STARTED })
|
||||
this._sendMessage({ control: controlMessages.GET_CURRENT_URL, href: window.location.href })
|
||||
this._sendMessage({
|
||||
control: controlMessages.GET_VIEWPORT_SIZE,
|
||||
coordinates: { width: window.innerWidth, height: window.innerHeight },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_addAllListeners(events) {
|
||||
const boundedRecordEvent = this._recordEvent.bind(this)
|
||||
events.forEach(type => window.addEventListener(type, boundedRecordEvent, true))
|
||||
}
|
||||
|
||||
_sendMessage(msg) {
|
||||
// filter messages based on enabled / disabled features
|
||||
if (msg.action === 'click' && !this._isRecordingClicks) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// poor man's way of detecting whether this script was injected by an actual extension, or is loaded for
|
||||
// testing purposes
|
||||
if (chrome.runtime && chrome.runtime.onMessage) {
|
||||
chrome.runtime.sendMessage(msg)
|
||||
} else {
|
||||
this._eventLog.push(msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug('caught error', err)
|
||||
}
|
||||
}
|
||||
|
||||
_recordEvent(e) {
|
||||
if (this._previousEvent && this._previousEvent.timeStamp === e.timeStamp) {
|
||||
return
|
||||
}
|
||||
this._previousEvent = e
|
||||
|
||||
// we explicitly catch any errors and swallow them, as none node-type events are also ingested.
|
||||
// for these events we cannot generate selectors, which is OK
|
||||
try {
|
||||
const selector = getSelector(e, { dataAttribute: this.store.state.dataAttribute })
|
||||
|
||||
if (selector.includes('#' + overlaySelectors.OVERLAY_ID)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.store.commit('showRecorded')
|
||||
|
||||
this._sendMessage({
|
||||
selector,
|
||||
value: e.target.value,
|
||||
tagName: e.target.tagName,
|
||||
action: e.type,
|
||||
keyCode: e.keyCode ? e.keyCode : null,
|
||||
href: e.target.href ? e.target.href : null,
|
||||
coordinates: Recorder._getCoordinates(e),
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
_getEventLog() {
|
||||
return this._eventLog
|
||||
}
|
||||
|
||||
_clearEventLog() {
|
||||
this._eventLog = []
|
||||
}
|
||||
|
||||
disableClickRecording() {
|
||||
this._isRecordingClicks = false
|
||||
}
|
||||
|
||||
enableClickRecording() {
|
||||
this._isRecordingClicks = true
|
||||
}
|
||||
|
||||
static _getCoordinates(evt) {
|
||||
const eventsWithCoordinates = {
|
||||
mouseup: true,
|
||||
mousedown: true,
|
||||
mousemove: true,
|
||||
mouseover: true,
|
||||
}
|
||||
return eventsWithCoordinates[evt.type] ? { x: evt.clientX, y: evt.clientY } : null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { finder } from '@medv/finder'
|
||||
|
||||
export default function selector(e, { dataAttribute } = {}) {
|
||||
if (dataAttribute && e.target.getAttribute(dataAttribute)) {
|
||||
return `[${dataAttribute}="${e.target.getAttribute(dataAttribute)}"]`
|
||||
}
|
||||
|
||||
if (e.target.id) {
|
||||
return `#${e.target.id}`
|
||||
}
|
||||
|
||||
return finder(e.target, {
|
||||
seedMinLength: 5,
|
||||
optimizedMinLength: e.target.id ? 2 : 10,
|
||||
attr: name => name === dataAttribute,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,28 +1,32 @@
|
||||
import EventEmitter from 'events'
|
||||
import { overlaySelectors } from '@/services/constants'
|
||||
|
||||
const BORDER_THICKNESS = 2
|
||||
|
||||
const defaults = {
|
||||
isClipped: false,
|
||||
}
|
||||
|
||||
class ScreenshotController extends EventEmitter {
|
||||
constructor(options) {
|
||||
options = Object.assign({}, defaults, options)
|
||||
|
||||
class Shooter extends EventEmitter {
|
||||
constructor() {
|
||||
super()
|
||||
this._overlay = null
|
||||
this._selector = null
|
||||
this._element = null
|
||||
this._dimensions = {}
|
||||
this._isClipped = options.isClipped
|
||||
this._isClipped = false
|
||||
|
||||
this._boundeMouseMove = this._mousemove.bind(this)
|
||||
this._boundeMouseUp = this._mouseup.bind(this)
|
||||
this._boundeMouseMove = this.mousemove.bind(this)
|
||||
this._boundeMouseUp = this.mouseup.bind(this)
|
||||
}
|
||||
|
||||
// init({ isClipped = false }) {
|
||||
// this._overlay = null
|
||||
// this._selector = null
|
||||
// this._element = null
|
||||
// this._dimensions = {}
|
||||
// this._isClipped = isClipped
|
||||
|
||||
// this._boundeMouseMove = this.mousemove.bind(this)
|
||||
// this._boundeMouseUp = this.mouseup.bind(this)
|
||||
// }
|
||||
|
||||
startScreenshotMode() {
|
||||
console.debug('ScreenshotController:show')
|
||||
if (!this._overlay) {
|
||||
this._overlay = document.createElement('div')
|
||||
this._overlay.className = 'headlessRecorderOverlay'
|
||||
@@ -51,7 +55,6 @@ class ScreenshotController extends EventEmitter {
|
||||
}
|
||||
|
||||
stopScreenshotMode() {
|
||||
console.debug('ScreenshotController:hide')
|
||||
if (this._overlay) {
|
||||
document.body.removeChild(this._overlay)
|
||||
}
|
||||
@@ -59,7 +62,21 @@ class ScreenshotController extends EventEmitter {
|
||||
this._dimensions = {}
|
||||
}
|
||||
|
||||
_mousemove(e) {
|
||||
showScreenshotEffect() {
|
||||
document.body.classList.add(overlaySelectors.FLASH_CLASS)
|
||||
document.body.classList.remove(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
setTimeout(() => document.body.classList.remove(overlaySelectors.FLASH_CLASS), 1000)
|
||||
}
|
||||
|
||||
addCameraIcon() {
|
||||
document.body.classList.add(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
}
|
||||
|
||||
removeCameraIcon() {
|
||||
document.body.classList.remove(overlaySelectors.CURSOR_CAMERA_CLASS)
|
||||
}
|
||||
|
||||
mousemove(e) {
|
||||
if (this._element !== e.target) {
|
||||
this._element = e.target
|
||||
|
||||
@@ -84,9 +101,10 @@ class ScreenshotController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
_mouseup(e) {
|
||||
|
||||
mouseup(e) {
|
||||
setTimeout(() => {
|
||||
this._cleanup()
|
||||
this.cleanup()
|
||||
|
||||
let clip = null
|
||||
|
||||
@@ -103,7 +121,7 @@ class ScreenshotController extends EventEmitter {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
_cleanup() {
|
||||
cleanup() {
|
||||
document.body.removeEventListener('mousemove', this._boundeMouseMove, false)
|
||||
document.body.removeEventListener('mouseup', this._boundeMouseUp, false)
|
||||
|
||||
@@ -112,4 +130,4 @@ class ScreenshotController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
export default ScreenshotController
|
||||
export default Shooter
|
||||
11
src/services/storage.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
get(props) {
|
||||
if (!chrome.storage || !chrome.storage.local) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
chrome.storage.local.get(props, props => resolve(props))
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { ref, watchEffect } from 'vue'
|
||||
|
||||
const props = {
|
||||
CONTROLS: 'controls',
|
||||
CODE: 'code',
|
||||
OPTIONS: 'options',
|
||||
CODE_P: 'codeForPlaywright',
|
||||
RECORDING: 'recording',
|
||||
}
|
||||
|
||||
export default class Store {
|
||||
constructor(context = '') {
|
||||
// TODO: Load values from local storage
|
||||
|
||||
this.controls = ref({
|
||||
isRecording: false,
|
||||
isPaused: false,
|
||||
})
|
||||
|
||||
this.isRecording = ref(false)
|
||||
this.isPaused = ref(true)
|
||||
|
||||
this.recording = ref([])
|
||||
|
||||
this.options = ref({})
|
||||
this.context = context
|
||||
}
|
||||
|
||||
get(props) {
|
||||
return Promise(resolve => chrome.storage.local.get(props, store => resolve(store)))
|
||||
}
|
||||
|
||||
save(store) {
|
||||
return new Promise(resolve => chrome.storage.local.set(store, res => resolve(res)))
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this.get(Object.keys(props))
|
||||
}
|
||||
|
||||
sync() {
|
||||
watchEffect(() => {
|
||||
this.save(this.isRecording)
|
||||
})
|
||||
|
||||
chrome.storage.onChanged.addListener(changes => {
|
||||
Object.keys(changes).forEach(key => {
|
||||
if (this[key] !== undefined && this[key] !== null) {
|
||||
this[key].value = changes[key].newValue
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onChange(cb) {
|
||||
chrome.storage.onChanged.addListener(changes => {
|
||||
cb.call(changes)
|
||||
})
|
||||
}
|
||||
|
||||
useStore() {
|
||||
return {
|
||||
controls: this.controls,
|
||||
options: this.options,
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/store/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
import { controlMessages } from '@/services/constants'
|
||||
|
||||
const store = createStore({
|
||||
state() {
|
||||
return {
|
||||
isPaused: false,
|
||||
isStopped: false,
|
||||
darkMode: false,
|
||||
screenshotMode: false,
|
||||
hasRecorded: false,
|
||||
|
||||
dataAttribute: '',
|
||||
|
||||
takeScreenshot: false,
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
showRecorded(state) {
|
||||
state.hasRecorded = true
|
||||
setTimeout(() => (state.hasRecorded = false), 250)
|
||||
},
|
||||
|
||||
takeScreenshot(state) {
|
||||
state.takeScreenshot = true
|
||||
},
|
||||
|
||||
setDataAttribute(state, value) {
|
||||
state.dataAttribute = value
|
||||
},
|
||||
|
||||
setDarkMode(state, value) {
|
||||
state.darkMode = value
|
||||
},
|
||||
|
||||
pause(state) {
|
||||
state.isPaused = !state.isPaused
|
||||
chrome.runtime.sendMessage({ control: controlMessages.OVERLAY_PAUSE })
|
||||
},
|
||||
|
||||
stop(state) {
|
||||
state.isStopped = true
|
||||
chrome.runtime.sendMessage({ control: controlMessages.OVERLAY_STOP })
|
||||
},
|
||||
|
||||
toggleScreenshotMode(state) {
|
||||
console.log(state)
|
||||
state.screenshotMode = !state.screenshotMode
|
||||
},
|
||||
|
||||
startScreenshotMode(state, isClipped = false) {
|
||||
chrome.runtime.sendMessage({
|
||||
control: isClipped
|
||||
? controlMessages.OVERLAY_CLIPPED_SCREENSHOT
|
||||
: controlMessages.OVERLAY_FULL_SCREENSHOT,
|
||||
})
|
||||
state.screenshotMode = true
|
||||
},
|
||||
|
||||
stopScreenshotMode(state) {
|
||||
state.screenshotMode = false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: load state from local storage
|
||||
|
||||
chrome.storage.onChanged.addListener(({ options = null }) => {
|
||||
if (options) {
|
||||
store.commit('setDarkMode', options.newValue.extension.darkMode)
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
@@ -19,7 +19,7 @@ module.exports = {
|
||||
browserExtension: {
|
||||
componentOptions: {
|
||||
background: {
|
||||
entry: 'src/background.js',
|
||||
entry: 'src/background/index.js',
|
||||
},
|
||||
contentScripts: {
|
||||
entries: {
|
||||
|
||||