Rewrite project (and rename as wimi)
12
README.md
|
@ -1,4 +1,8 @@
|
|||
## About
|
||||
<p align="center">
|
||||
<img src="src/images/icon128x128.png"></img>
|
||||
<br>
|
||||
<strong>wimi</strong>
|
||||
</p>
|
||||
|
||||
wimi relays information about your public IP address
|
||||
from
|
||||
|
@ -13,12 +17,6 @@ The extension can be built locally and installed as a
|
|||
developer extension / addon on both Chromium and FireFox.
|
||||
There are XPI files provided for FireFox users as well.
|
||||
|
||||
## Example
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/0x1eef/wimi/main/share/wimi/wimi.png">
|
||||
</p>
|
||||
|
||||
## Install
|
||||
|
||||
**Chrome**
|
||||
|
|
15
bin/iconify
|
@ -1,15 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
sizes="16x16 48x48 64x64 128x128 256x256"
|
||||
dir="src/images/icons"
|
||||
for size in ${sizes}; do
|
||||
convert ${dir}/icon.svg \
|
||||
-resize ${size} \
|
||||
-background none \
|
||||
-trim +repage \
|
||||
-fuzz 5% \
|
||||
-transparent white \
|
||||
-gravity center \
|
||||
-extent $size \
|
||||
${dir}/wtf${size}.png
|
||||
done
|
22
bin/mkicons
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
##
|
||||
# variables
|
||||
sizes="16x16 48x48 64x64 128x128 256x256"
|
||||
dir="src/images/"
|
||||
|
||||
##
|
||||
# main
|
||||
for size in ${sizes}; do
|
||||
gm convert \
|
||||
"${dir}"/icon.svg \
|
||||
-resize "${size}" \
|
||||
-background none \
|
||||
-trim +repage \
|
||||
-fuzz 5% \
|
||||
-transparent white \
|
||||
-gravity center \
|
||||
-extent "${size}" \
|
||||
"${dir}"/icon"${size}".png
|
||||
done
|
|
@ -1,5 +1,17 @@
|
|||
# -*- mode: org -*-
|
||||
|
||||
** vNEXT
|
||||
|
||||
**** Add bin/mkicons
|
||||
Replace the previous script
|
||||
|
||||
**** Add new icons
|
||||
Replace the previous set of icons with wimi icons
|
||||
|
||||
**** Hello wimi
|
||||
The project is now known as wimi - short for:
|
||||
what is my ip address
|
||||
|
||||
** v0.5.1
|
||||
|
||||
**** Shorten description in manifest
|
||||
|
|
Before Width: | Height: | Size: 11 KiB |
|
@ -1,45 +1,11 @@
|
|||
:root {
|
||||
/* Defaults */
|
||||
--default-font-size: 1.15em;
|
||||
|
||||
/* Colors */
|
||||
--primary-red: #d73e48;
|
||||
--primary-green: #32b643;
|
||||
--primary-blue: #302ecd;
|
||||
--secondary-blue: #807fe2;
|
||||
--primary-white: #FFF;
|
||||
--secondary-white: #f1f1fc;
|
||||
--primary-color: #E3F2FD;
|
||||
--secondary-color: #333333;
|
||||
--accent-color: #007BFF;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: small;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#root, .loader {
|
||||
height: 100%;
|
||||
}
|
||||
a {
|
||||
color: var(--primary-blue) !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
color: var(--primary-blue) !important;
|
||||
}
|
||||
a:active, a:focus, a:hover {
|
||||
color: var(--primary-blue) !important;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
footer {
|
||||
background: var(--secondary-white);
|
||||
color: var(--primary-red);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
place-content: center;
|
||||
#root {
|
||||
background: var(--primary-color);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="stylesheet" href="/css/index.css"/>
|
||||
<link rel="stylesheet" href="/css/components/BooleanLabel.css"/>
|
||||
</head>
|
||||
<body class="h-60">
|
||||
<div id="root" class="h-full font-sans"></div>
|
||||
<body class="h-12 w-full">
|
||||
<div id="root"></div>
|
||||
<script src="/js/index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
1
src/images/icon.svg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/images/icon128x128.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
src/images/icon16x16.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/images/icon256x256.png
Normal file
After Width: | Height: | Size: 69 KiB |
BIN
src/images/icon48x48.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
src/images/icon64x64.png
Normal file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 81 KiB |
Before Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 15 KiB |
|
@ -1,15 +1,29 @@
|
|||
import { ErrorRenderer } from "~/components/ErrorRenderer";
|
||||
import { ResponseRenderer } from "~/components/ResponseRenderer";
|
||||
import { Loader } from "~/components/Loader";
|
||||
import { useWebService } from "~/hooks/useWebService";
|
||||
|
||||
export function App() {
|
||||
const [response, error] = useWebService();
|
||||
if (response) {
|
||||
return <ResponseRenderer response={response} />;
|
||||
return (
|
||||
<div data-testid="response" className="font-sans p-2 flex">
|
||||
<img className="w-8 h-8" src={`/images/flags/${response.countryCode}.svg`} />
|
||||
<div className="flex flex-col ml-3">
|
||||
<span className="text-xs">{response.IPAddress}</span>
|
||||
<span className="text-xs">{response.ISP}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (error) {
|
||||
return <ErrorRenderer error={error} />;
|
||||
return (
|
||||
<div data-testid="error" className="font-sans p-2 flex items-center w-full h-full">
|
||||
<img className="w-8 h-8" src={`/images/icon.svg`} />
|
||||
<span className="ml-3 text-xs">Error</span>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <Loader />;
|
||||
return (
|
||||
<div data-testid="loading" className="font-sans p-2 flex items-center w-full h-full">
|
||||
<img className="w-8 h-8" src={`/images/icon.svg`} />
|
||||
<span className="ml-3 text-xs">Loading</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { Footer } from "~/components/Footer";
|
||||
|
||||
export function ErrorRenderer({ error }: { error: Error }) {
|
||||
const t = chrome.i18n.getMessage;
|
||||
return (
|
||||
<div data-testid="error" className="w-full h-full">
|
||||
<div className="flex flex-col h-5/6 justify-center w-3/4 m-auto"></div>
|
||||
<Footer>
|
||||
<span className="font-bold">{t("error")}</span>:
|
||||
<span data-testid="error-message">{error.message}</span>
|
||||
</Footer>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import type { ReactNode } from "react";
|
||||
|
||||
export function Footer({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<footer className="flex h-10">
|
||||
<div className="w-3/4 m-auto">{children}</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
import { Footer } from "~/components/Footer";
|
||||
|
||||
export function Loader() {
|
||||
const t = chrome.i18n.getMessage;
|
||||
return (
|
||||
<div className="w-full h-full" data-testid="loader">
|
||||
<div className="flex flex-col h-5/6 justify-center w-3/4 m-auto"></div>
|
||||
<Footer>{t("loading")}</Footer>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
import { useEffect, ReactNode } from "react";
|
||||
import { TResponse } from "~/lib/response";
|
||||
import { Footer } from "~/components/Footer";
|
||||
type TFunction = typeof chrome.i18n.getMessage;
|
||||
|
||||
export function ResponseRenderer({ response }: { response: TResponse }) {
|
||||
const t = chrome.i18n.getMessage;
|
||||
|
||||
useEffect(() => {
|
||||
const { body } = document;
|
||||
const width = (() => {
|
||||
const addr = response.IPAddress;
|
||||
if (addr.length >= 30) {
|
||||
return 525;
|
||||
} else if (addr.length >= 20) {
|
||||
return 450;
|
||||
} else {
|
||||
return 400;
|
||||
}
|
||||
})();
|
||||
body.style.width = `${width}px`;
|
||||
}, [response]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="flex flex-col h-5/6 justify-center w-3/4 m-auto" data-testid="response">
|
||||
<Row name={t("ip_address")} value={response.IPAddress} />
|
||||
<Row name={t("location")} value={`${response.City}, ${response.Country}`} />
|
||||
<Row name={t("isp")} value={response.ISP} />
|
||||
<Row name={t("tor_exit_node")} value={<BooleanLabel on={response.isTorExitNode} t={t} />} />
|
||||
</div>
|
||||
<Footer>
|
||||
<a target="_blank" href={`https://${t("clean.myip.wtf")}`}>
|
||||
{t("clean.myip.wtf")}
|
||||
</a>
|
||||
</Footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BooleanLabel({ on, t }: { on: boolean; t: TFunction }) {
|
||||
if (on) {
|
||||
return <label className="label label-rounded label-success">{t("yes")}</label>;
|
||||
} else {
|
||||
return <label className="label label-rounded label-error">{t("no")}</label>;
|
||||
}
|
||||
}
|
||||
|
||||
function Row({ name, value }: { name: string; value: ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-8">
|
||||
<div className="flex w-2/4 items-center font-semibold">{name}</div>
|
||||
<div className="flex w-3/4 items-center justify-end">{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ export type TResponse = {
|
|||
ISP: string;
|
||||
City: string;
|
||||
Country: string;
|
||||
countryCode: string;
|
||||
isTorExitNode: boolean;
|
||||
};
|
||||
|
||||
|
@ -11,6 +12,7 @@ export type TServerResponse = {
|
|||
YourISP: string;
|
||||
YourCity: string;
|
||||
YourCountry: string;
|
||||
YourCountryCode: string;
|
||||
YourTorExit: boolean;
|
||||
};
|
||||
|
||||
|
@ -21,6 +23,7 @@ export function Response(res: TServerResponse): TResponse {
|
|||
self.ISP = res.YourISP;
|
||||
self.City = res.YourCity;
|
||||
self.Country = res.YourCountry;
|
||||
self.countryCode = res.YourCountryCode;
|
||||
self.isTorExitNode = res.YourTorExit;
|
||||
|
||||
return self;
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
"default_popup": "/html/browseraction.html"
|
||||
},
|
||||
"icons": {
|
||||
"16": "images/icons/wtf16x16.png",
|
||||
"48": "images/icons/wtf48x48.png",
|
||||
"64": "images/icons/wtf64x64.png",
|
||||
"128": "images/icons/wtf128x128.png",
|
||||
"256": "images/icons/wtf256x256.png"
|
||||
"16": "images/icon16x16.png",
|
||||
"48": "images/icon48x48.png",
|
||||
"64": "images/icon64x64.png",
|
||||
"128": "images/icon128x128.png",
|
||||
"256": "images/icon256x256.png"
|
||||
},
|
||||
"permissions": [],
|
||||
"default_locale": "en",
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("App.tsx", () => {
|
|||
|
||||
test("loading text is rendered", () => {
|
||||
render(<App/>);
|
||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("loading")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import React from "react";
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ErrorRenderer } from "~/components/ErrorRenderer";
|
||||
import { getMessage } from "./mocks/chrome.i18n";
|
||||
|
||||
describe("ErrorRenderer.tsx", () => {
|
||||
const globalChrome = global.chrome;
|
||||
const error = new Error("This is an example error message");
|
||||
|
||||
beforeEach(() => {
|
||||
const chrome: any = { i18n: { getMessage } };
|
||||
global.chrome = chrome;
|
||||
render(<ErrorRenderer error={error}/>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.chrome = globalChrome;
|
||||
});
|
||||
|
||||
test("an error is rendered", () => {
|
||||
const { getByTestId, getByText } = screen;
|
||||
const span = getByTestId("error-message");
|
||||
expect(getByText("Error")).toBeInTheDocument();
|
||||
expect(span.textContent).toEqual(error.message);
|
||||
});
|
||||
});
|
|
@ -1,67 +0,0 @@
|
|||
import React from "react";
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { ResponseRenderer } from "~/components/ResponseRenderer";
|
||||
import { getMessage } from "./mocks/chrome.i18n";
|
||||
|
||||
describe("ResponseRenderer.tsx", () => {
|
||||
const globalChrome = global.chrome;
|
||||
|
||||
beforeEach(() => {
|
||||
const chrome: any = { i18n: { getMessage } };
|
||||
global.chrome = chrome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.chrome = globalChrome;
|
||||
});
|
||||
|
||||
const defaultResponse = {
|
||||
IPAddress: "89.222.123.45",
|
||||
ISP: "FooBar Ltd",
|
||||
City: "FooBar City",
|
||||
Country: "United States of FooBar",
|
||||
isTorExitNode: false
|
||||
};
|
||||
|
||||
describe("when isTorExitNode is false", () => {
|
||||
const response = { ...defaultResponse };
|
||||
|
||||
beforeEach(() => {
|
||||
render(<ResponseRenderer response={response}/>);
|
||||
});
|
||||
|
||||
test("an IP address is rendered", () => {
|
||||
expect(screen.getByText("IP Address")).toBeInTheDocument();
|
||||
expect(screen.getByText(response.IPAddress)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("an ISP is rendered", () => {
|
||||
expect(screen.getByText("ISP")).toBeInTheDocument();
|
||||
expect(screen.getByText(response.ISP)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("a location is rendered", () => {
|
||||
expect(screen.getByText("Location")).toBeInTheDocument();
|
||||
expect(screen.getByText(`${response.City}, ${response.Country}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("isTorExitNode is rendered as No", () => {
|
||||
expect(screen.getByText("Tor exit node")).toBeInTheDocument();
|
||||
expect(screen.getByText("No")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when isTorExitNode is true", () => {
|
||||
const response = { ...defaultResponse, isTorExitNode: true };
|
||||
|
||||
beforeEach(() => {
|
||||
render(<ResponseRenderer response={response}/>);
|
||||
});
|
||||
|
||||
test("isTorExitNode is rendered as Yes", () => {
|
||||
expect(screen.getByText("Tor exit node")).toBeInTheDocument();
|
||||
expect(screen.getByText("Yes")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@ export function success(_path: RequestInfo | URL, _options?: RequestInit) {
|
|||
YourISP: "FooBar Ltd",
|
||||
YourCity: "Foo City",
|
||||
YourCountry: "United States of FooBar",
|
||||
YourCountryCode: "BR",
|
||||
YourTorExit: false
|
||||
})
|
||||
)
|
||||
|
|