Stuff
This commit is contained in:
		
							parent
							
								
									c35e5b33d1
								
							
						
					
					
						commit
						c2f16f740b
					
				
					 21 changed files with 332 additions and 547 deletions
				
			
		|  | @ -6,9 +6,9 @@ import { | |||
|     topicUrlAuth, | ||||
|     topicUrlJsonPoll, | ||||
|     topicUrlJsonPollWithSince, | ||||
|     userAccountUrl, | ||||
|     userTokenUrl, | ||||
|     userStatsUrl, userSubscriptionUrl, userSubscriptionDeleteUrl | ||||
|     accountSettingsUrl, | ||||
|     accountTokenUrl, | ||||
|     userStatsUrl, accountSubscriptionUrl, accountSubscriptionSingleUrl, accountUrl | ||||
| } from "./utils"; | ||||
| import userManager from "./UserManager"; | ||||
| 
 | ||||
|  | @ -120,7 +120,7 @@ class Api { | |||
|     } | ||||
| 
 | ||||
|     async login(baseUrl, user) { | ||||
|         const url = userTokenUrl(baseUrl); | ||||
|         const url = accountTokenUrl(baseUrl); | ||||
|         console.log(`[Api] Checking auth for ${url}`); | ||||
|         const response = await fetch(url, { | ||||
|             headers: maybeWithBasicAuth({}, user) | ||||
|  | @ -136,7 +136,7 @@ class Api { | |||
|     } | ||||
| 
 | ||||
|     async logout(baseUrl, token) { | ||||
|         const url = userTokenUrl(baseUrl); | ||||
|         const url = accountTokenUrl(baseUrl); | ||||
|         console.log(`[Api] Logging out from ${url} using token ${token}`); | ||||
|         const response = await fetch(url, { | ||||
|             method: "DELETE", | ||||
|  | @ -159,8 +159,24 @@ class Api { | |||
|         return stats; | ||||
|     } | ||||
| 
 | ||||
|     async userAccount(baseUrl, token) { | ||||
|         const url = userAccountUrl(baseUrl); | ||||
|     async createAccount(baseUrl, username, password) { | ||||
|         const url = accountUrl(baseUrl); | ||||
|         const body = JSON.stringify({ | ||||
|             username: username, | ||||
|             password: password | ||||
|         }); | ||||
|         console.log(`[Api] Creating user account ${url}`); | ||||
|         const response = await fetch(url, { | ||||
|             method: "POST", | ||||
|             body: body | ||||
|         }); | ||||
|         if (response.status !== 200) { | ||||
|             throw new Error(`Unexpected server response ${response.status}`); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async getAccountSettings(baseUrl, token) { | ||||
|         const url = accountSettingsUrl(baseUrl); | ||||
|         console.log(`[Api] Fetching user account ${url}`); | ||||
|         const response = await fetch(url, { | ||||
|             headers: maybeWithBearerAuth({}, token) | ||||
|  | @ -173,8 +189,8 @@ class Api { | |||
|         return account; | ||||
|     } | ||||
| 
 | ||||
|     async updateUserAccount(baseUrl, token, payload) { | ||||
|         const url = userAccountUrl(baseUrl); | ||||
|     async updateAccountSettings(baseUrl, token, payload) { | ||||
|         const url = accountSettingsUrl(baseUrl); | ||||
|         const body = JSON.stringify(payload); | ||||
|         console.log(`[Api] Updating user account ${url}: ${body}`); | ||||
|         const response = await fetch(url, { | ||||
|  | @ -187,8 +203,8 @@ class Api { | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async userSubscriptionAdd(baseUrl, token, payload) { | ||||
|         const url = userSubscriptionUrl(baseUrl); | ||||
|     async addAccountSubscription(baseUrl, token, payload) { | ||||
|         const url = accountSubscriptionUrl(baseUrl); | ||||
|         const body = JSON.stringify(payload); | ||||
|         console.log(`[Api] Adding user subscription ${url}: ${body}`); | ||||
|         const response = await fetch(url, { | ||||
|  | @ -204,8 +220,8 @@ class Api { | |||
|         return subscription; | ||||
|     } | ||||
| 
 | ||||
|     async userSubscriptionDelete(baseUrl, token, remoteId) { | ||||
|         const url = userSubscriptionDeleteUrl(baseUrl, remoteId); | ||||
|     async deleteAccountSubscription(baseUrl, token, remoteId) { | ||||
|         const url = accountSubscriptionSingleUrl(baseUrl, remoteId); | ||||
|         console.log(`[Api] Removing user subscription ${url}`); | ||||
|         const response = await fetch(url, { | ||||
|             method: "DELETE", | ||||
|  |  | |||
|  | @ -19,10 +19,11 @@ export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJ | |||
| export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`; | ||||
| export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic)); | ||||
| export const userStatsUrl = (baseUrl) => `${baseUrl}/user/stats`; | ||||
| export const userTokenUrl = (baseUrl) => `${baseUrl}/user/token`; | ||||
| export const userAccountUrl = (baseUrl) => `${baseUrl}/user/account`; | ||||
| export const userSubscriptionUrl = (baseUrl) => `${baseUrl}/user/subscription`; | ||||
| export const userSubscriptionDeleteUrl = (baseUrl, id) => `${baseUrl}/user/subscription/${id}`; | ||||
| export const accountUrl = (baseUrl) => `${baseUrl}/v1/account`; | ||||
| export const accountTokenUrl = (baseUrl) => `${baseUrl}/v1/account/token`; | ||||
| export const accountSettingsUrl = (baseUrl) => `${baseUrl}/v1/account/settings`; | ||||
| export const accountSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/subscription`; | ||||
| export const accountSubscriptionSingleUrl = (baseUrl, id) => `${baseUrl}/v1/account/subscription/${id}`; | ||||
| export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, ""); | ||||
| export const expandUrl = (url) => [`https://${url}`, `http://${url}`]; | ||||
| export const expandSecureUrl = (url) => `https://${url}`; | ||||
|  |  | |||
|  | @ -115,7 +115,7 @@ const SettingsIcons = (props) => { | |||
|         handleClose(event); | ||||
|         await subscriptionManager.remove(props.subscription.id); | ||||
|         if (session.exists() && props.subscription.remoteId) { | ||||
|             await api.userSubscriptionDelete("http://localhost:2586", session.token(), props.subscription.remoteId); | ||||
|             await api.deleteAccountSubscription("http://localhost:2586", session.token(), props.subscription.remoteId); | ||||
|         } | ||||
|         const newSelected = await subscriptionManager.first(); // May be undefined
 | ||||
|         if (newSelected) { | ||||
|  |  | |||
|  | @ -91,7 +91,7 @@ const Layout = () => { | |||
| 
 | ||||
|     useEffect(() => { | ||||
|         (async () => { | ||||
|             const account = await api.userAccount("http://localhost:2586", session.token()); | ||||
|             const account = await api.getAccountSettings("http://localhost:2586", session.token()); | ||||
|             if (account) { | ||||
|                 if (account.language) { | ||||
|                     await i18n.changeLanguage(account.language); | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ import Box from "@mui/material/Box"; | |||
| import api from "../app/Api"; | ||||
| import routes from "./routes"; | ||||
| import session from "../app/Session"; | ||||
| import logo from "../img/ntfy2.svg"; | ||||
| import {NavLink} from "react-router-dom"; | ||||
| 
 | ||||
| const Login = () => { | ||||
|     const handleSubmit = async (event) => { | ||||
|  | @ -24,68 +26,59 @@ const Login = () => { | |||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     marginTop: 8, | ||||
|                     display: 'flex', | ||||
|                     flexDirection: 'column', | ||||
|                     alignItems: 'center', | ||||
|                 }} | ||||
|             > | ||||
|                 <Avatar sx={{m: 1, bgcolor: 'secondary.main'}}> | ||||
|                     <LockOutlinedIcon/> | ||||
|                 </Avatar> | ||||
|                 <Typography component="h1" variant="h5"> | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 display: 'flex', | ||||
|                 flexGrow: 1, | ||||
|                 justifyContent: 'center', | ||||
|                 flexDirection: 'column', | ||||
|                 alignContent: 'center', | ||||
|                 alignItems: 'center', | ||||
|                 height: '100vh' | ||||
|             }} | ||||
|         > | ||||
|             <Avatar | ||||
|                 sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} | ||||
|                 src={logo} | ||||
|                 variant="rounded" | ||||
|             /> | ||||
|             <Typography sx={{ typography: 'h6' }}> | ||||
|                 Sign in to your ntfy account | ||||
|             </Typography> | ||||
|             <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     required | ||||
|                     fullWidth | ||||
|                     id="username" | ||||
|                     label="Username" | ||||
|                     name="username" | ||||
|                     autoFocus | ||||
|                 /> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     required | ||||
|                     fullWidth | ||||
|                     name="password" | ||||
|                     label="Password" | ||||
|                     type="password" | ||||
|                     id="password" | ||||
|                     autoComplete="current-password" | ||||
|                 /> | ||||
|                 <Button | ||||
|                     type="submit" | ||||
|                     fullWidth | ||||
|                     variant="contained" | ||||
|                     sx={{mt: 2, mb: 2}} | ||||
|                 > | ||||
|                     Sign in | ||||
|                 </Typography> | ||||
|                 <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1}}> | ||||
|                     <TextField | ||||
|                         margin="normal" | ||||
|                         required | ||||
|                         fullWidth | ||||
|                         id="username" | ||||
|                         label="Username" | ||||
|                         name="username" | ||||
|                         autoFocus | ||||
|                     /> | ||||
|                     <TextField | ||||
|                         margin="normal" | ||||
|                         required | ||||
|                         fullWidth | ||||
|                         name="password" | ||||
|                         label="Password" | ||||
|                         type="password" | ||||
|                         id="password" | ||||
|                         autoComplete="current-password" | ||||
|                     /> | ||||
|                     <FormControlLabel | ||||
|                         control={<Checkbox value="remember" color="primary"/>} | ||||
|                         label="Remember me" | ||||
|                     /> | ||||
|                     <Button | ||||
|                         type="submit" | ||||
|                         fullWidth | ||||
|                         variant="contained" | ||||
|                         sx={{mt: 3, mb: 2}} | ||||
|                     > | ||||
|                         Sign In | ||||
|                     </Button> | ||||
|                     <Grid container> | ||||
|                         <Grid item xs> | ||||
|                             <Link href="#" variant="body2"> | ||||
|                                 Forgot password? | ||||
|                             </Link> | ||||
|                         </Grid> | ||||
|                         <Grid item> | ||||
|                             <Link to={routes.signup} variant="body2"> | ||||
|                                 {"Don't have an account? Sign Up"} | ||||
|                             </Link> | ||||
|                         </Grid> | ||||
|                     </Grid> | ||||
|                 </Button> | ||||
|                 <Box sx={{width: "100%"}}> | ||||
|                     <NavLink to="#" variant="body1" sx={{float: "left"}}>Reset password</NavLink> | ||||
|                     <div style={{float: "right"}}><NavLink to={routes.signup} variant="body1">Sign Up</NavLink></div> | ||||
|                 </Box> | ||||
|             </Box> | ||||
|         </> | ||||
|         </Box> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,7 +73,7 @@ const Sound = () => { | |||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setSound(ev.target.value); | ||||
|         if (session.exists()) { | ||||
|             await api.updateUserAccount("http://localhost:2586", session.token(), { | ||||
|             await api.updateAccountSettings("http://localhost:2586", session.token(), { | ||||
|                 notification: { | ||||
|                     sound: ev.target.value | ||||
|                 } | ||||
|  | @ -113,7 +113,7 @@ const MinPriority = () => { | |||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setMinPriority(ev.target.value); | ||||
|         if (session.exists()) { | ||||
|             await api.updateUserAccount("http://localhost:2586", session.token(), { | ||||
|             await api.updateAccountSettings("http://localhost:2586", session.token(), { | ||||
|                 notification: { | ||||
|                     min_priority: ev.target.value | ||||
|                 } | ||||
|  | @ -163,7 +163,7 @@ const DeleteAfter = () => { | |||
|     const handleChange = async (ev) => { | ||||
|         await prefs.setDeleteAfter(ev.target.value); | ||||
|         if (session.exists()) { | ||||
|             await api.updateUserAccount("http://localhost:2586", session.token(), { | ||||
|             await api.updateAccountSettings("http://localhost:2586", session.token(), { | ||||
|                 notification: { | ||||
|                     delete_after: ev.target.value | ||||
|                 } | ||||
|  | @ -467,7 +467,7 @@ const Language = () => { | |||
|     const handleChange = async (ev) => { | ||||
|         await i18n.changeLanguage(ev.target.value); | ||||
|         if (session.exists()) { | ||||
|             await api.updateUserAccount("http://localhost:2586", session.token(), { | ||||
|             await api.updateAccountSettings("http://localhost:2586", session.token(), { | ||||
|                 language: ev.target.value | ||||
|             }); | ||||
|         } | ||||
|  |  | |||
|  | @ -1,24 +1,27 @@ | |||
| import * as React from 'react'; | ||||
| import {Avatar, Checkbox, FormControlLabel, Grid, Link, Stack} from "@mui/material"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import Container from "@mui/material/Container"; | ||||
| import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; | ||||
| import {Avatar, Link} from "@mui/material"; | ||||
| import TextField from "@mui/material/TextField"; | ||||
| import Button from "@mui/material/Button"; | ||||
| import Box from "@mui/material/Box"; | ||||
| import api from "../app/Api"; | ||||
| import {useNavigate} from "react-router-dom"; | ||||
| import routes from "./routes"; | ||||
| import session from "../app/Session"; | ||||
| import logo from "../img/ntfy2.svg"; | ||||
| import Typography from "@mui/material/Typography"; | ||||
| import {NavLink} from "react-router-dom"; | ||||
| 
 | ||||
| const Signup = () => { | ||||
|     const handleSubmit = async (event) => { | ||||
|         event.preventDefault(); | ||||
|         const data = new FormData(event.currentTarget); | ||||
|         const username = data.get('username'); | ||||
|         const password = data.get('password'); | ||||
|         const user = { | ||||
|             username: data.get('username'), | ||||
|             password: data.get('password'), | ||||
|         } | ||||
|             username: username, | ||||
|             password: password | ||||
|         }; // FIXME omg so awful
 | ||||
| 
 | ||||
|         await api.createAccount("http://localhost:2586"/*window.location.origin*/, username, password); | ||||
|         const token = await api.login("http://localhost:2586"/*window.location.origin*/, user); | ||||
|         console.log(`[Api] User auth for user ${user.username} successful, token is ${token}`); | ||||
|         session.store(user.username, token); | ||||
|  | @ -26,68 +29,69 @@ const Signup = () => { | |||
|     }; | ||||
| 
 | ||||
|     return ( | ||||
|         <> | ||||
|             <Box | ||||
|                 sx={{ | ||||
|                     marginTop: 8, | ||||
|                     display: 'flex', | ||||
|                     flexDirection: 'column', | ||||
|                     alignItems: 'center', | ||||
|                 }} | ||||
|             > | ||||
|                 <Avatar sx={{m: 1, bgcolor: 'secondary.main'}}> | ||||
|                     <LockOutlinedIcon/> | ||||
|                 </Avatar> | ||||
|                 <Typography component="h1" variant="h5"> | ||||
|                     Sign in | ||||
|                 </Typography> | ||||
|                 <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1}}> | ||||
|                     <TextField | ||||
|                         margin="normal" | ||||
|                         required | ||||
|                         fullWidth | ||||
|                         id="username" | ||||
|                         label="Username" | ||||
|                         name="username" | ||||
|                         autoFocus | ||||
|                     /> | ||||
|                     <TextField | ||||
|                         margin="normal" | ||||
|                         required | ||||
|                         fullWidth | ||||
|                         name="password" | ||||
|                         label="Password" | ||||
|                         type="password" | ||||
|                         id="password" | ||||
|                         autoComplete="current-password" | ||||
|                     /> | ||||
|                     <FormControlLabel | ||||
|                         control={<Checkbox value="remember" color="primary"/>} | ||||
|                         label="Remember me" | ||||
|                     /> | ||||
|                     <Button | ||||
|                         type="submit" | ||||
|                         fullWidth | ||||
|                         variant="contained" | ||||
|                         sx={{mt: 3, mb: 2}} | ||||
|                     > | ||||
|                         Sign up | ||||
|                     </Button> | ||||
|                     <Grid container> | ||||
|                         <Grid item xs> | ||||
|                             <Link href="#" variant="body2"> | ||||
|                                 Forgot password? | ||||
|                             </Link> | ||||
|                         </Grid> | ||||
|                         <Grid item> | ||||
|                             <Link to={routes.signup} variant="body2"> | ||||
|                                 {"Don't have an account? Sign Up"} | ||||
|                             </Link> | ||||
|                         </Grid> | ||||
|                     </Grid> | ||||
|                 </Box> | ||||
|         <Box | ||||
|             sx={{ | ||||
|                 display: 'flex', | ||||
|                 flexGrow: 1, | ||||
|                 justifyContent: 'center', | ||||
|                 flexDirection: 'column', | ||||
|                 alignContent: 'center', | ||||
|                 alignItems: 'center', | ||||
|                 height: '100vh' | ||||
|             }} | ||||
|         > | ||||
|             <Avatar | ||||
|                 sx={{ m: 2, width: 64, height: 64, borderRadius: 3 }} | ||||
|                 src={logo} | ||||
|                 variant="rounded" | ||||
|             /> | ||||
|             <Typography sx={{ typography: 'h6' }}> | ||||
|                 Create a ntfy account | ||||
|             </Typography> | ||||
|             <Box component="form" onSubmit={handleSubmit} noValidate sx={{mt: 1, maxWidth: 400}}> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     required | ||||
|                     fullWidth | ||||
|                     id="username" | ||||
|                     label="Username" | ||||
|                     name="username" | ||||
|                     autoFocus | ||||
|                 /> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     required | ||||
|                     fullWidth | ||||
|                     name="password" | ||||
|                     label="Password" | ||||
|                     type="password" | ||||
|                     id="password" | ||||
|                     autoComplete="current-password" | ||||
|                 /> | ||||
|                 <TextField | ||||
|                     margin="dense" | ||||
|                     required | ||||
|                     fullWidth | ||||
|                     name="confirm-password" | ||||
|                     label="Confirm password" | ||||
|                     type="password" | ||||
|                     id="confirm-password" | ||||
|                 /> | ||||
|                 <Button | ||||
|                     type="submit" | ||||
|                     fullWidth | ||||
|                     variant="contained" | ||||
|                     sx={{mt: 2, mb: 2}} | ||||
|                 > | ||||
|                     Sign up | ||||
|                 </Button> | ||||
|             </Box> | ||||
|         </> | ||||
|             <Typography sx={{mb: 4}}> | ||||
|                 <NavLink to={routes.login} variant="body1"> | ||||
|                     Already have an account? Sign in | ||||
|                 </NavLink> | ||||
|             </Typography> | ||||
|         </Box> | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ const SiteLayout = (props) => { | |||
|                         <li><NavLink to={routes.home} activeStyle>Features</NavLink></li> | ||||
|                         <li><NavLink to={routes.pricing} activeStyle>Pricing</NavLink></li> | ||||
|                         <li><NavLink to="/docs" reloadDocument={true} activeStyle>Docs</NavLink></li> | ||||
|                         {session.exists() && <li><NavLink to={routes.signup} activeStyle>Sign up</NavLink></li>} | ||||
|                         {!session.exists() && <li><NavLink to={routes.signup} activeStyle>Sign up</NavLink></li>} | ||||
|                         {!session.exists() && <li><NavLink to={routes.login} activeStyle>Login</NavLink></li>} | ||||
|                         <li><NavLink to={routes.app} activeStyle>Open app</NavLink></li> | ||||
|                     </ol> | ||||
|  |  | |||
|  | @ -28,7 +28,7 @@ const SubscribeDialog = (props) => { | |||
|         const actualBaseUrl = (baseUrl) ? baseUrl : window.location.origin; | ||||
|         const subscription = await subscriptionManager.add(actualBaseUrl, topic); | ||||
|         if (session.exists()) { | ||||
|             const remoteSubscription = await api.userSubscriptionAdd("http://localhost:2586", session.token(), { | ||||
|             const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), { | ||||
|                 base_url: actualBaseUrl, | ||||
|                 topic: topic | ||||
|             }); | ||||
|  |  | |||
|  | @ -64,7 +64,7 @@ export const useAutoSubscribe = (subscriptions, selected) => { | |||
|             (async () => { | ||||
|                 const subscription = await subscriptionManager.add(baseUrl, params.topic); | ||||
|                 if (session.exists()) { | ||||
|                     const remoteSubscription = await api.userSubscriptionAdd("http://localhost:2586", session.token(), { | ||||
|                     const remoteSubscription = await api.addAccountSubscription("http://localhost:2586", session.token(), { | ||||
|                         base_url: baseUrl, | ||||
|                         topic: params.topic | ||||
|                     }); | ||||
|  |  | |||
|  | @ -1,255 +1 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <!-- Created with Inkscape (http://www.inkscape.org/) --> | ||||
| 
 | ||||
| <svg | ||||
|    width="50mm" | ||||
|    height="50mm" | ||||
|    viewBox="0 0 50 50" | ||||
|    version="1.1" | ||||
|    id="svg8" | ||||
|    inkscape:version="1.1.1 (3bf5ae0, 2021-09-20)" | ||||
|    sodipodi:docname="appstore_ios.svg" | ||||
|    inkscape:export-filename="/home/pheckel/Code/ntfy-android/assets/appstore_ios.png" | ||||
|    inkscape:export-xdpi="520.19202" | ||||
|    inkscape:export-ydpi="520.19202" | ||||
|    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||
|    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||
|    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/"> | ||||
|   <defs | ||||
|      id="defs2"> | ||||
|     <linearGradient | ||||
|        inkscape:collect="always" | ||||
|        id="linearGradient4714"> | ||||
|       <stop | ||||
|          style="stop-color:#348878;stop-opacity:1" | ||||
|          offset="0" | ||||
|          id="stop4710" /> | ||||
|       <stop | ||||
|          style="stop-color:#52bca6;stop-opacity:1" | ||||
|          offset="1" | ||||
|          id="stop4712" /> | ||||
|     </linearGradient> | ||||
|     <linearGradient | ||||
|        inkscape:collect="always" | ||||
|        id="linearGradient28858-5"> | ||||
|       <stop | ||||
|          style="stop-color:#348878;stop-opacity:1" | ||||
|          offset="0" | ||||
|          id="stop28854-3" /> | ||||
|       <stop | ||||
|          style="stop-color:#56bda8;stop-opacity:1" | ||||
|          offset="1" | ||||
|          id="stop28856-5" /> | ||||
|     </linearGradient> | ||||
|     <linearGradient | ||||
|        inkscape:collect="always" | ||||
|        xlink:href="#linearGradient28858-5" | ||||
|        id="linearGradient3255" | ||||
|        x1="160.72209" | ||||
|        y1="128.53317" | ||||
|        x2="168.41153" | ||||
|        y2="134.32626" | ||||
|        gradientUnits="userSpaceOnUse" | ||||
|        gradientTransform="matrix(3.7495873,0,0,3.7495873,-541.79055,-387.59852)" /> | ||||
|     <linearGradient | ||||
|        inkscape:collect="always" | ||||
|        xlink:href="#linearGradient4714" | ||||
|        id="linearGradient4633" | ||||
|        x1="0.034492966" | ||||
|        y1="-0.0003150744" | ||||
|        x2="50.319355" | ||||
|        y2="50.284546" | ||||
|        gradientUnits="userSpaceOnUse" | ||||
|        gradientTransform="matrix(0.99433502,0,0,0.99433502,-0.03429756,-1.7848888e-6)" /> | ||||
|     <filter | ||||
|        style="color-interpolation-filters:sRGB;" | ||||
|        inkscape:label="Drop Shadow" | ||||
|        id="filter3958" | ||||
|        x="-0.076083149" | ||||
|        y="-0.091641662" | ||||
|        width="1.1759423" | ||||
|        height="1.2114791"> | ||||
|       <feFlood | ||||
|          flood-opacity="0.192157" | ||||
|          flood-color="rgb(0,0,0)" | ||||
|          result="flood" | ||||
|          id="feFlood3948" /> | ||||
|       <feComposite | ||||
|          in="flood" | ||||
|          in2="SourceGraphic" | ||||
|          operator="in" | ||||
|          result="composite1" | ||||
|          id="feComposite3950" /> | ||||
|       <feGaussianBlur | ||||
|          in="composite1" | ||||
|          stdDeviation="4" | ||||
|          result="blur" | ||||
|          id="feGaussianBlur3952" /> | ||||
|       <feOffset | ||||
|          dx="3" | ||||
|          dy="2.95367" | ||||
|          result="offset" | ||||
|          id="feOffset3954" /> | ||||
|       <feComposite | ||||
|          in="SourceGraphic" | ||||
|          in2="offset" | ||||
|          operator="over" | ||||
|          result="composite2" | ||||
|          id="feComposite3956" /> | ||||
|     </filter> | ||||
|   </defs> | ||||
|   <sodipodi:namedview | ||||
|      id="base" | ||||
|      pagecolor="#ffffff" | ||||
|      bordercolor="#666666" | ||||
|      borderopacity="1.0" | ||||
|      inkscape:pageopacity="0.0" | ||||
|      inkscape:pageshadow="2" | ||||
|      inkscape:zoom="1.8244841" | ||||
|      inkscape:cx="4.6588512" | ||||
|      inkscape:cy="174.84395" | ||||
|      inkscape:document-units="mm" | ||||
|      inkscape:current-layer="layer3" | ||||
|      showgrid="false" | ||||
|      inkscape:measure-start="0,0" | ||||
|      inkscape:measure-end="0,0" | ||||
|      inkscape:snap-text-baseline="true" | ||||
|      inkscape:window-width="1863" | ||||
|      inkscape:window-height="1025" | ||||
|      inkscape:window-x="57" | ||||
|      inkscape:window-y="27" | ||||
|      inkscape:window-maximized="1" | ||||
|      fit-margin-top="0" | ||||
|      fit-margin-left="0" | ||||
|      fit-margin-right="0" | ||||
|      fit-margin-bottom="0" | ||||
|      showguides="false" | ||||
|      inkscape:guide-bbox="true" | ||||
|      inkscape:pagecheckerboard="0"> | ||||
|     <sodipodi:guide | ||||
|        position="10.173514,67.718331" | ||||
|        orientation="1,0" | ||||
|        id="guide1770" /> | ||||
|     <sodipodi:guide | ||||
|        position="39.965574,62.077508" | ||||
|        orientation="1,0" | ||||
|        id="guide1772" /> | ||||
|     <sodipodi:guide | ||||
|        position="10.173514,39.789015" | ||||
|        orientation="0,-1" | ||||
|        id="guide1774" /> | ||||
|     <sodipodi:guide | ||||
|        position="-2.3077334,9.9462015" | ||||
|        orientation="0,-1" | ||||
|        id="guide1776" /> | ||||
|     <sodipodi:guide | ||||
|        position="14.990626,36.198285" | ||||
|        orientation="1,0" | ||||
|        id="guide4020" /> | ||||
|     <sodipodi:guide | ||||
|        position="34.930725,39.789015" | ||||
|        orientation="1,0" | ||||
|        id="guide4022" /> | ||||
|     <sodipodi:guide | ||||
|        position="12.7026,32.00465" | ||||
|        orientation="0,-1" | ||||
|        id="guide4024" /> | ||||
|     <sodipodi:guide | ||||
|        position="11.377711,17.981227" | ||||
|        orientation="0,-1" | ||||
|        id="guide4026" /> | ||||
|   </sodipodi:namedview> | ||||
|   <metadata | ||||
|      id="metadata5"> | ||||
|     <rdf:RDF> | ||||
|       <cc:Work | ||||
|          rdf:about=""> | ||||
|         <dc:format>image/svg+xml</dc:format> | ||||
|         <dc:type | ||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||
|       </cc:Work> | ||||
|     </rdf:RDF> | ||||
|   </metadata> | ||||
|   <g | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer2" | ||||
|      inkscape:label="background" | ||||
|      style="display:inline"> | ||||
|     <rect | ||||
|        style="fill:url(#linearGradient4633);fill-opacity:1;stroke:none;stroke-width:0.286502;stroke-linejoin:bevel" | ||||
|        id="rect4545" | ||||
|        width="50" | ||||
|        height="50" | ||||
|        x="0" | ||||
|        y="-0.0003150744" /> | ||||
|   </g> | ||||
|   <g | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer5" | ||||
|      inkscape:label="drop shadow" | ||||
|      style="display:inline"> | ||||
|     <path | ||||
|        id="path3646" | ||||
|        style="color:#000000;display:inline;fill:#ffffff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#filter3958)" | ||||
|        d="m 50.400391,46.882812 c -9.16879,0 -17.023438,7.2146 -17.023438,16.386719 v 0.0078 l 0.08984,71.369139 -2.302735,16.99219 31.3125,-8.31836 h 77.841802 c 9.16877,0 17.02344,-7.22425 17.02344,-16.39648 V 63.269531 c 0,-9.169496 -7.85031,-16.382463 -17.01563,-16.386719 h -0.008 z m 0,11.566407 h 89.917969 0.008 c 3.22151,0.0033 5.44922,2.346918 5.44922,4.820312 v 63.654299 c 0,2.47551 -2.23164,4.82031 -5.45703,4.82031 H 60.779297 l -15.908203,4.80664 0.162109,-0.9375 -0.08789,-72.343749 c 0,-2.475337 2.229739,-4.820312 5.455078,-4.820312 z" | ||||
|        transform="scale(0.26458333)" /> | ||||
|   </g> | ||||
|   <g | ||||
|      inkscape:label="foreground" | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer1" | ||||
|      transform="translate(-51.147327,-81.515579)" | ||||
|      style="display:inline"> | ||||
|     <path | ||||
|        style="color:#000000;fill:url(#linearGradient3255);stroke:none;stroke-width:2.49558;-inkscape-stroke:none" | ||||
|        d="M 88.200706,95.308804 H 64.918622 c -1.600657,0 -2.910245,1.235977 -2.910245,2.74661 l 0.02224,18.601596 -0.434711,2.5057 6.231592,-1.88118 h 20.371766 c 1.600658,0 2.910282,-1.23597 2.910282,-2.74664 V 98.055414 c 0,-1.510633 -1.309624,-2.74661 -2.910282,-2.74661 z" | ||||
|        id="path7368" /> | ||||
|     <path | ||||
|        id="path2498" | ||||
|        style="color:#000000;fill:#ffffff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none" | ||||
|        d="m 50.400391,46.882812 c -9.16879,0 -17.023438,7.2146 -17.023438,16.386719 v 0.0078 l 0.08984,71.369139 -2.302735,16.99219 31.3125,-8.31836 h 77.841802 c 9.16877,0 17.02344,-7.22425 17.02344,-16.39648 V 63.269531 c 0,-9.169496 -7.85031,-16.382463 -17.01563,-16.386719 h -0.008 z m 0,11.566407 h 89.917969 0.008 c 3.22151,0.0033 5.44922,2.346918 5.44922,4.820312 v 63.654299 c 0,2.47551 -2.23164,4.82031 -5.45703,4.82031 H 60.779297 l -15.908203,4.80664 0.162109,-0.9375 -0.08789,-72.343749 c 0,-2.475337 2.229739,-4.820312 5.455078,-4.820312 z" | ||||
|        transform="matrix(0.26458333,0,0,0.26458333,51.147327,81.515579)" /> | ||||
|     <g | ||||
|        id="path1011-6-2" | ||||
|        transform="matrix(1.4536603,0,0,1.728146,-23.97473,-90.437157)" | ||||
|        style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke:none;stroke-width:0.525121"> | ||||
|       <path | ||||
|          style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none" | ||||
|          d="m 62.57046,116.77004 v -1.31201 l 3.280018,-1.45904 q 0.158346,-0.0679 0.305381,-0.1018 0.158346,-0.0452 0.282761,-0.0679 0.135725,-0.0113 0.271449,-0.0226 v -0.0905 q -0.135724,-0.0113 -0.271449,-0.0452 -0.124415,-0.0226 -0.282761,-0.0566 -0.147035,-0.0452 -0.305381,-0.1131 l -3.280018,-1.45904 v -1.32332 l 5.067063,2.31863 v 1.4138 z" | ||||
|          id="path7553" /> | ||||
|       <path | ||||
|          style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none" | ||||
|          d="m 62.308594,110.31055 v 1.90234 l 3.4375,1.5293 c 0.0073,0.003 0.0142,0.005 0.02148,0.008 -0.0073,0.003 -0.0142,0.005 -0.02148,0.008 l -3.4375,1.5293 v 1.89258 l 0.371093,-0.16992 5.220704,-2.39063 v -1.75 z m 0.52539,0.8164 4.541016,2.08008 v 1.07617 l -4.541016,2.07813 v -0.73242 l 3.119141,-1.38868 0.0039,-0.002 c 0.09141,-0.0389 0.178343,-0.0676 0.257813,-0.0859 h 0.0059 l 0.0078,-0.002 c 0.09483,-0.0271 0.176055,-0.0474 0.246093,-0.0606 l 0.498047,-0.041 v -0.57422 l -0.240234,-0.0195 c -0.07606,-0.006 -0.153294,-0.0198 -0.230469,-0.0391 l -0.0078,-0.002 -0.0078,-0.002 c -0.07608,-0.0138 -0.16556,-0.0318 -0.263672,-0.0527 -0.08398,-0.0262 -0.172736,-0.058 -0.265625,-0.0977 l -0.0039,-0.002 -3.119141,-1.38868 z" | ||||
|          id="path7555" /> | ||||
|     </g> | ||||
|     <g | ||||
|        id="g1224" | ||||
|        transform="matrix(1.4493527,0,0,1.6641427,-22.956963,-85.389973)" | ||||
|        style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;stroke:none;stroke-width:0.525121"> | ||||
|       <path | ||||
|          style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none" | ||||
|          d="m 69.17132,117.75404 h 5.428996 v 1.27808 H 69.17132 Z" | ||||
|          id="path1220" /> | ||||
|       <path | ||||
|          style="color:#000000;-inkscape-font-specification:'JetBrains Mono, Bold';fill:#ffffff;stroke:none;-inkscape-stroke:none" | ||||
|          d="m 68.908203,117.49219 v 0.26172 1.54101 h 5.955078 v -1.80273 z m 0.525391,0.52344 h 4.904297 v 0.7539 h -4.904297 z" | ||||
|          id="path1222" /> | ||||
|     </g> | ||||
|   </g> | ||||
|   <g | ||||
|      inkscape:groupmode="layer" | ||||
|      id="layer3" | ||||
|      inkscape:label="round icon preview" | ||||
|      style="display:none"> | ||||
|     <path | ||||
|        id="path18850-8-1" | ||||
|        style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:0.255654" | ||||
|        d="M 50.337488,80.973198 V 131.61213 H 101.65302 V 80.973198 Z m 25.676545,1.442307 h 0.555989 a 24.369387,24.369387 0 0 1 23.860308,21.232925 v 6.09963 a 24.369387,24.369387 0 0 1 -21.288308,21.19336 h 21.288308 v 0.0138 H 51.963792 v -0.0158 H 73.428179 A 24.369387,24.369387 0 0 1 51.963792,107.97535 v -2.49089 A 24.369387,24.369387 0 0 1 76.014033,82.415508 Z" | ||||
|        transform="translate(-51.147326,-81.51558)" /> | ||||
|   </g> | ||||
| </svg> | ||||
| <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="50mm" height="50mm" viewBox="0 0 50 50"><defs><linearGradient id="b"><stop offset="0" style="stop-color:#348878;stop-opacity:1"/><stop offset="1" style="stop-color:#52bca6;stop-opacity:1"/></linearGradient><linearGradient id="a"><stop offset="0" style="stop-color:#348878;stop-opacity:1"/><stop offset="1" style="stop-color:#56bda8;stop-opacity:1"/></linearGradient><linearGradient xlink:href="#a" id="e" x1="160.722" x2="168.412" y1="128.533" y2="134.326" gradientTransform="matrix(3.74959 0 0 3.74959 -541.79 -387.599)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#b" id="c" x1=".034" x2="50.319" y1="0" y2="50.285" gradientTransform="matrix(.99434 0 0 .99434 -.034 0)" gradientUnits="userSpaceOnUse"/><filter id="d" width="1.176" height="1.211" x="-.076" y="-.092" style="color-interpolation-filters:sRGB"><feFlood flood-color="#000" flood-opacity=".192" result="flood"/><feComposite in="flood" in2="SourceGraphic" operator="in" result="composite1"/><feGaussianBlur in="composite1" result="blur" stdDeviation="4"/><feOffset dx="3" dy="2.954" result="offset"/><feComposite in="SourceGraphic" in2="offset" result="composite2"/></filter></defs><g style="display:inline"><path d="M0 0h50v50H0z" style="fill:url(#c);fill-opacity:1;stroke:none;stroke-width:.286502;stroke-linejoin:bevel"/></g><g style="display:inline"><path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#000;display:inline;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none;filter:url(#d)" transform="scale(.26458)"/></g><g style="display:inline"><path d="M88.2 95.309H64.92c-1.601 0-2.91 1.236-2.91 2.746l.022 18.602-.435 2.506 6.231-1.881H88.2c1.6 0 2.91-1.236 2.91-2.747v-16.48c0-1.51-1.31-2.746-2.91-2.746z" style="color:#000;fill:url(#e);stroke:none;stroke-width:2.49558;-inkscape-stroke:none" transform="translate(-51.147 -81.516)"/><path d="M50.4 46.883c-9.168 0-17.023 7.214-17.023 16.387v.007l.09 71.37-2.303 16.992 31.313-8.319h77.841c9.17 0 17.024-7.224 17.024-16.396V63.27c0-9.17-7.85-16.383-17.016-16.387h-.008zm0 11.566h89.926c3.222.004 5.45 2.347 5.45 4.82v63.655c0 2.475-2.232 4.82-5.457 4.82h-79.54l-15.908 4.807.162-.938-.088-72.343c0-2.476 2.23-4.82 5.455-4.82z" style="color:#000;fill:#fff;stroke:none;stroke-width:1.93113;-inkscape-stroke:none" transform="scale(.26458)"/><g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#000;stroke:none;stroke-width:.525121"><path d="M62.57 116.77v-1.312l3.28-1.459q.159-.068.306-.102.158-.045.283-.068l.271-.022v-.09q-.136-.012-.271-.046-.125-.023-.283-.057-.147-.045-.306-.113l-3.28-1.459v-1.323l5.068 2.319v1.413z" style="color:#000;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/><path d="M62.309 110.31v1.903l3.437 1.53.022.007-.022.008-3.437 1.53v1.892l.37-.17 5.221-2.39v-1.75zm.525.817 4.541 2.08v1.076l-4.541 2.078v-.732l3.12-1.389.003-.002a1.56 1.56 0 0 1 .258-.086h.006l.008-.002c.094-.027.176-.047.246-.06l.498-.041v-.574l-.24-.02a1.411 1.411 0 0 1-.231-.04l-.008-.001-.008-.002a9.077 9.077 0 0 1-.263-.053 2.781 2.781 0 0 1-.266-.097l-.004-.002-3.119-1.39z" style="color:#000;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.45366 0 0 1.72815 -75.122 -171.953)"/></g><g style="font-size:8.48274px;font-family:sans-serif;letter-spacing:0;word-spacing:0;fill:#000;stroke:none;stroke-width:.525121"><path d="M69.171 117.754h5.43v1.278h-5.43Z" style="color:#000;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/><path d="M68.908 117.492v1.802h5.955v-1.802zm.526.524h4.904v.754h-4.904z" style="color:#000;-inkscape-font-specification:"JetBrains Mono, Bold";fill:#fff;stroke:none;-inkscape-stroke:none" transform="matrix(1.44935 0 0 1.66414 -74.104 -166.906)"/></g></g></svg> | ||||
| Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.3 KiB | 
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue