forked from ProfessionalUwU/stickerpicker
Add server with basic auth stuff
This commit is contained in:
196
web/.eslintrc.json
Normal file
196
web/.eslintrc.json
Normal file
@ -0,0 +1,196 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:import/errors",
|
||||
"plugin:import/warnings"
|
||||
],
|
||||
"parser": "@babel/eslint-parser",
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"plugins": [
|
||||
"import",
|
||||
"react-hooks"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"only-multiline"
|
||||
],
|
||||
"comma-spacing": [
|
||||
"error",
|
||||
{
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-trailing-spaces": [
|
||||
"error"
|
||||
],
|
||||
"camelcase": [
|
||||
"error",
|
||||
{
|
||||
"properties": "always"
|
||||
}
|
||||
],
|
||||
"import/no-unresolved": "off",
|
||||
"import/named": "error",
|
||||
"import/namespace": "error",
|
||||
"import/default": "error",
|
||||
"import/export": "error",
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"newlines-between": "always",
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "{.,..,../..,../../..,../../../..}/lib/**",
|
||||
"group": "external"
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
"builtin",
|
||||
"external",
|
||||
[
|
||||
"internal",
|
||||
"sibling",
|
||||
"parent"
|
||||
],
|
||||
"index"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
"error",
|
||||
{
|
||||
"code": 100
|
||||
}
|
||||
],
|
||||
"prefer-const": [
|
||||
"error",
|
||||
{
|
||||
"destructuring": "all",
|
||||
"ignoreReadBeforeAssign": false
|
||||
}
|
||||
],
|
||||
"arrow-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"space-before-blocks": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"array-bracket-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"space-in-parens": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"keyword-spacing": [
|
||||
"error",
|
||||
{
|
||||
"before": true,
|
||||
"after": true
|
||||
}
|
||||
],
|
||||
"key-spacing": [
|
||||
"error",
|
||||
{
|
||||
"afterColon": true
|
||||
}
|
||||
],
|
||||
"template-curly-spacing": [
|
||||
"error",
|
||||
"never"
|
||||
],
|
||||
"no-empty": [
|
||||
"error",
|
||||
{
|
||||
"allowEmptyCatch": true
|
||||
}
|
||||
],
|
||||
"arrow-body-style": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"no-multiple-empty-lines": [
|
||||
"error",
|
||||
{
|
||||
"max": 1,
|
||||
"maxBOF": 0,
|
||||
"maxEOF": 0
|
||||
}
|
||||
],
|
||||
"no-prototype-builtins": "off",
|
||||
"dot-notation": [
|
||||
"error",
|
||||
{
|
||||
"allowKeywords": true
|
||||
}
|
||||
],
|
||||
"quote-props": [
|
||||
"error",
|
||||
"as-needed"
|
||||
],
|
||||
"no-multi-spaces": [
|
||||
"error"
|
||||
],
|
||||
"space-infix-ops": [
|
||||
"error"
|
||||
],
|
||||
"object-curly-newline": [
|
||||
"error",
|
||||
{
|
||||
"multiline": false,
|
||||
"consistent": true
|
||||
}
|
||||
],
|
||||
"no-mixed-operators": [
|
||||
"error"
|
||||
],
|
||||
"no-extra-parens": [
|
||||
"error",
|
||||
"all",
|
||||
{
|
||||
"nestedBinaryExpressions": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -5,15 +5,15 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
||||
<title>Maunium sticker picker</title>
|
||||
|
||||
<link rel="modulepreload" href="src/widget-api.js"/>
|
||||
<link rel="modulepreload" href="src/frequently-used.js"/>
|
||||
<link rel="modulepreload" href="src/spinner.js"/>
|
||||
<link rel="modulepreload" href="src/widget/widget-api.js"/>
|
||||
<link rel="modulepreload" href="src/widget/frequently-used.js"/>
|
||||
<link rel="modulepreload" href="src/Spinner.js"/>
|
||||
<link rel="modulepreload" href="lib/htm/preact.js"/>
|
||||
<link rel="preload" href="packs/index.json" as="fetch" type="application/json" crossorigin/>
|
||||
|
||||
<link rel="stylesheet" href="style/index.css"/>
|
||||
<link rel="stylesheet" href="style/widget.css"/>
|
||||
<link rel="stylesheet" href="style/spinner.css"/>
|
||||
<script src="src/index.js" type="module"></script>
|
||||
<script src="src/widget/index.js" type="module"></script>
|
||||
<script nomodule>document.body.innerText = "This sticker picker requires modern JavaScript"</script>
|
||||
</head>
|
||||
<body>
|
||||
|
3
web/lib/common/preact.module-9c264606.js
Normal file
3
web/lib/common/preact.module-9c264606.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5
web/lib/preact/hooks.js
Normal file
5
web/lib/preact/hooks.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { n } from '../common/preact.module-9c264606.js';
|
||||
|
||||
var t,u,r,o=0,i=[],c=n.__r,f=n.diffed,e=n.__c,a=n.unmount;function v(t,r){n.__h&&n.__h(u,t,o||r),o=0;var i=u.__H||(u.__H={__:[],__h:[]});return t>=i.__.length&&i.__.push({}),i.__[t]}function m(n){return o=1,p(k,n)}function p(n,r,o){var i=v(t++,2);return i.t=n,i.__c||(i.__=[o?o(r):k(void 0,r),function(n){var t=i.t(i.__[0],n);i.__[0]!==t&&(i.__=[t,i.__[1]],i.__c.setState({}));}],i.__c=u),i.__}function y(r,o){var i=v(t++,3);!n.__s&&j(i.__H,o)&&(i.__=r,i.__H=o,u.__H.__h.push(i));}function l(r,o){var i=v(t++,4);!n.__s&&j(i.__H,o)&&(i.__=r,i.__H=o,u.__h.push(i));}function h(n){return o=5,_(function(){return {current:n}},[])}function s(n,t,u){o=6,l(function(){"function"==typeof n?n(t()):n&&(n.current=t());},null==u?u:u.concat(n));}function _(n,u){var r=v(t++,7);return j(r.__H,u)&&(r.__=n(),r.__H=u,r.__h=n),r.__}function A(n,t){return o=8,_(function(){return n},t)}function F(n){var r=u.context[n.__c],o=v(t++,9);return o.__c=n,r?(null==o.__&&(o.__=!0,r.sub(u)),r.props.value):n.__}function T(t,u){n.useDebugValue&&n.useDebugValue(u?u(t):t);}function d(n){var r=v(t++,10),o=m();return r.__=n,u.componentDidCatch||(u.componentDidCatch=function(n){r.__&&r.__(n),o[1](n);}),[o[0],function(){o[1](void 0);}]}function q(){i.forEach(function(t){if(t.__P)try{t.__H.__h.forEach(b),t.__H.__h.forEach(g),t.__H.__h=[];}catch(u){t.__H.__h=[],n.__e(u,t.__v);}}),i=[];}n.__r=function(n){c&&c(n),t=0;var r=(u=n.__c).__H;r&&(r.__h.forEach(b),r.__h.forEach(g),r.__h=[]);},n.diffed=function(t){f&&f(t);var u=t.__c;u&&u.__H&&u.__H.__h.length&&(1!==i.push(u)&&r===n.requestAnimationFrame||((r=n.requestAnimationFrame)||function(n){var t,u=function(){clearTimeout(r),x&&cancelAnimationFrame(t),setTimeout(n);},r=setTimeout(u,100);x&&(t=requestAnimationFrame(u));})(q));},n.__c=function(t,u){u.some(function(t){try{t.__h.forEach(b),t.__h=t.__h.filter(function(n){return !n.__||g(n)});}catch(r){u.some(function(n){n.__h&&(n.__h=[]);}),u=[],n.__e(r,t.__v);}}),e&&e(t,u);},n.unmount=function(t){a&&a(t);var u=t.__c;if(u&&u.__H)try{u.__H.__.forEach(b);}catch(t){n.__e(t,u.__v);}};var x="function"==typeof requestAnimationFrame;function b(n){"function"==typeof n.__c&&n.__c();}function g(n){n.__c=n.__();}function j(n,t){return !n||n.length!==t.length||t.some(function(t,u){return t!==n[u]})}function k(n,t){return "function"==typeof t?t(n):t}
|
||||
|
||||
export { A as useCallback, F as useContext, T as useDebugValue, y as useEffect, d as useErrorBoundary, s as useImperativeHandle, l as useLayoutEffect, _ as useMemo, p as useReducer, h as useRef, m as useState };
|
@ -12,7 +12,8 @@
|
||||
},
|
||||
"snowpack": {
|
||||
"install": [
|
||||
"htm/preact"
|
||||
"htm/preact",
|
||||
"preact/hooks"
|
||||
],
|
||||
"installOptions": {
|
||||
"sourceMap": false,
|
||||
@ -22,10 +23,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"htm": "^3.0.4",
|
||||
"preact": "^10.4.8",
|
||||
"snowpack": "^2.10.3"
|
||||
"preact": "^10.5.5",
|
||||
"snowpack": "^2.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.14.1"
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/eslint-parser": "^7.12.1",
|
||||
"eslint": "^7.12.1",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"node-sass": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
23
web/setup/index.html
Normal file
23
web/setup/index.html
Normal file
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
||||
<title>Setup - Maunium sticker picker</title>
|
||||
|
||||
<link rel="modulepreload" href="../lib/htm/preact.js"/>
|
||||
<link rel="modulepreload" href="../lib/preact/hooks.js"/>
|
||||
<link rel="modulepreload" href="src/Spinner.js"/>
|
||||
<link rel="modulepreload" href="src/Button.js"/>
|
||||
|
||||
<link rel="stylesheet" href="../style/setup.css"/>
|
||||
<link rel="stylesheet" href="../style/setup-login.css"/>
|
||||
<link rel="stylesheet" href="../style/spinner.css"/>
|
||||
<link rel="stylesheet" href="../style/button.css"/>
|
||||
<script src="../src/setup/index.js" type="module"></script>
|
||||
<script nomodule>document.body.innerText = "This setup page requires modern JavaScript"</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>This setup page requires JavaScript</noscript>
|
||||
</body>
|
||||
</html>
|
30
web/src/Button.js
Normal file
30
web/src/Button.js
Normal file
@ -0,0 +1,30 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html } from "../lib/htm/preact.js"
|
||||
|
||||
const Button = ({
|
||||
type = "button", class: className = "", children,
|
||||
variant = "filled", size = "normal",
|
||||
...customProps
|
||||
}) => {
|
||||
const props = {
|
||||
class: `mau-button variant-${variant} size-${size} ${className}`,
|
||||
type, ...customProps,
|
||||
}
|
||||
return html`<button ...${props}>${children}</button>`
|
||||
}
|
||||
|
||||
export default Button
|
@ -15,7 +15,7 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html } from "../lib/htm/preact.js"
|
||||
|
||||
export const Spinner = ({ size = 40, noCenter = false, noMargin = false, green = false }) => {
|
||||
const Spinner = ({ size = 40, noCenter = false, noMargin = false, green = false }) => {
|
||||
let margin = 0
|
||||
if (!isNaN(+size)) {
|
||||
size = +size
|
||||
@ -39,3 +39,5 @@ export const Spinner = ({ size = 40, noCenter = false, noMargin = false, green =
|
||||
}
|
||||
return comp
|
||||
}
|
||||
|
||||
export default Spinner
|
215
web/src/setup/LoginView.js
Normal file
215
web/src/setup/LoginView.js
Normal file
@ -0,0 +1,215 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "../../lib/preact/hooks.js"
|
||||
import { html } from "../../lib/htm/preact.js"
|
||||
|
||||
import {
|
||||
getLoginFlows,
|
||||
loginMatrix,
|
||||
requestIntegrationToken,
|
||||
requestOpenIDToken,
|
||||
resolveWellKnown,
|
||||
} from "./matrix-api.js"
|
||||
import Button from "../Button.js"
|
||||
import Spinner from "../Spinner.js"
|
||||
|
||||
const query = Object.fromEntries(location.search
|
||||
.substr(1).split("&")
|
||||
.map(part => part.split("="))
|
||||
.map(([key, value = ""]) => [key, value]))
|
||||
|
||||
const LoginView = ({ onLoggedIn }) => {
|
||||
const usernameWrapperRef = useRef()
|
||||
const usernameRef = useRef()
|
||||
const serverRef = useRef()
|
||||
const passwordRef = useRef()
|
||||
const previousServerValue = useRef()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [userIDFocused, setUserIDFocused] = useState(false)
|
||||
const [supportedFlows, setSupportedFlows] = useState(["m.login.password"])
|
||||
const [username, setUsername] = useState("")
|
||||
const [server, setServer] = useState("")
|
||||
const [serverURL, setServerURL] = useState("")
|
||||
const [password, setPassword] = useState("")
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const keyDown = evt => {
|
||||
if ((evt.target.name === "username" && evt.key === ":") || evt.key === "Enter") {
|
||||
if (evt.target.name === "username") {
|
||||
serverRef.current.focus()
|
||||
} else if (evt.target.name === "server") {
|
||||
passwordRef.current.focus()
|
||||
}
|
||||
evt.preventDefault()
|
||||
} else if (evt.target.name === "server" && !evt.target.value && evt.key === "Backspace") {
|
||||
usernameRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const paste = evt => {
|
||||
if (usernameRef.current.value !== "" || serverRef.current.value !== "") {
|
||||
return
|
||||
}
|
||||
|
||||
let data = evt.clipboardData.getData("text")
|
||||
if (data.startsWith("@")) {
|
||||
data = data.substr(1)
|
||||
}
|
||||
const separator = data.indexOf(":")
|
||||
if (separator === -1) {
|
||||
setUsername(data)
|
||||
} else {
|
||||
setUsername(data.substr(0, separator))
|
||||
setServer(data.substr(separator + 1))
|
||||
serverRef.current.focus()
|
||||
}
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
useLayoutEffect(() => usernameRef.current.focus(), [])
|
||||
const onFocus = () => setUserIDFocused(true)
|
||||
const onBlur = () => {
|
||||
setUserIDFocused(false)
|
||||
if (previousServerValue.current !== server && server) {
|
||||
previousServerValue.current = server
|
||||
setSupportedFlows(null)
|
||||
setError(null)
|
||||
resolveWellKnown(server).then(url => {
|
||||
setServerURL(url)
|
||||
localStorage.mxServerName = server
|
||||
localStorage.mxHomeserver = url
|
||||
return getLoginFlows(url)
|
||||
}).then(flows => {
|
||||
setSupportedFlows(flows)
|
||||
}).catch(err => {
|
||||
setError(err.message)
|
||||
setSupportedFlows(["m.login.password"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (localStorage.mxHomeserver && query.loginToken) {
|
||||
console.log("Found homeserver in localstorage and loginToken in query, " +
|
||||
"attempting SSO token login")
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
submit(query.loginToken, localStorage.mxHomeserver)
|
||||
.catch(err => console.error("Fatal error:", err))
|
||||
.finally(() => setLoading(false))
|
||||
const url = new URL(location.href)
|
||||
url.searchParams.delete("loginToken")
|
||||
history.replaceState({}, document.title, url.toString())
|
||||
}
|
||||
}, [])
|
||||
|
||||
const submit = async (token, serverURLOverride) => {
|
||||
let authInfo
|
||||
if (token) {
|
||||
authInfo = {
|
||||
type: "m.login.token",
|
||||
token,
|
||||
}
|
||||
} else {
|
||||
authInfo = {
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: username,
|
||||
},
|
||||
password,
|
||||
}
|
||||
}
|
||||
try {
|
||||
const actualServerURL = serverURLOverride || serverURL
|
||||
const [accessToken, userID, realURL] = await loginMatrix(actualServerURL, authInfo)
|
||||
console.log(userID, realURL)
|
||||
const openIDToken = await requestOpenIDToken(realURL, userID, accessToken)
|
||||
console.log(openIDToken)
|
||||
const integrationData = await requestIntegrationToken(openIDToken)
|
||||
console.log(integrationData)
|
||||
localStorage.mxAccessToken = accessToken
|
||||
localStorage.mxUserID = userID
|
||||
localStorage.accessToken = integrationData.token
|
||||
onLoggedIn()
|
||||
} catch (err) {
|
||||
setError(err.message)
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = evt => {
|
||||
evt.preventDefault()
|
||||
setError(null)
|
||||
setLoading(true)
|
||||
submit()
|
||||
.catch(err => console.error("Fatal error:", err))
|
||||
.finally(() => setLoading(false))
|
||||
}
|
||||
|
||||
const startSSOLogin = () => {
|
||||
const redir = encodeURIComponent(location.href)
|
||||
location.href = `${serverURL}/_matrix/client/r0/login/sso/redirect?redirectUrl=${redir}`
|
||||
}
|
||||
|
||||
const usernameWrapperClick = evt => evt.target === usernameWrapperRef.current
|
||||
&& usernameRef.current.focus()
|
||||
|
||||
const ssoButton = html`
|
||||
<${Button} type="button" disabled=${!serverURL} onClick=${startSSOLogin}
|
||||
title=${!serverURL ? "Enter your server name before using SSO" : undefined}>
|
||||
${loading ? html`<${Spinner} size=30/>` : "Log in with SSO"}
|
||||
</Button>
|
||||
`
|
||||
|
||||
const disablePwLogin = !username || !server || !serverURL
|
||||
const loginButton = html`
|
||||
<${Button} type="submit" disabled=${disablePwLogin}
|
||||
title=${disablePwLogin ? "Fill out the form before submitting" : undefined}>
|
||||
${loading ? html`<${Spinner} size=30/>` : "Log in"}
|
||||
</Button>
|
||||
`
|
||||
|
||||
return html`
|
||||
<main class="login-view">
|
||||
<form class="login-box ${error ? "has-error" : ""}" onSubmit=${onSubmit}>
|
||||
<h1>Stickerpicker setup</h1>
|
||||
<div class="username input ${userIDFocused ? "focus" : ""}"
|
||||
ref=${usernameWrapperRef} onClick=${usernameWrapperClick}>
|
||||
<span onClick=${() => usernameRef.current.focus()}>@</span>
|
||||
<input type="text" placeholder="username" name="username" value=${username}
|
||||
onChange=${evt => setUsername(evt.target.value)} ref=${usernameRef}
|
||||
onKeyDown=${keyDown} onFocus=${onFocus} onBlur=${onBlur}
|
||||
onPaste=${paste}/>
|
||||
<span onClick=${() => serverRef.current.focus()}>:</span>
|
||||
<input type="text" placeholder="example.com" name="server" value=${server}
|
||||
onChange=${evt => setServer(evt.target.value)} ref=${serverRef}
|
||||
onKeyDown=${keyDown} onFocus=${onFocus} onBlur=${onBlur}/>
|
||||
</div>
|
||||
<input type="password" placeholder="password" name="password" value=${password}
|
||||
class="password input" ref=${passwordRef}
|
||||
disabled=${supportedFlows && !supportedFlows.includes("m.login.password")}
|
||||
onChange=${evt => setPassword(evt.target.value)}/>
|
||||
<div class="button-group">
|
||||
${supportedFlows === null && html`<${Spinner} green size=30 />`}
|
||||
${supportedFlows?.includes("m.login.sso") && ssoButton}
|
||||
${supportedFlows?.includes("m.login.password") && loginButton}
|
||||
</div>
|
||||
${error && html`<div class="error">${error}</div>`}
|
||||
</form>
|
||||
</main>`
|
||||
}
|
||||
|
||||
export default LoginView
|
20
web/src/setup/index.js
Normal file
20
web/src/setup/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html, render } from "../../lib/htm/preact.js"
|
||||
|
||||
import LoginView from "./LoginView.js"
|
||||
|
||||
render(html`<${LoginView} onLoggedIn=${() => console.log("Logged in")}/>`, document.body)
|
102
web/src/setup/matrix-api.js
Normal file
102
web/src/setup/matrix-api.js
Normal file
@ -0,0 +1,102 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import { tryFetch, integrationPrefix } from "./tryGet.js"
|
||||
|
||||
export const resolveWellKnown = async (server) => {
|
||||
try {
|
||||
const resp = await fetch(`https://${server}/.well-known/matrix/client`)
|
||||
const data = await resp.json()
|
||||
let url = data["m.homeserver"].base_url
|
||||
if (url.endsWith("/")) {
|
||||
url = url.slice(0, -1)
|
||||
}
|
||||
return url
|
||||
} catch (err) {
|
||||
console.error("Resolution failed:", err)
|
||||
throw new Error(`Failed to resolve Matrix URL for ${server}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const getLoginFlows = async (address) => {
|
||||
const data = await tryFetch(`${address}/_matrix/client/r0/login`, {},
|
||||
{ service: address, requestType: "get login flows" })
|
||||
const flows = []
|
||||
for (const flow of data.flows) {
|
||||
flows.push(flow.type)
|
||||
}
|
||||
return flows
|
||||
}
|
||||
|
||||
export const loginMatrix = async (address, authInfo) => {
|
||||
const data = await tryFetch(`${address}/_matrix/client/r0/login`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
...authInfo,
|
||||
/* eslint-disable camelcase */
|
||||
device_id: "maunium-stickerpicker",
|
||||
initial_device_display_name: "maunium-stickerpicker",
|
||||
/* eslint-enable camelcase */
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}, {
|
||||
service: address,
|
||||
requestType: "login",
|
||||
})
|
||||
if (data.well_known && data.well_known["m.homeserver"]) {
|
||||
address = data.well_known["m.homeserver"].base_url || address
|
||||
if (address.endsWith("/")) {
|
||||
address = address.slice(0, -1)
|
||||
}
|
||||
}
|
||||
return [data.access_token, data.user_id, address]
|
||||
}
|
||||
|
||||
export const requestOpenIDToken = (address, userID, accessToken) => tryFetch(
|
||||
`${address}/_matrix/client/r0/user/${userID}/openid/request_token`,
|
||||
{
|
||||
method: "POST",
|
||||
body: "{}",
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "OpenID",
|
||||
requestType: "token",
|
||||
},
|
||||
)
|
||||
|
||||
export const requestIntegrationToken = tokenData => tryFetch(
|
||||
`${integrationPrefix}/account/register`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(tokenData),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
{
|
||||
service: "sticker server",
|
||||
requestType: "register",
|
||||
})
|
||||
|
||||
export const logout = () => tryFetch(`${integrationPrefix}/account/logout`, { method: "POST" }, {
|
||||
service: "sticker server",
|
||||
requestType: "logout",
|
||||
})
|
67
web/src/setup/tryGet.js
Normal file
67
web/src/setup/tryGet.js
Normal file
@ -0,0 +1,67 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
export const integrationPrefix = "../_matrix/integrations/v1"
|
||||
|
||||
export const queryToURL = (url, query) => {
|
||||
if (!Array.isArray(query)) {
|
||||
query = Object.entries(query)
|
||||
}
|
||||
query = query.map(([key, value]) =>
|
||||
[key, typeof value === "string" ? value : JSON.stringify(value)])
|
||||
url = `${url}?${new URLSearchParams(query)}`
|
||||
return url
|
||||
}
|
||||
|
||||
export const tryFetch = async (url, options, reqInfo) => {
|
||||
if (options.query) {
|
||||
url = queryToURL(url, options.query)
|
||||
delete options.query
|
||||
}
|
||||
options.headers = {
|
||||
Authorization: `Bearer ${localStorage.accessToken}`,
|
||||
...options.headers,
|
||||
}
|
||||
const reqName = `${reqInfo.service} ${reqInfo.requestType}`
|
||||
let resp
|
||||
try {
|
||||
resp = await fetch(url, options)
|
||||
} catch (err) {
|
||||
console.error(reqName, "request failed:", err)
|
||||
throw new Error(`Failed to contact ${reqInfo.service}`)
|
||||
}
|
||||
if (resp.status === 502) {
|
||||
console.error("Unexpected", reqName, "request bad gateway:", await resp.text())
|
||||
throw new Error(`Failed to contact ${reqInfo.service}`)
|
||||
}
|
||||
if (reqInfo.raw) {
|
||||
return resp
|
||||
} else if (resp.status === 204) {
|
||||
return
|
||||
}
|
||||
let data
|
||||
try {
|
||||
data = await resp.json()
|
||||
} catch (err) {
|
||||
console.error(reqName, "request JSON parse failed:", err)
|
||||
throw new Error(`Invalid response from ${reqInfo.service}`)
|
||||
}
|
||||
if (resp.status >= 400) {
|
||||
console.error("Unexpected", reqName, "request status:", resp.status, data)
|
||||
throw new Error(data.error || data.message || `Invalid response from ${reqInfo.service}`)
|
||||
}
|
||||
return data
|
||||
}
|
@ -13,8 +13,9 @@
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { html, render, Component } from "../lib/htm/preact.js"
|
||||
import { Spinner } from "./spinner.js"
|
||||
import { html, render, Component } from "../../lib/htm/preact.js"
|
||||
|
||||
import Spinner from "../Spinner.js"
|
||||
import * as widgetAPI from "./widget-api.js"
|
||||
import * as frequent from "./frequently-used.js"
|
||||
|
||||
@ -24,23 +25,25 @@ const PACKS_BASE_URL = "packs"
|
||||
// This is updated from packs/index.json
|
||||
let HOMESERVER_URL = "https://matrix-client.matrix.org"
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const makeThumbnailURL = mxc => `${HOMESERVER_URL}/_matrix/media/r0/thumbnail/${mxc.substr(6)}?height=128&width=128&method=scale`
|
||||
|
||||
// We need to detect iOS webkit because it has a bug related to scrolling non-fixed divs
|
||||
// This is also used to fix scrolling to sections on Element iOS
|
||||
const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/)
|
||||
const isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/)
|
||||
&& navigator.userAgent.match(/AppleWebKit/)
|
||||
|
||||
export const parseQuery = str => Object.fromEntries(
|
||||
str.split("&")
|
||||
.map(part => part.split("="))
|
||||
.map(([key, value = ""]) => [key, value]))
|
||||
const query = Object.fromEntries(location.search
|
||||
.substr(1).split("&")
|
||||
.map(part => part.split("="))
|
||||
.map(([key, value = ""]) => [key, value]))
|
||||
|
||||
const supportedThemes = ["light", "dark", "black"]
|
||||
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.defaultTheme = parseQuery(location.search.substr(1)).theme
|
||||
this.defaultTheme = query.theme
|
||||
this.state = {
|
||||
packs: [],
|
||||
loading: true,
|
||||
@ -61,7 +64,8 @@ class App extends Component {
|
||||
this.defaultTheme = "light"
|
||||
}
|
||||
this.stickersByID = new Map(JSON.parse(localStorage.mauFrequentlyUsedStickerCache || "[]"))
|
||||
this.state.frequentlyUsed.stickers = this._getStickersByID(this.state.frequentlyUsed.stickerIDs)
|
||||
this.state.frequentlyUsed.stickers = this._getStickersByID(
|
||||
this.state.frequentlyUsed.stickerIDs)
|
||||
this.imageObserver = null
|
||||
this.packListRef = null
|
||||
this.navRef = null
|
||||
@ -86,12 +90,14 @@ class App extends Component {
|
||||
stickers,
|
||||
},
|
||||
})
|
||||
localStorage.mauFrequentlyUsedStickerCache = JSON.stringify(stickers.map(sticker => [sticker.id, sticker]))
|
||||
localStorage.mauFrequentlyUsedStickerCache = JSON.stringify(
|
||||
stickers.map(sticker => [sticker.id, sticker]))
|
||||
}
|
||||
|
||||
setStickersPerRow(val) {
|
||||
localStorage.mauStickersPerRow = val
|
||||
document.documentElement.style.setProperty("--stickers-per-row", localStorage.mauStickersPerRow)
|
||||
document.documentElement.style.setProperty("--stickers-per-row",
|
||||
localStorage.mauStickersPerRow)
|
||||
this.setState({
|
||||
stickersPerRow: val,
|
||||
})
|
||||
@ -116,20 +122,34 @@ class App extends Component {
|
||||
}
|
||||
|
||||
_loadPacks(disableCache = false) {
|
||||
const cache = disableCache ? "no-cache" : undefined
|
||||
fetch(`${PACKS_BASE_URL}/index.json`, { cache }).then(async indexRes => {
|
||||
const args = {
|
||||
cache: disableCache ? "no-cache" : undefined,
|
||||
headers: query.user_id && query.token ? {
|
||||
Authorization: `Bearer ${query.token}`,
|
||||
"X-Matrix-User-ID": query.user_id,
|
||||
} : {},
|
||||
}
|
||||
fetch(`${PACKS_BASE_URL}/index.json`, args).then(async indexRes => {
|
||||
if (indexRes.status >= 400) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: indexRes.status !== 404 ? indexRes.statusText : null,
|
||||
})
|
||||
try {
|
||||
const errData = await indexRes.json()
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: errData.error,
|
||||
})
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
loading: false,
|
||||
error: indexRes.status !== 404 ? indexRes.statusText : null,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const indexData = await indexRes.json()
|
||||
HOMESERVER_URL = indexData.homeserver_url || HOMESERVER_URL
|
||||
// TODO only load pack metadata when scrolled into view?
|
||||
for (const packFile of indexData.packs) {
|
||||
const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, { cache })
|
||||
const packRes = await fetch(`${PACKS_BASE_URL}/${packFile}`, args)
|
||||
const packData = await packRes.json()
|
||||
for (const sticker of packData.stickers) {
|
||||
this.stickersByID.set(sticker.id, sticker)
|
||||
@ -140,11 +160,15 @@ class App extends Component {
|
||||
})
|
||||
}
|
||||
this.updateFrequentlyUsed()
|
||||
}, error => this.setState({ loading: false, error }))
|
||||
}, error => this.setState({
|
||||
loading: false,
|
||||
error,
|
||||
}))
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.documentElement.style.setProperty("--stickers-per-row", this.state.stickersPerRow.toString())
|
||||
document.documentElement.style.setProperty("--stickers-per-row",
|
||||
this.state.stickersPerRow.toString())
|
||||
this._loadPacks()
|
||||
this.imageObserver = new IntersectionObserver(this.observeImageIntersections, {
|
||||
rootMargin: "100px",
|
||||
@ -226,27 +250,37 @@ class App extends Component {
|
||||
render() {
|
||||
const theme = `theme-${this.state.theme}`
|
||||
if (this.state.loading) {
|
||||
return html`<main class="spinner ${theme}"><${Spinner} size=${80} green /></main>`
|
||||
return html`
|
||||
<main class="spinner ${theme}">
|
||||
<${Spinner} size=${80} green />
|
||||
</main>`
|
||||
} else if (this.state.error) {
|
||||
return html`<main class="error ${theme}">
|
||||
<h1>Failed to load packs</h1>
|
||||
<p>${this.state.error}</p>
|
||||
</main>`
|
||||
return html`
|
||||
<main class="error ${theme}">
|
||||
<h1>Failed to load packs</h1>
|
||||
<p>${this.state.error}</p>
|
||||
</main>`
|
||||
} else if (this.state.packs.length === 0) {
|
||||
return html`<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`
|
||||
return html`
|
||||
<main class="empty ${theme}"><h1>No packs found 😿</h1></main>`
|
||||
}
|
||||
return html`<main class="has-content ${theme}">
|
||||
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
|
||||
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
|
||||
${this.state.packs.map(pack => html`<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
|
||||
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }} iconOverride="settings" />
|
||||
</nav>
|
||||
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}" ref=${elem => this.packListRef = elem}>
|
||||
<${Pack} pack=${this.state.frequentlyUsed} send=${this.sendSticker} />
|
||||
${this.state.packs.map(pack => html`<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker} />`)}
|
||||
<${Settings} app=${this}/>
|
||||
</div>
|
||||
</main>`
|
||||
return html`
|
||||
<main class="has-content ${theme}">
|
||||
<nav onWheel=${this.navScroll} ref=${elem => this.navRef = elem}>
|
||||
<${NavBarItem} pack=${this.state.frequentlyUsed} iconOverride="recent" />
|
||||
${this.state.packs.map(pack => html`
|
||||
<${NavBarItem} id=${pack.id} pack=${pack}/>`)}
|
||||
<${NavBarItem} pack=${{ id: "settings", title: "Settings" }}
|
||||
iconOverride="settings" />
|
||||
</nav>
|
||||
<div class="pack-list ${isMobileSafari ? "ios-safari-hack" : ""}"
|
||||
ref=${elem => this.packListRef = elem}>
|
||||
<${Pack} pack=${this.state.frequentlyUsed} send=${this.sendSticker}/>
|
||||
${this.state.packs.map(pack => html`
|
||||
<${Pack} id=${pack.id} pack=${pack} send=${this.sendSticker}/>`)}
|
||||
<${Settings} app=${this}/>
|
||||
</div>
|
||||
</main>`
|
||||
}
|
||||
}
|
||||
|
||||
@ -257,9 +291,9 @@ const Settings = ({ app }) => html`
|
||||
<button onClick=${app.reloadPacks}>Reload</button>
|
||||
<div>
|
||||
<label for="stickers-per-row">Stickers per row: ${app.state.stickersPerRow}</label>
|
||||
<input type="range" min=2 max=10 id="stickers-per-row" id="stickers-per-row"
|
||||
value=${app.state.stickersPerRow}
|
||||
onInput=${evt => app.setStickersPerRow(evt.target.value)} />
|
||||
<input type="range" min=2 max=10 id="stickers-per-row"
|
||||
value=${app.state.stickersPerRow}
|
||||
onInput=${evt => app.setStickersPerRow(evt.target.value)}/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="theme">Theme: </label>
|
||||
@ -278,25 +312,34 @@ const Settings = ({ app }) => html`
|
||||
// open the link in the browser instead of just scrolling there, so we need to scroll manually:
|
||||
const scrollToSection = (evt, id) => {
|
||||
const pack = document.getElementById(`pack-${id}`)
|
||||
pack.scrollIntoView({ block: "start", behavior: "instant" })
|
||||
pack.scrollIntoView({
|
||||
block: "start",
|
||||
behavior: "instant",
|
||||
})
|
||||
evt.preventDefault()
|
||||
}
|
||||
|
||||
const NavBarItem = ({ pack, iconOverride = null }) => html`
|
||||
const NavBarItem = ({
|
||||
pack,
|
||||
iconOverride = null,
|
||||
}) => html`
|
||||
<a href="#pack-${pack.id}" id="nav-${pack.id}" data-pack-id=${pack.id} title=${pack.title}
|
||||
onClick=${isMobileSafari ? (evt => scrollToSection(evt, pack.id)) : undefined}>
|
||||
onClick=${isMobileSafari ? evt => scrollToSection(evt, pack.id) : undefined}>
|
||||
<div class="sticker">
|
||||
${iconOverride ? html`
|
||||
<span class="icon icon-${iconOverride}"/>
|
||||
<span class="icon icon-${iconOverride}" />
|
||||
` : html`
|
||||
<img src=${makeThumbnailURL(pack.stickers[0].url)}
|
||||
alt=${pack.stickers[0].body} class="visible" />
|
||||
alt=${pack.stickers[0].body} class="visible" />
|
||||
`}
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
|
||||
const Pack = ({ pack, send }) => html`
|
||||
const Pack = ({
|
||||
pack,
|
||||
send,
|
||||
}) => html`
|
||||
<section class="stickerpack" id="pack-${pack.id}" data-pack-id=${pack.id}>
|
||||
<h1>${pack.title}</h1>
|
||||
<div class="sticker-list">
|
||||
@ -307,9 +350,12 @@ const Pack = ({ pack, send }) => html`
|
||||
</section>
|
||||
`
|
||||
|
||||
const Sticker = ({ content, send }) => html`
|
||||
const Sticker = ({
|
||||
content,
|
||||
send,
|
||||
}) => html`
|
||||
<div class="sticker" onClick=${send} data-sticker-id=${content.id}>
|
||||
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body} />
|
||||
<img data-src=${makeThumbnailURL(content.url)} alt=${content.body}/>
|
||||
</div>
|
||||
`
|
||||
|
1
web/style/button.css
Normal file
1
web/style/button.css
Normal file
@ -0,0 +1 @@
|
||||
button.mau-button{cursor:pointer;margin:.5rem 0;border-radius:.25rem;font-size:1rem;box-sizing:border-box;padding:0}button.mau-button:disabled{cursor:default}button.mau-button.size-thick{height:3rem}button.mau-button.size-normal{height:2.5rem}button.mau-button.size-thin{height:2rem}button.mau-button.variant-filled{background-color:#2e7d32;color:#fff;border:none}button.mau-button.variant-filled:hover{background-color:#005005}button.mau-button.variant-filled:disabled{background-color:#CCC;color:#212121}button.mau-button.variant-outlined{background-color:#fff;border:2px solid #2e7d32;color:#2e7d32}button.mau-button.variant-outlined:hover{background-color:#60ad5e}button.mau-button.variant-outlined:disabled{background-color:#fff;border-color:#CCC}
|
60
web/style/button.sass
Normal file
60
web/style/button.sass
Normal file
@ -0,0 +1,60 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
@import theme.sass
|
||||
|
||||
button.mau-button
|
||||
cursor: pointer
|
||||
margin: .5rem 0
|
||||
border-radius: .25rem
|
||||
font-size: 1rem
|
||||
box-sizing: border-box
|
||||
padding: 0
|
||||
|
||||
&:disabled
|
||||
cursor: default
|
||||
|
||||
&.size-thick
|
||||
height: 3rem
|
||||
|
||||
&.size-normal
|
||||
height: 2.5rem
|
||||
|
||||
&.size-thin
|
||||
height: 2rem
|
||||
|
||||
&.variant-filled
|
||||
background-color: $primary
|
||||
color: $primaryContrastText
|
||||
border: none
|
||||
|
||||
&:hover
|
||||
background-color: $primaryDark
|
||||
|
||||
&:disabled
|
||||
background-color: $disabled
|
||||
color: $text
|
||||
|
||||
&.variant-outlined
|
||||
background-color: $background
|
||||
border: 2px solid $primary
|
||||
color: $primary
|
||||
|
||||
&:hover
|
||||
background-color: $primaryLight
|
||||
|
||||
&:disabled
|
||||
background-color: $background
|
||||
border-color: $disabled
|
1
web/style/setup-login.css
Normal file
1
web/style/setup-login.css
Normal file
@ -0,0 +1 @@
|
||||
main.login-view{position:fixed;top:0;bottom:0;right:0;left:0;background-color:#2e7d32;display:flex;justify-content:space-around}main.login-view form.login-box{background-color:#fff;width:25rem;height:22.5rem;padding:2.5rem 2.5rem 2rem;margin-top:3rem;border-radius:.25rem;box-sizing:border-box;display:flex;flex-direction:column}main.login-view form.login-box.has-error{min-height:27rem;height:auto;margin-bottom:auto}main.login-view form.login-box h1{color:#2e7d32;margin:.5rem auto 3rem;font-size:1.5rem}main.login-view form.login-box .input{margin:.5rem 0;border-radius:.25rem;border:1px solid #DDD;padding:1px}main.login-view form.login-box .input:hover,main.login-view form.login-box .input:focus,main.login-view form.login-box .input.focus{border-color:#2e7d32}main.login-view form.login-box .input:focus,main.login-view form.login-box .input.focus{border-width:2px;padding:0}main.login-view form.login-box .username{display:flex;cursor:text}main.login-view form.login-box .username>input{border:none;padding:.75rem .125rem;color:#212121;min-width:0;font-size:1rem}main.login-view form.login-box .username>input:last-of-type{padding-right:.5rem;border-radius:0 .25rem .25rem 0}main.login-view form.login-box .username>input:focus{outline:none}main.login-view form.login-box .username>span{user-select:none;padding:.75rem 0;color:#212121}main.login-view form.login-box .username>span:first-of-type{padding-left:.5rem}main.login-view form.login-box .password{font-size:1rem;margin:.5rem 0;border-radius:.25rem;border:1px solid #DDD;padding:calc(.75rem + 1px) 1rem;box-sizing:border-box}main.login-view form.login-box .password:hover:not(:disabled),main.login-view form.login-box .password:focus:not(:disabled){border-color:#2e7d32}main.login-view form.login-box .password:focus{padding:0.75rem calc(1rem - 1px);border-width:2px;outline:none}main.login-view form.login-box .button-group{display:flex;gap:4px}main.login-view form.login-box .button-group button{width:100%}main.login-view form.login-box .error{padding:1rem;border-radius:.25rem;border:2px solid #B71C1C;background-color:#F7A9A1;margin:.5rem 0;width:100%;box-sizing:border-box}
|
105
web/style/setup-login.sass
Normal file
105
web/style/setup-login.sass
Normal file
@ -0,0 +1,105 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
@import theme.sass
|
||||
|
||||
main.login-view
|
||||
position: fixed
|
||||
top: 0
|
||||
bottom: 0
|
||||
right: 0
|
||||
left: 0
|
||||
background-color: $primary
|
||||
display: flex
|
||||
justify-content: space-around
|
||||
|
||||
form.login-box
|
||||
background-color: $background
|
||||
width: 25rem
|
||||
height: 22.5rem
|
||||
padding: 2.5rem 2.5rem 2rem
|
||||
margin-top: 3rem
|
||||
border-radius: .25rem
|
||||
box-sizing: border-box
|
||||
display: flex
|
||||
flex-direction: column
|
||||
|
||||
&.has-error
|
||||
min-height: 27rem
|
||||
height: auto
|
||||
margin-bottom: auto
|
||||
|
||||
h1
|
||||
color: $primary
|
||||
margin: .5rem auto 3rem
|
||||
font-size: 1.5rem
|
||||
|
||||
.input
|
||||
margin: .5rem 0
|
||||
border-radius: .25rem
|
||||
border: 1px solid $border
|
||||
padding: 1px
|
||||
|
||||
&:hover, &:focus, &.focus
|
||||
border-color: $primary
|
||||
|
||||
&:focus, &.focus
|
||||
border-width: 2px
|
||||
padding: 0
|
||||
|
||||
.username
|
||||
display: flex
|
||||
cursor: text
|
||||
|
||||
& > input
|
||||
border: none
|
||||
padding: .75rem .125rem
|
||||
color: $text
|
||||
min-width: 0
|
||||
font-size: 1rem
|
||||
|
||||
&:last-of-type
|
||||
padding-right: .5rem
|
||||
border-radius: 0 .25rem .25rem 0
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
|
||||
& > span
|
||||
user-select: none
|
||||
padding: .75rem 0
|
||||
color: $text
|
||||
|
||||
&:first-of-type
|
||||
padding-left: .5rem
|
||||
|
||||
.password
|
||||
@include input
|
||||
|
||||
.button-group
|
||||
display: flex
|
||||
gap: 4px
|
||||
|
||||
button
|
||||
width: 100%
|
||||
|
||||
.error
|
||||
padding: 1rem
|
||||
border-radius: .25rem
|
||||
border: 2px solid $errorDark
|
||||
background-color: $error
|
||||
margin: .5rem 0
|
||||
width: 100%
|
||||
box-sizing: border-box
|
1
web/style/setup.css
Normal file
1
web/style/setup.css
Normal file
@ -0,0 +1 @@
|
||||
body{font-family:sans-serif}
|
18
web/style/setup.sass
Normal file
18
web/style/setup.sass
Normal file
@ -0,0 +1,18 @@
|
||||
// maunium-stickerpicker - A fast and simple Matrix sticker picker widget.
|
||||
// Copyright (C) 2020 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
body
|
||||
font-family: sans-serif
|
0
web/style/theme.css
Normal file
0
web/style/theme.css
Normal file
33
web/style/theme.sass
Normal file
33
web/style/theme.sass
Normal file
@ -0,0 +1,33 @@
|
||||
// Material UI green 800
|
||||
$primary: #2e7d32
|
||||
$primaryDark: #005005
|
||||
$primaryLight: #60ad5e
|
||||
// Material UI blue 700
|
||||
$secondary: #1976d2
|
||||
$secondaryDark: #004ba0
|
||||
$secondaryLight: #63a4ff
|
||||
|
||||
$error: #F7A9A1
|
||||
$errorDark: #B71C1C
|
||||
|
||||
$primaryContrastText: white
|
||||
$background: white
|
||||
$text: #212121
|
||||
$border: #DDD
|
||||
$disabled: #CCC
|
||||
|
||||
@mixin input
|
||||
font-size: 1rem
|
||||
margin: .5rem 0
|
||||
border-radius: .25rem
|
||||
border: 1px solid $border
|
||||
padding: calc(.75rem + 1px) 1rem
|
||||
box-sizing: border-box
|
||||
|
||||
&:hover:not(:disabled), &:focus:not(:disabled)
|
||||
border-color: $primary
|
||||
|
||||
&:focus
|
||||
padding: .75rem calc(1rem - 1px)
|
||||
border-width: 2px
|
||||
outline: none
|
1478
web/yarn.lock
1478
web/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user