Merge pull request #55 from NBTX/master
🎨 Add theme support (and settings storage)
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
:root {
|
||||
--bg-color: #121212;
|
||||
--fg-color: #fff;
|
||||
--ac-color: #51ff0d;
|
||||
--err-color: #393939;
|
||||
}
|
||||
$responsiveWidth: 720px;
|
||||
|
||||
::selection {
|
||||
background-color: var(--ac-color);
|
||||
color: var(--bg-color);
|
||||
color: var(--act-color);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@@ -70,6 +65,20 @@ footer {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@media(max-width: $responsiveWidth){
|
||||
header {
|
||||
display: block;
|
||||
text-align: center;
|
||||
|
||||
nav {
|
||||
display: inline-flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
nav {
|
||||
a:not(:last-of-type) {
|
||||
margin-right: 15px;
|
||||
@@ -92,7 +101,7 @@ button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--ac-color);
|
||||
color: var(--bg-color);
|
||||
color: var(--act-color);
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
@@ -115,55 +124,55 @@ fieldset pre {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset.request {
|
||||
fieldset.blue {
|
||||
border-color: #57b5f9;
|
||||
}
|
||||
|
||||
fieldset.request legend {
|
||||
fieldset.blue legend {
|
||||
color: #57b5f9;
|
||||
}
|
||||
|
||||
fieldset.history {
|
||||
border-color: #9b9b9b;
|
||||
fieldset.gray {
|
||||
border-color: #9B9B9B;
|
||||
}
|
||||
|
||||
fieldset.history legend {
|
||||
color: #9b9b9b;
|
||||
fieldset.gray legend {
|
||||
color: #9B9B9B;
|
||||
}
|
||||
|
||||
fieldset.authentication {
|
||||
border-color: #b8e986;
|
||||
fieldset.green {
|
||||
border-color: #B8E986;
|
||||
}
|
||||
|
||||
fieldset.authentication legend {
|
||||
color: #b8e986;
|
||||
fieldset.green legend {
|
||||
color: #B8E986;
|
||||
}
|
||||
|
||||
fieldset.parameters {
|
||||
border-color: #50e3c2;
|
||||
fieldset.cyan {
|
||||
border-color: #50E3C2;
|
||||
}
|
||||
|
||||
fieldset.parameters legend {
|
||||
color: #50e3c2;
|
||||
fieldset.cyan legend {
|
||||
color: #50E3C2;
|
||||
}
|
||||
|
||||
fieldset.reqbody {
|
||||
border-color: #4a90e2;
|
||||
fieldset.blue-dark {
|
||||
border-color: #4A90E2;
|
||||
}
|
||||
|
||||
fieldset.reqbody legend {
|
||||
color: #4a90e2;
|
||||
fieldset.blue-dark legend {
|
||||
color: #4A90E2;
|
||||
}
|
||||
|
||||
fieldset.response {
|
||||
border-color: #c198fb;
|
||||
fieldset.purple {
|
||||
border-color: #C198FB;
|
||||
}
|
||||
|
||||
fieldset.response legend {
|
||||
color: #c198fb;
|
||||
fieldset.purple legend {
|
||||
color: #C198FB;
|
||||
}
|
||||
|
||||
.hidden .collapsible {
|
||||
.collapsible.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -176,13 +185,28 @@ pre {
|
||||
padding: 8px 16px;
|
||||
width: calc(100% - 8px);
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
background-color: var(--bg-dark-color);
|
||||
color: var(--fg-color);
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
// Force the same height, for dropdowns and regular input boxes.
|
||||
select,
|
||||
input,
|
||||
option {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: initial;
|
||||
|
||||
&, & + label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--err-color);
|
||||
}
|
||||
@@ -211,7 +235,7 @@ ol li {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: $responsiveWidth) {
|
||||
ul,
|
||||
ol {
|
||||
flex-direction: column;
|
||||
|
||||
39
assets/css/themes.scss
Normal file
39
assets/css/themes.scss
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
Main Themes:
|
||||
|
||||
- dark (default)
|
||||
- light
|
||||
*/
|
||||
|
||||
// Dark is the default theme variant.
|
||||
:root {
|
||||
--bg-dark-color: #000000;
|
||||
// Background color
|
||||
--bg-color: #121212;
|
||||
// Text color
|
||||
--fg-color: #FFF;
|
||||
|
||||
// Error color
|
||||
--err-color: #393939;
|
||||
|
||||
// Active color
|
||||
--ac-color: #51FF0D;
|
||||
// Active text color
|
||||
--act-color: #121212;
|
||||
}
|
||||
|
||||
:root.light {
|
||||
--bg-dark-color: #ffffff;
|
||||
// Background color
|
||||
--bg-color: #F6F8FA;
|
||||
// Text color
|
||||
--fg-color: #121212;
|
||||
|
||||
// Error color
|
||||
--err-color: invert(#393939, 1);
|
||||
|
||||
// Active color
|
||||
--ac-color: #51FF0D;
|
||||
// Active text color
|
||||
--act-color: #121212;
|
||||
}
|
||||
29
components/logo.vue
Normal file
29
components/logo.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<svg version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 612.001 612.001" style="enable-background:new 0 0 612.001 612.001;" xml:space="preserve">
|
||||
<defs id="defs11" />
|
||||
<g id="g3826" transform="translate(-516.40798,-163.88978)">
|
||||
<circle transform="scale(1,-1)" style="stroke-width:1.19531453" r="178.70923" cy="-501.55591" cx="822.40845" id="circle3814" />
|
||||
<g id="g3820" transform="translate(516.40798,163.89028)">
|
||||
<g id="g3818">
|
||||
<path :fill="color" id="path3816" data-old_color="#121212" class="active-path" data-original="#121212" d="M 64.601,236.822 C 64.601,394.256 192.786,612 306.001,612 412.582,612 547.4,394.256 547.4,236.822 547.4,79.388 439.322,0 306,0 172.678,0 64.601,79.388 64.601,236.822 Z m 304.12,116.415 c 29.475,-29.475 70.598,-40.195 108.552,-32.173 8.021,37.954 -2.698,79.077 -32.173,108.552 -29.475,29.475 -70.598,40.195 -108.552,32.173 -8.022,-37.955 2.698,-79.078 32.173,-108.552 z M 134.727,321.063 c 37.954,-8.021 79.077,2.698 108.552,32.173 29.475,29.475 40.195,70.598 32.173,108.552 -37.954,8.021 -79.077,-2.698 -108.552,-32.173 -29.475,-29.476 -40.194,-70.598 -32.173,-108.552 z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#path3816 {
|
||||
fill: var(--ac-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
'color': {
|
||||
type: String
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
47
components/section.vue
Normal file
47
components/section.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<fieldset :class="{ 'no-colored-frames': noFrameColors }">
|
||||
<legend @click.prevent="collapse">{{ label }} ↕</legend>
|
||||
<div class="collapsible" :class="{ hidden: collapsed }">
|
||||
<slot />
|
||||
</div>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
fieldset.no-colored-frames {
|
||||
border-color: #afafaf !important;
|
||||
}
|
||||
|
||||
fieldset.no-colored-frames legend {
|
||||
color: var(--ac-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
|
||||
computed: {
|
||||
noFrameColors () {
|
||||
return this.$store.state.postwoman.settings.DISABLE_FRAME_COLORS || false;
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
"label": {
|
||||
type: String,
|
||||
default: "Section"
|
||||
},
|
||||
"collapsed": {
|
||||
type: Boolean
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
collapse({ target }) {
|
||||
const parent = target.parentNode;
|
||||
parent.querySelector(".collapsible").classList.toggle('hidden');
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
67
components/settings/swatch.vue
Normal file
67
components/settings/swatch.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="color" :data-color="color">
|
||||
<span :style="{backgroundColor: color}" class="preview">
|
||||
<svg v-if="active" class="activeTick" width="24" height="24" xmlns="http://www.w3.org/2000/svg" fill-rule="evenodd" clip-rule="evenodd"><path d="M21 6.285l-11.16 12.733-6.84-6.018 1.319-1.49 5.341 4.686 9.865-11.196 1.475 1.285z"/></svg>
|
||||
</span>
|
||||
{{ name || color }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.color {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
|
||||
padding: 8px 15px;
|
||||
margin: 5px;
|
||||
background-color: rgba(93, 93, 93, 0.2);
|
||||
border-radius: 4px;
|
||||
|
||||
&.active {
|
||||
background-color: rgba(93, 93, 93, 0.3);
|
||||
}
|
||||
|
||||
.preview {
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 100%;
|
||||
margin-right: 10px;
|
||||
|
||||
position: relative;
|
||||
.activeTick {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
fill: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.color.vibrant {
|
||||
.preview .activeTick {
|
||||
fill: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
'color': {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
'name': {
|
||||
type: String
|
||||
},
|
||||
|
||||
'active': {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
<header>
|
||||
<div class="slide-in">
|
||||
<nuxt-link to="/">
|
||||
<h1 class="logo"><img src="~static/icons/logo.svg" alt="" style="height: 24px; margin-right: 16px">Postwoman</h1>
|
||||
<h1 class="logo"><logo alt="" style="height: 24px; margin-right: 16px"/>Postwoman</h1>
|
||||
</nuxt-link>
|
||||
<h3>Lightweight API request builder</h3>
|
||||
</div>
|
||||
@@ -11,13 +11,22 @@
|
||||
<nav>
|
||||
<nuxt-link to="/">HTTP</nuxt-link>
|
||||
<nuxt-link to="/websocket">WebSocket</nuxt-link>
|
||||
<nuxt-link to="/settings">
|
||||
<!-- Settings cog -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
|
||||
<path d="M24 14.187v-4.374c-2.148-.766-2.726-.802-3.027-1.529-.303-.729.083-1.169 1.059-3.223l-3.093-3.093c-2.026.963-2.488 1.364-3.224 1.059-.727-.302-.768-.889-1.527-3.027h-4.375c-.764 2.144-.8 2.725-1.529 3.027-.752.313-1.203-.1-3.223-1.059l-3.093 3.093c.977 2.055 1.362 2.493 1.059 3.224-.302.727-.881.764-3.027 1.528v4.375c2.139.76 2.725.8 3.027 1.528.304.734-.081 1.167-1.059 3.223l3.093 3.093c1.999-.95 2.47-1.373 3.223-1.059.728.302.764.88 1.529 3.027h4.374c.758-2.131.799-2.723 1.537-3.031.745-.308 1.186.099 3.215 1.062l3.093-3.093c-.975-2.05-1.362-2.492-1.059-3.223.3-.726.88-.763 3.027-1.528zm-4.875.764c-.577 1.394-.068 2.458.488 3.578l-1.084 1.084c-1.093-.543-2.161-1.076-3.573-.49-1.396.581-1.79 1.693-2.188 2.877h-1.534c-.398-1.185-.791-2.297-2.183-2.875-1.419-.588-2.507-.045-3.579.488l-1.083-1.084c.557-1.118 1.066-2.18.487-3.58-.579-1.391-1.691-1.784-2.876-2.182v-1.533c1.185-.398 2.297-.791 2.875-2.184.578-1.394.068-2.459-.488-3.579l1.084-1.084c1.082.538 2.162 1.077 3.58.488 1.392-.577 1.785-1.69 2.183-2.875h1.534c.398 1.185.792 2.297 2.184 2.875 1.419.588 2.506.045 3.579-.488l1.084 1.084c-.556 1.121-1.065 2.187-.488 3.58.577 1.391 1.689 1.784 2.875 2.183v1.534c-1.188.398-2.302.791-2.877 2.183zm-7.125-5.951c1.654 0 3 1.346 3 3s-1.346 3-3 3-3-1.346-3-3 1.346-3 3-3zm0-2c-2.762 0-5 2.238-5 5s2.238 5 5 5 5-2.238 5-5-2.238-5-5-5z"/>
|
||||
</svg>
|
||||
</nuxt-link>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<nuxt id="main" />
|
||||
|
||||
<footer>
|
||||
<a href="https://github.com/liyasthomas/postwoman" target="_blank"><img src="~static/icons/github.svg" alt="" style="margin-right: 16px">GitHub</a>
|
||||
<div>
|
||||
<a href="https://github.com/liyasthomas/postwoman" target="_blank"><img src="~static/icons/github.svg" alt="" style="margin-right: 16px">GitHub</a>
|
||||
</div>
|
||||
|
||||
<button id="installPWA" @click.prevent="showInstallPrompt()">
|
||||
Install PWA
|
||||
</button>
|
||||
@@ -47,16 +56,21 @@
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
header { padding-right: 0; }
|
||||
|
||||
nav {
|
||||
svg {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 8px 16px;
|
||||
fill: var(--fg-color);
|
||||
color: var(--fg-color);
|
||||
|
||||
&.nuxt-link-exact-active {
|
||||
color: black;
|
||||
color: var(--act-color);
|
||||
fill: var(--act-color);
|
||||
&:before { width: 100%; height: 100% }
|
||||
}
|
||||
|
||||
@@ -87,8 +101,13 @@
|
||||
|
||||
<script>
|
||||
import intializePwa from '../assets/js/pwa';
|
||||
import logo from "../components/logo";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
logo
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
// Once the PWA code is initialized, this holds a method
|
||||
@@ -98,6 +117,22 @@
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount () {
|
||||
// Load theme settings
|
||||
(() => {
|
||||
// Apply theme from settings.
|
||||
document.documentElement.className = this.$store.state.postwoman.settings.THEME_CLASS || '';
|
||||
|
||||
// Load theme color data from settings, or use default color.
|
||||
let color = this.$store.state.postwoman.settings.THEME_COLOR || '#51FF0D';
|
||||
let vibrant = this.$store.state.postwoman.settings.THEME_COLOR_VIBRANT;
|
||||
if(vibrant == null) vibrant = true;
|
||||
|
||||
document.documentElement.style.setProperty('--ac-color', color);
|
||||
document.documentElement.style.setProperty('--act-color', vibrant ? '#121212' : '#fff');
|
||||
})();
|
||||
},
|
||||
|
||||
mounted () {
|
||||
// Initializes the PWA code - checks if the app is installed,
|
||||
// etc.
|
||||
|
||||
@@ -86,10 +86,12 @@ export default {
|
||||
** Customize the progress-bar color
|
||||
*/
|
||||
loading: { color: '#88FB4F' },
|
||||
|
||||
/*
|
||||
** Global CSS
|
||||
*/
|
||||
css: [
|
||||
'@/assets/css/themes.scss',
|
||||
'@/assets/css/fonts.scss',
|
||||
'@/assets/css/styles.scss'
|
||||
],
|
||||
@@ -97,6 +99,7 @@ export default {
|
||||
** Plugins to load before mounting the App
|
||||
*/
|
||||
plugins: [
|
||||
{ src: '~/plugins/vuex-persist' }
|
||||
],
|
||||
/*
|
||||
** Nuxt.js dev-modules
|
||||
|
||||
19
package-lock.json
generated
19
package-lock.json
generated
@@ -4410,6 +4410,11 @@
|
||||
"locate-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"flatted": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
|
||||
"integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg=="
|
||||
},
|
||||
"flatten": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/flatten/-/flatten-1.0.2.tgz",
|
||||
@@ -5731,6 +5736,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4="
|
||||
},
|
||||
"lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||
},
|
||||
"lodash.template": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz",
|
||||
@@ -9992,6 +10002,15 @@
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.1.tgz",
|
||||
"integrity": "sha512-ER5moSbLZuNSMBFnEBVGhQ1uCBNJslH9W/Dw2W7GZN23UQA69uapP5GTT9Vm8Trc0PzBSVt6LzF3hGjmv41xcg=="
|
||||
},
|
||||
"vuex-persist": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vuex-persist/-/vuex-persist-2.0.1.tgz",
|
||||
"integrity": "sha512-V3WSBYmxcAP6ei0VVt2lxGloeWJZgk1Ao4r+iOxLMhAv+UYIK91qbjwvqk6Xr3tAi052jzSwn8aoamtuz8ArsQ==",
|
||||
"requires": {
|
||||
"flatted": "^2.0.0",
|
||||
"lodash.merge": "^4.6.1"
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.0.tgz",
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
"generate": "nuxt generate"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/pwa": "^3.0.0-0",
|
||||
"nuxt": "^2.0.0",
|
||||
"@nuxtjs/pwa": "^3.0.0-0"
|
||||
"vuex-persist": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.12.0",
|
||||
|
||||
410
pages/index.vue
410
pages/index.vue
@@ -1,196 +1,183 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
|
||||
<fieldset class="request" ref="request">
|
||||
<legend v-on:click="collapse">Request ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="method">Method</label>
|
||||
<select v-model="method">
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
<option>PUT</option>
|
||||
<option>DELETE</option>
|
||||
<option>OPTIONS</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<label for="url">URL</label>
|
||||
<input type="url" v-bind:class="{ error: urlNotValid }" v-model="url" v-on:keyup.enter="sendRequest">
|
||||
</li>
|
||||
<li>
|
||||
<label for="path">Path</label>
|
||||
<input v-model="path" v-on:keyup.enter="sendRequest">
|
||||
</li>
|
||||
<li>
|
||||
<label for="action"> </label>
|
||||
<button v-bind:class="{ disabled: urlNotValid }" name="action" @click="sendRequest">Send</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="reqbody" v-if="method === 'POST' || method === 'PUT'">
|
||||
<legend v-on:click="collapse">Request Body ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label>Content Type</label>
|
||||
<select v-model="contentType">
|
||||
<option>application/json</option>
|
||||
<option>www-form/urlencoded</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<ol v-for="(param, index) in bodyParams">
|
||||
<li>
|
||||
<label :for="'bparam'+index">Key {{index + 1}}</label>
|
||||
<input :name="'bparam'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'bvalue'+index">Value {{index + 1}}</label>
|
||||
<input :name="'bvalue'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="request"> </label>
|
||||
<button name="request" @click="removeRequestBodyParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="addrequest">Action</label>
|
||||
<button name="addrequest" @click="addRequestBodyParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="authentication hidden">
|
||||
<legend v-on:click="collapse">Authentication ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="auth">Authentication Type</label>
|
||||
<select v-model="auth">
|
||||
<option>None</option>
|
||||
<option>Basic</option>
|
||||
<option>Bearer Token</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="auth === 'Basic'">
|
||||
<li>
|
||||
<label for="http_basic_user">User</label>
|
||||
<input v-model="httpUser">
|
||||
</li>
|
||||
<li>
|
||||
<label for="http_basic_passwd">Password</label>
|
||||
<input v-model="httpPassword" type="password">
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="auth === 'Bearer Token'">
|
||||
<li>
|
||||
<label for="bearer_token">Token</label>
|
||||
<input v-model="bearerToken">
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="parameters hidden">
|
||||
<legend v-on:click="collapse">Parameters ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ol v-for="(param, index) in params">
|
||||
<li>
|
||||
<label :for="'param'+index">Key {{index + 1}}</label>
|
||||
<input :name="'param'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'value'+index">Value {{index + 1}}</label>
|
||||
<input :name="'value'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="param"> </label>
|
||||
<button name="param" @click="removeRequestParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="add">Action</label>
|
||||
<button name="add" @click="addRequestParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{queryString || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="response" id="response" ref="response">
|
||||
<legend v-on:click="collapse">Response ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="status">status</label>
|
||||
<input name="status" type="text" readonly :value="response.status || '(waiting to send request)'" :class="statusCategory ? statusCategory.className : ''" >
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-for="(value, key) in response.headers">
|
||||
<li>
|
||||
<label for="value">{{key}}</label>
|
||||
<input name="value" :value="value" readonly>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="body">response</label>
|
||||
<textarea name="body" rows="10" readonly>{{response.body || '(waiting to send request)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="history">
|
||||
<legend v-on:click="collapse">History ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<button v-bind:class="{ disabled: noHistoryToClear }" v-on:click="clearHistory">Clear History</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-for="entry in history">
|
||||
<li>
|
||||
<label for="time">Time</label>
|
||||
<input name="time" type="text" readonly :value="entry.time">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Method</label>
|
||||
<input name="name" type="text" readonly :value="entry.method">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">URL</label>
|
||||
<input name="name" type="text" readonly :value="entry.url">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Path</label>
|
||||
<input name="name" type="text" readonly :value="entry.path">
|
||||
</li>
|
||||
<li>
|
||||
<label for="delete"> </label>
|
||||
<button name="delete" @click="deleteHistory(entry)">Delete</button>
|
||||
</li>
|
||||
<li>
|
||||
<label for="use"> </label>
|
||||
<button name="use" @click="useHistory(entry)">Use</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<pw-section class="blue" label="Request" ref="request">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="method">Method</label>
|
||||
<select id="method" v-model="method">
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
<option>PUT</option>
|
||||
<option>DELETE</option>
|
||||
<option>OPTIONS</option>
|
||||
</select>
|
||||
</li>
|
||||
<li>
|
||||
<label for="url">URL</label>
|
||||
<input id="url" type="url" v-bind:class="{ error: !isValidURL }" v-model="url" v-on:keyup.enter="sendRequest">
|
||||
</li>
|
||||
<li>
|
||||
<label for="path">Path</label>
|
||||
<input id="path" v-model="path" v-on:keyup.enter="sendRequest">
|
||||
</li>
|
||||
<li>
|
||||
<label for="action"> </label>
|
||||
<button id="action" name="action" @click="sendRequest" :disabled="!isValidURL">Send</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="blue-dark" label="Request Body" v-if="method === 'POST' || method === 'PUT'">
|
||||
<ul>
|
||||
<li>
|
||||
<label>Content Type</label>
|
||||
<select v-model="contentType">
|
||||
<option>application/json</option>
|
||||
<option>www-form/urlencoded</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<ol v-for="(param, index) in bodyParams">
|
||||
<li>
|
||||
<label :for="'bparam'+index">Key {{index + 1}}</label>
|
||||
<input :name="'bparam'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'bvalue'+index">Value {{index + 1}}</label>
|
||||
<input :name="'bvalue'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="request"> </label>
|
||||
<button name="request" @click="removeRequestBodyParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="addrequest">Action</label>
|
||||
<button name="addrequest" @click="addRequestBodyParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{rawRequestBody || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="green" label="Authentication" collapsed>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="auth">Authentication Type</label>
|
||||
<select v-model="auth">
|
||||
<option>None</option>
|
||||
<option>Basic</option>
|
||||
<option>Bearer Token</option>
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="auth === 'Basic'">
|
||||
<li>
|
||||
<label for="http_basic_user">User</label>
|
||||
<input v-model="httpUser">
|
||||
</li>
|
||||
<li>
|
||||
<label for="http_basic_passwd">Password</label>
|
||||
<input v-model="httpPassword" type="password">
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-if="auth === 'Bearer Token'">
|
||||
<li>
|
||||
<label for="bearer_token">Token</label>
|
||||
<input v-model="bearerToken">
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="cyan" label="Parameters" collapsed>
|
||||
<ol v-for="(param, index) in params">
|
||||
<li>
|
||||
<label :for="'param'+index">Key {{index + 1}}</label>
|
||||
<input :name="'param'+index" v-model="param.key">
|
||||
</li>
|
||||
<li>
|
||||
<label :for="'value'+index">Value {{index + 1}}</label>
|
||||
<input :name="'value'+index" v-model="param.value">
|
||||
</li>
|
||||
<li>
|
||||
<label for="param"> </label>
|
||||
<button name="param" @click="removeRequestParam(index)">Remove</button>
|
||||
</li>
|
||||
</ol>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="add">Action</label>
|
||||
<button name="add" @click="addRequestParam">Add</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="request">Parameter List</label>
|
||||
<textarea name="request" rows="1" readonly>{{queryString || '(add at least one parameter)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="purple" label="Response" id="response" ref="response">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="status">status</label>
|
||||
<input name="status" type="text" readonly :value="response.status || '(waiting to send request)'" :class="statusCategory ? statusCategory.className : ''">
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-for="(value, key) in response.headers">
|
||||
<li>
|
||||
<label for="value">{{key}}</label>
|
||||
<input name="value" :value="value" readonly>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="body">response</label>
|
||||
<textarea name="body" rows="10" readonly>{{response.body || '(waiting to send request)'}}</textarea>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<pw-section class="gray" label="History">
|
||||
<ul>
|
||||
<li>
|
||||
<button v-bind:class="{ disabled: noHistoryToClear }" v-on:click="clearHistory">Clear History</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul v-for="entry in history">
|
||||
<li>
|
||||
<label for="time">Time</label>
|
||||
<input name="time" type="text" readonly :value="entry.time">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Method</label>
|
||||
<input name="name" type="text" readonly :value="entry.method">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">URL</label>
|
||||
<input name="name" type="text" readonly :value="entry.url">
|
||||
</li>
|
||||
<li>
|
||||
<label for="name">Path</label>
|
||||
<input name="name" type="text" readonly :value="entry.path">
|
||||
</li>
|
||||
<li>
|
||||
<label for="delete"> </label>
|
||||
<button name="delete" @click="deleteHistory(entry)">Delete</button>
|
||||
</li>
|
||||
<li>
|
||||
<label for="use"> </label>
|
||||
<button name="use" @click="useHistory(entry)">Use</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -208,7 +195,13 @@
|
||||
return headerMap
|
||||
};
|
||||
|
||||
import section from "../components/section";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
'pw-section': section
|
||||
},
|
||||
|
||||
data () {
|
||||
return {
|
||||
method: 'GET',
|
||||
@@ -231,27 +224,24 @@
|
||||
},
|
||||
computed: {
|
||||
statusCategory(){
|
||||
const statusCategory = [
|
||||
return [
|
||||
{name: 'informational', statusCodeRegex: new RegExp(/[1][0-9]+/), className: 'info-response'},
|
||||
{name: 'successful', statusCodeRegex: new RegExp(/[2][0-9]+/), className: 'success-response'},
|
||||
{name: 'redirection', statusCodeRegex: new RegExp(/[3][0-9]+/), className: 'redir-response'},
|
||||
{name: 'client error', statusCodeRegex: new RegExp(/[4][0-9]+/), className: 'cl-error-response'},
|
||||
{name: 'server error', statusCodeRegex: new RegExp(/[5][0-9]+/), className: 'sv-error-response'},
|
||||
].find(status => status.statusCodeRegex.test(this.response.status));
|
||||
|
||||
return statusCategory;
|
||||
},
|
||||
noHistoryToClear() {
|
||||
return this.history.length === 0;
|
||||
},
|
||||
urlNotValid() {
|
||||
const pattern = new RegExp('^(https?:\\/\\/)?' +
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))' +
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?' +
|
||||
'(\\#[-a-z\\d_]*)?$', 'i');
|
||||
return !pattern.test(this.url)
|
||||
isValidURL() {
|
||||
const protocol = '^(https?:\\/\\/)?';
|
||||
|
||||
const validIP = new RegExp(protocol + "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$");
|
||||
const validHostname = new RegExp(protocol + "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$");
|
||||
|
||||
return validIP.test(this.url) || validHostname.test(this.url);
|
||||
},
|
||||
rawRequestBody() {
|
||||
const {
|
||||
@@ -306,19 +296,13 @@
|
||||
this.method = method
|
||||
this.url = url
|
||||
this.path = path
|
||||
this.$refs.request.scrollIntoView({
|
||||
this.$refs.request.$el.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
},
|
||||
collapse({
|
||||
target
|
||||
}) {
|
||||
const el = target.parentNode.className
|
||||
document.getElementsByClassName(el)[0].classList.toggle('hidden')
|
||||
},
|
||||
sendRequest() {
|
||||
if (this.urlNotValid) {
|
||||
alert('Please check the formatting of the URL')
|
||||
if (!this.isValidURL()) {
|
||||
alert('Please check the formatting of the URL');
|
||||
return
|
||||
}
|
||||
const n = new Date().toLocaleTimeString()
|
||||
@@ -329,10 +313,10 @@
|
||||
path: this.path
|
||||
}, ...this.history]
|
||||
window.localStorage.setItem('history', JSON.stringify(this.history))
|
||||
if (this.$refs.response.classList.contains('hidden')) {
|
||||
this.$refs.response.classList.toggle('hidden')
|
||||
if (this.$refs.response.$el.classList.contains('hidden')) {
|
||||
this.$refs.response.$el.classList.toggle('hidden')
|
||||
}
|
||||
this.$refs.response.scrollIntoView({
|
||||
this.$refs.response.$el.scrollIntoView({
|
||||
behavior: 'smooth'
|
||||
})
|
||||
this.response.status = 'Fetching...'
|
||||
|
||||
126
pages/settings.vue
Normal file
126
pages/settings.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
|
||||
<pw-section class="blue" label="Theme">
|
||||
<ul>
|
||||
<li>
|
||||
<h3>Background</h3>
|
||||
<div class="backgrounds">
|
||||
<span v-for="theme in themes" :key="theme.class" @click="applyTheme(theme.class)">
|
||||
<swatch :color="theme.color" :name="theme.name" :class="{ vibrant: theme.vibrant }" :active="settings.THEME_CLASS === theme.class"></swatch>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<br><br>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
<h3>Color</h3>
|
||||
<div class="colors">
|
||||
<span v-for="entry in colors" :key="entry.color"
|
||||
@click.prevent="setActiveColor(entry.color, entry.vibrant)">
|
||||
<swatch
|
||||
:color="entry.color"
|
||||
:name="entry.name"
|
||||
:class="{ vibrant: entry.vibrant }"
|
||||
:active="settings.THEME_COLOR === entry.color.toUpperCase()" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<input id="disableFrameColors" type="checkbox"
|
||||
:checked="!settings.DISABLE_FRAME_COLORS"
|
||||
@change="toggleSetting('DISABLE_FRAME_COLORS')">
|
||||
<label for="disableFrameColors">Enable multi-colored frames</label>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import section from "../components/section";
|
||||
import swatch from "../components/settings/swatch";
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
// NOTE:: You need to first set the CSS for your theme in /assets/css/themes.scss
|
||||
// You should copy the existing light theme as a template and then just
|
||||
// set the relevant values.
|
||||
themes: [
|
||||
{ "color": "#121212", "name": "Dark (Default)", "class": "" },
|
||||
{ "color": "#DFDFDF", "name": "Light", "vibrant": true, "class": "light" }
|
||||
],
|
||||
|
||||
// You can define a new color here! It will simply store the color value.
|
||||
colors: [
|
||||
// If the color is vibrant, black is used as the active foreground color.
|
||||
{ "color": "#51ff0d", "name":"Lime (Default)", "vibrant": true },
|
||||
{ "color": "#FFC107", "name":"Yellow", "vibrant": true },
|
||||
{ "color": "#E91E63", "name":"Pink", "vibrant": false },
|
||||
{ "color": "#e74c3c", "name":"Red", "vibrant": false },
|
||||
{ "color": "#9b59b6", "name":"Purple", "vibrant": false },
|
||||
{ "color": "#2980b9", "name":"Blue", "vibrant": false },
|
||||
],
|
||||
settings: {
|
||||
THEME_CLASS: this.$store.state.postwoman.settings.THEME_CLASS || '',
|
||||
THEME_COLOR: '',
|
||||
THEME_COLOR_VIBRANT: true,
|
||||
|
||||
DISABLE_FRAME_COLORS: this.$store.state.postwoman.settings.DISABLE_FRAME_COLORS || false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
'pw-section': section,
|
||||
'swatch': swatch
|
||||
},
|
||||
|
||||
methods: {
|
||||
applyTheme (name) {
|
||||
this.applySetting('THEME_CLASS', name);
|
||||
document.documentElement.className = name;
|
||||
},
|
||||
|
||||
setActiveColor (color, vibrant) {
|
||||
// By default, the color is vibrant.
|
||||
if(vibrant == null) vibrant = true;
|
||||
|
||||
document.documentElement.style.setProperty('--ac-color', color);
|
||||
document.documentElement.style.setProperty('--act-color', vibrant ? '#121212' : '#fff');
|
||||
|
||||
this.applySetting('THEME_COLOR', color.toUpperCase());
|
||||
this.applySetting('THEME_COLOR_VIBRANT', vibrant);
|
||||
},
|
||||
|
||||
getActiveColor () {
|
||||
// This strips extra spaces and # signs from the strings.
|
||||
const strip = (str) => str.replace(/#/g, '').replace(/ /g, '');
|
||||
|
||||
return `#${strip(window.getComputedStyle(document.documentElement).getPropertyValue('--ac-color')).toUpperCase()}`;
|
||||
},
|
||||
|
||||
applySetting (key, value) {
|
||||
this.settings[key] = value;
|
||||
this.$store.commit('postwoman/applySetting', [key, value]);
|
||||
},
|
||||
|
||||
toggleSetting (key) {
|
||||
this.settings[key] = !this.settings[key];
|
||||
this.$store.commit('postwoman/applySetting', [key, this.settings[key]]);
|
||||
}
|
||||
},
|
||||
|
||||
beforeMount () {
|
||||
this.settings.THEME_COLOR = this.getActiveColor();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,50 +1,45 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
|
||||
<fieldset class="request" ref="request">
|
||||
<legend v-on:click="collapse">Request ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="url">URL</label>
|
||||
<input type="url" :class="{ error: !urlValid }" v-model="url" @keyup.enter="toggleConnection">
|
||||
</li>
|
||||
<li class="no-grow">
|
||||
<label for="action"> </label>
|
||||
<button class="action" :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
<pw-section class="blue" label="Request" ref="request">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="url">URL</label>
|
||||
<input id="url" type="url" :class="{ error: !urlValid }" v-model="url" @keyup.enter="toggleConnection">
|
||||
</li>
|
||||
<li class="no-grow">
|
||||
<label> </label>
|
||||
<button class="action" :class="{ disabled: !urlValid }" name="action" @click="toggleConnection">{{ toggleConnectionVerb }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<fieldset class="response" id="response" ref="response">
|
||||
<legend v-on:click="collapse">Communication ↕</legend>
|
||||
<div class="collapsible">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="body">Log</label>
|
||||
<div name="body" class="body" readonly>
|
||||
<pw-section class="purple" label="Communication" id="response" ref="response">
|
||||
<ul>
|
||||
<li>
|
||||
<label for="log">Log</label>
|
||||
<div id="log" name="log" class="log" readonly>
|
||||
<span v-if="communication.log">
|
||||
<span v-for="logEntry in communication.log" :style="{ color: logEntry.color }">{{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
|
||||
</span>
|
||||
<span v-else>(Waiting for connection...)</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<span v-else>(Waiting for connection...)</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
<label for="status">Message</label>
|
||||
<input name="status" type="text" v-model="communication.input" :readonly="!connectionState" @keyup.enter="sendMessage">
|
||||
</li>
|
||||
<ul>
|
||||
<li>
|
||||
<label for="message">Message</label>
|
||||
<input id="message" name="message" type="text" v-model="communication.input" :readonly="!connectionState" @keyup.enter="sendMessage">
|
||||
</li>
|
||||
|
||||
<li class="no-grow">
|
||||
<label> </label>
|
||||
<button class="action" name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button>
|
||||
</li>
|
||||
</ul>
|
||||
</pw-section>
|
||||
|
||||
<li class="no-grow">
|
||||
<label for="send"> </label>
|
||||
<button class="action" name="send" :class="{ disabled: !connectionState }" @click="sendMessage">Send</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,12 +51,12 @@
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
div.body {
|
||||
div.log {
|
||||
margin: 4px;
|
||||
padding: 8px 16px;
|
||||
width: calc(100% - 8px);
|
||||
border-radius: 4px;
|
||||
background-color: #000;
|
||||
background-color: var(--bg-dark-color);
|
||||
color: var(--fg-color);
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
@@ -80,146 +75,152 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import section from "../components/section";
|
||||
|
||||
export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
connectionState: false,
|
||||
url: "wss://echo.websocket.org",
|
||||
socket: null,
|
||||
components: {
|
||||
'pw-section': section
|
||||
},
|
||||
|
||||
communication: {
|
||||
log: null,
|
||||
input: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
connectionState: false,
|
||||
url: "wss://echo.websocket.org",
|
||||
socket: null,
|
||||
|
||||
computed: {
|
||||
toggleConnectionVerb () {
|
||||
return !this.connectionState ? "Connect" : "Disconnect";
|
||||
},
|
||||
|
||||
urlValid () {
|
||||
const pattern = new RegExp('^(wss?:\\/\\/)?' +
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))' +
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?' +
|
||||
'(\\#[-a-z\\d_]*)?$', 'i');
|
||||
return pattern.test(this.url);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleConnection () {
|
||||
// If it is connecting:
|
||||
if(!this.connectionState) return this.connect();
|
||||
|
||||
// Otherwise, it's disconnecting.
|
||||
else return this.disconnect();
|
||||
},
|
||||
|
||||
connect () {
|
||||
this.communication.log = [
|
||||
{
|
||||
payload: `Connecting to ${this.url}...`,
|
||||
source: 'info',
|
||||
color: 'lime'
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
this.socket = new WebSocket(this.url);
|
||||
|
||||
this.socket.onopen = (event) => {
|
||||
this.connectionState = true;
|
||||
|
||||
this.communication.log = [
|
||||
{
|
||||
payload: `Connected to ${this.url}.`,
|
||||
source: 'info',
|
||||
color: 'lime'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
this.socket.onerror = (event) => {
|
||||
this.handleError();
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
this.connectionState = false;
|
||||
|
||||
this.communication.log.push({
|
||||
payload: `Disconnected from ${this.url}.`,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
this.communication.log.push({
|
||||
payload: event.data,
|
||||
source: 'server'
|
||||
});
|
||||
}
|
||||
}catch(ex){
|
||||
this.handleError(ex);
|
||||
}
|
||||
},
|
||||
|
||||
disconnect () {
|
||||
if(this.socket != null) this.socket.close();
|
||||
},
|
||||
|
||||
handleError (error) {
|
||||
this.disconnect();
|
||||
this.connectionState = false;
|
||||
|
||||
this.communication.log.push({
|
||||
payload: `An error has occurred.`,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
if(error != null) this.communication.log.push({
|
||||
payload: error,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage () {
|
||||
const message = this.communication.input;
|
||||
|
||||
this.socket.send(message);
|
||||
this.communication.log.push({
|
||||
payload: message,
|
||||
source: 'client'
|
||||
});
|
||||
|
||||
this.communication.input = "";
|
||||
},
|
||||
|
||||
collapse({target}) {
|
||||
const el = target.parentNode.className;
|
||||
document.getElementsByClassName(el)[0].classList.toggle('hidden');
|
||||
},
|
||||
|
||||
getSourcePrefix(source){
|
||||
const sourceEmojis = {
|
||||
// Source used for info messages.
|
||||
'info': 'ℹ️ [INFO]:\t',
|
||||
// Source used for client to server messages.
|
||||
'client': '👽 [SENT]:\t',
|
||||
// Source used for server to client messages.
|
||||
'server': '📥 [RECEIVED]:\t'
|
||||
};
|
||||
|
||||
if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source];
|
||||
return '';
|
||||
communication: {
|
||||
log: null,
|
||||
input: ""
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
toggleConnectionVerb () {
|
||||
return !this.connectionState ? "Connect" : "Disconnect";
|
||||
},
|
||||
|
||||
urlValid () {
|
||||
const pattern = new RegExp('^(wss?:\\/\\/)?' +
|
||||
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' +
|
||||
'((\\d{1,3}\\.){3}\\d{1,3}))' +
|
||||
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' +
|
||||
'(\\?[;&a-z\\d%_.~+=-]*)?' +
|
||||
'(\\#[-a-z\\d_]*)?$', 'i');
|
||||
return pattern.test(this.url);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleConnection () {
|
||||
// If it is connecting:
|
||||
if(!this.connectionState) return this.connect();
|
||||
|
||||
// Otherwise, it's disconnecting.
|
||||
else return this.disconnect();
|
||||
},
|
||||
|
||||
connect () {
|
||||
this.communication.log = [
|
||||
{
|
||||
payload: `Connecting to ${this.url}...`,
|
||||
source: 'info',
|
||||
color: 'lime'
|
||||
}
|
||||
];
|
||||
|
||||
try {
|
||||
this.socket = new WebSocket(this.url);
|
||||
|
||||
this.socket.onopen = (event) => {
|
||||
this.connectionState = true;
|
||||
|
||||
this.communication.log = [
|
||||
{
|
||||
payload: `Connected to ${this.url}.`,
|
||||
source: 'info',
|
||||
color: 'lime'
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
this.socket.onerror = (event) => {
|
||||
this.handleError();
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
this.connectionState = false;
|
||||
|
||||
this.communication.log.push({
|
||||
payload: `Disconnected from ${this.url}.`,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
this.communication.log.push({
|
||||
payload: event.data,
|
||||
source: 'server'
|
||||
});
|
||||
}
|
||||
}catch(ex){
|
||||
this.handleError(ex);
|
||||
}
|
||||
},
|
||||
|
||||
disconnect () {
|
||||
if(this.socket != null) this.socket.close();
|
||||
},
|
||||
|
||||
handleError (error) {
|
||||
this.disconnect();
|
||||
this.connectionState = false;
|
||||
|
||||
this.communication.log.push({
|
||||
payload: `An error has occurred.`,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
if(error != null) this.communication.log.push({
|
||||
payload: error,
|
||||
source: 'info',
|
||||
color: 'red'
|
||||
});
|
||||
},
|
||||
|
||||
sendMessage () {
|
||||
const message = this.communication.input;
|
||||
|
||||
this.socket.send(message);
|
||||
this.communication.log.push({
|
||||
payload: message,
|
||||
source: 'client'
|
||||
});
|
||||
|
||||
this.communication.input = "";
|
||||
},
|
||||
|
||||
collapse({target}) {
|
||||
const el = target.parentNode.className;
|
||||
document.getElementsByClassName(el)[0].classList.toggle('hidden');
|
||||
},
|
||||
|
||||
getSourcePrefix(source){
|
||||
const sourceEmojis = {
|
||||
// Source used for info messages.
|
||||
'info': 'ℹ️ [INFO]:\t',
|
||||
// Source used for client to server messages.
|
||||
'client': '👽 [SENT]:\t',
|
||||
// Source used for server to client messages.
|
||||
'server': '📥 [RECEIVED]:\t'
|
||||
};
|
||||
|
||||
if (Object.keys(sourceEmojis).includes(source)) return sourceEmojis[source];
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
5
plugins/vuex-persist.js
Normal file
5
plugins/vuex-persist.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import VuexPersistence from "vuex-persist";
|
||||
|
||||
export default ({ store }) => {
|
||||
new VuexPersistence().plugin(store);
|
||||
}
|
||||
3
store/index.js
Normal file
3
store/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
|
||||
}
|
||||
51
store/postwoman.js
Normal file
51
store/postwoman.js
Normal file
@@ -0,0 +1,51 @@
|
||||
export const SETTINGS_KEYS = [
|
||||
/**
|
||||
* The CSS class that should be applied to the root element.
|
||||
* Essentially, the name of the background theme.
|
||||
*/
|
||||
"THEME_CLASS",
|
||||
|
||||
/**
|
||||
* The hex color code for the currently active theme.
|
||||
*/
|
||||
"THEME_COLOR",
|
||||
|
||||
/**
|
||||
* Whether or not THEME_COLOR is considered 'vibrant'.
|
||||
*
|
||||
* For readability reasons, if the THEME_COLOR is vibrant,
|
||||
* any text placed on the theme color will have its color
|
||||
* inverted from white to black.
|
||||
*/
|
||||
"THEME_COLOR_VIBRANT",
|
||||
|
||||
/**
|
||||
* Normally, section frames are multicolored in the UI
|
||||
* to emphasise the different sections.
|
||||
* This setting allows that to be turned off.
|
||||
*/
|
||||
"DISABLE_FRAME_COLORS"
|
||||
];
|
||||
|
||||
export const state = () => ({
|
||||
settings: {}
|
||||
});
|
||||
|
||||
export const mutations = {
|
||||
|
||||
applySetting (state, setting) {
|
||||
if(setting == null || !(setting instanceof Array) || setting.length !== 2)
|
||||
throw new Error("You must provide a setting (array in the form [key, value])");
|
||||
|
||||
let key = setting[0];
|
||||
let value = setting[1];
|
||||
// Do not just remove this check.
|
||||
// Add your settings key to the SETTINGS_KEYS array at the
|
||||
// top of the file.
|
||||
// This is to ensure that application settings remain documented.
|
||||
if(!SETTINGS_KEYS.includes(key)) throw new Error("The settings key does not include the key " + key);
|
||||
|
||||
state.settings[key] = value;
|
||||
}
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user