diff --git a/src/assets/img/error.svg b/src/assets/img/error.svg new file mode 100644 index 0000000..7a4f06e --- /dev/null +++ b/src/assets/img/error.svg @@ -0,0 +1,5 @@ + + + diff --git a/src/assets/img/globe-shield-off.svg b/src/assets/img/globe-shield-off.svg index 9e57dbd..702621f 100644 --- a/src/assets/img/globe-shield-off.svg +++ b/src/assets/img/globe-shield-off.svg @@ -1,4 +1,4 @@ - + diff --git a/src/assets/img/globe-shield-on.svg b/src/assets/img/globe-shield-on.svg index be5ab21..ad021c2 100644 --- a/src/assets/img/globe-shield-on.svg +++ b/src/assets/img/globe-shield-on.svg @@ -1,5 +1,10 @@ - - + + + diff --git a/src/background/vpncontroller/states.js b/src/background/vpncontroller/states.js index cc31f9c..a300f59 100644 --- a/src/background/vpncontroller/states.js +++ b/src/background/vpncontroller/states.js @@ -33,6 +33,8 @@ export class VPNState { subscribed = true; // True if it is authenticated authenticated = false; + // Can be "Stable", "Unstable", "NoSignal" + connectionStability = "Stable"; /** * A socks:// url to connect to * to bypass the vpn. @@ -52,6 +54,10 @@ export class VPNState { * Timestamp since the VPN connection was established */ connectedSince = 0; + + static NoSignal = "NoSignal"; + static Unstable = "Unstable"; + static Stable = "Stable"; } /** @@ -121,10 +127,28 @@ export class StateVPNEnabled extends StateVPNDisabled { * @param {ServerCity | undefined} exitServerCity * @param {ServerCountry | undefined } exitServerCountry */ - constructor(exitServerCity, exitServerCountry, aloophole, connectedSince) { + constructor( + exitServerCity, + exitServerCountry, + aloophole, + connectedSince, + connectionHealth = "Stable" + ) { super(exitServerCity, exitServerCountry); + this.exitServerCity = exitServerCity; + this.exitServerCountry = exitServerCountry; this.loophole = aloophole; this.connectedSince = connectedSince; + if ( + ![VPNState.NoSignal, VPNState.Stable, VPNState.Unstable].includes( + connectionHealth + ) + ) { + throw new Error( + `${connectionHealth} is not a Valid Value for ConnectionHealth` + ); + } + this.connectionHealth = connectionHealth; } state = "Enabled"; subscribed = true; @@ -171,6 +195,7 @@ export class vpnStatusResponse { connectedSince: "0", app: "MozillaVPN::CustomState", vpn: "Controller::StateOn", + connectionHealth: "Stable", localProxy: { available: false, url: "https://localhost:8080", diff --git a/src/background/vpncontroller/vpncontroller.js b/src/background/vpncontroller/vpncontroller.js index 838f83c..a584a91 100644 --- a/src/background/vpncontroller/vpncontroller.js +++ b/src/background/vpncontroller/vpncontroller.js @@ -311,7 +311,8 @@ export function fromVPNStatusResponse( exitServerCity, exitServerCountry, status.localProxy?.url, - connectedSince + connectedSince, + status.connectionHealth ); } if ( diff --git a/src/components/vpncard.js b/src/components/vpncard.js index c6d0253..174e58c 100644 --- a/src/components/vpncard.js +++ b/src/components/vpncard.js @@ -12,6 +12,8 @@ import { import { tr } from "../shared/i18n.js"; import { resetSizing } from "./styles.js"; +import { VPNState } from "../background/vpncontroller/states.js"; + /** * @typedef {import("../background/vpncontroller/states.js").VPNState} VPNState */ @@ -22,6 +24,7 @@ export class VPNCard extends LitElement { connectedSince: { type: Date }, cityName: { type: String }, countryFlag: { type: String }, + stability: { type: String }, }; constructor() { @@ -30,6 +33,7 @@ export class VPNCard extends LitElement { this.cityName = ""; this.countryFlag = ""; this.connectedSince = 0; + this.stability = VPNState.Stable; } #intervalHandle = null; @@ -54,11 +58,13 @@ export class VPNCard extends LitElement { const boxClasses = { box: true, on: this.enabled, + unstable: this.enabled && this.stability === VPNState.Unstable, + noSignal: this.enabled && this.stability === VPNState.NoSignal, + stable: + this.enabled && + this.stability != VPNState.Unstable && + this.stability != VPNState.NoSignal, }; - const shieldURL = this.enabled - ? "../../assets/img/globe-shield-on.svg" - : "../../assets/img/globe-shield-off.svg"; - function formatSingle(value) { if (value === 0) return "00"; return (value < 10 ? "0" : "") + value; @@ -81,22 +87,19 @@ export class VPNCard extends LitElement { const time = Date.now() - this.connectedSince; //console.log(`Elapsed Time: ${time}`) - const timeString = this.enabled - ? html`

${formatTime(time)}

` - : html``; - - const subLine = this.enabled - ? null - : html`

${tr("turnOnForPrivateConnection")}

`; + const timeString = + this.enabled && this.stability === VPNState.Stable + ? html`

${formatTime(time)}

` + : html``; const vpnHeader = this.enabled ? tr("vpnIsOn") : tr("vpnIsOff"); return html`
- + ${VPNCard.shield(this.enabled)}

${vpnHeader}

- ${subLine} ${timeString} + ${VPNCard.subline(this.enabled, this.stability)} ${timeString}
@@ -119,6 +122,41 @@ export class VPNCard extends LitElement { `; } + static subline(enabled, stability) { + if (!enabled) { + return html`

${tr("turnOnForPrivateConnection")}

`; + } + const errorSvg = html` + + + + `; + + switch (stability) { + case VPNState.NoSignal: + return html`

${errorSvg} No Signal

`; + case VPNState.Unstable: + return html`

${errorSvg} Unstable

`; + default: + null; + } + } + + static shield(enabled) { + if (!enabled) { + return html` + + + + `; + } + return html` + + + + `; + } + static styles = css` ${resetSizing} @@ -147,6 +185,9 @@ export class VPNCard extends LitElement { width: 100%; justify-content: baseline; } + footer img { + margin-right: 8px; + } main { justify-content: space-between; padding: var(--default-padding); @@ -173,12 +214,21 @@ export class VPNCard extends LitElement { .box.on * { color: var(--main-card-text-color); } + .box.unstable { + --shield-color: var(--color-warning); + --error-fill: var(--color-warning); + } + .box.noSignal { + --shield-color: var(--color-fatal-error); + --error-fill: var(--color-fatal-error); + } + .box.stable { + --shield-color: var(--color-enabled); + } + .infobox { flex: 4; } - img { - margin-right: var(--default-padding); - } h1 { font-size: 18px; line-height: 20px; @@ -188,7 +238,6 @@ export class VPNCard extends LitElement { font-size: 14px; line-height: 21px; font-weight: 400; - color: var(--main-card-text-color); opacity: 0.7; } @@ -196,7 +245,25 @@ export class VPNCard extends LitElement { .subline { margin-block-start: calc(var(--default-padding) / 2); } + .subline svg { + width: 14px; + height: 14px; + margin: 0px; + transform: scale(1.1); + } + .unstable .subline { + color: var(--color-warning); + } + .noSignal .subline { + color: var(--color-fatal-error); + } + svg { + height: 48px; + width: 48px; + transition: all 3s; + margin-right: var(--default-padding); + } .pill { width: 45px; height: 24px; diff --git a/src/ui/browserAction/popupPage.js b/src/ui/browserAction/popupPage.js index 8aadf48..f3e8265 100644 --- a/src/ui/browserAction/popupPage.js +++ b/src/ui/browserAction/popupPage.js @@ -151,6 +151,7 @@ export class BrowserActionPopup extends LitElement { .cityName=${this.vpnState?.exitServerCity?.name} .countryFlag=${this.vpnState?.exitServerCountry?.code} .connectedSince=${this.vpnState?.connectedSince} + .stability=${this.vpnState?.connectionStability} > ${this.locationSettings()} diff --git a/src/ui/variables.css b/src/ui/variables.css index cecdcb0..dec57eb 100644 --- a/src/ui/variables.css +++ b/src/ui/variables.css @@ -38,6 +38,8 @@ --font-family-semi-bold: "Inter Semi Bold"; --font-family-bold: "Inter Bold"; --color-enabled: #3fe1b0; + --color-warning: #ffa436; + --color-fatal-error: #ff6a75; --main-card-background: #321c64; --main-card-text-color: white; --main-card--pill-background: lch(