├── README.md ├── license.md ├── scriptable-api ├── .gitignore ├── README.md ├── next-env.d.ts ├── package.json ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── hello.js │ └── index.tsx ├── public │ ├── compiled-widgets │ │ ├── widget-modules │ │ │ ├── covid19WidgetModule.js │ │ │ ├── covid19WidgetModule.meta.json │ │ │ ├── covid19WidgetModule.png │ │ │ ├── kitchenSinkWidgetModule.js │ │ │ ├── kitchenSinkWidgetModule.meta.json │ │ │ ├── kitchenSinkWidgetModule.png │ │ │ ├── simpleAnalyticsWidgetModule.js │ │ │ ├── simpleAnalyticsWidgetModule.meta.json │ │ │ ├── simpleAnalyticsWidgetModule.png │ │ │ ├── stickyWidgetModule.js │ │ │ ├── stickyWidgetModule.meta.json │ │ │ └── stickyWidgetModule.png │ │ └── widgetLoader.js │ ├── favicon.ico │ ├── scriptable-hero.png │ └── vercel.svg ├── src │ ├── components │ │ ├── CodeWithClipboard.tsx │ │ └── WidgetModuleCard.tsx │ ├── interfaces.ts │ └── theme.ts ├── tsconfig.json └── yarn.lock └── widgets ├── .gitignore ├── README.md ├── code ├── components │ ├── images │ │ ├── ErrorImage.ts │ │ ├── SparkBarImage.ts │ │ └── UnsplashImage.ts │ ├── stacks │ │ ├── addFlexSpacer.ts │ │ ├── addSymbol.ts │ │ └── addTextWithSymbolStack.ts │ └── widgets │ │ ├── ErrorWidget.ts │ │ ├── SimpleSparkBarWidget.ts │ │ └── SimpleTextWidget.ts ├── utils │ ├── color.ts │ ├── debug-utils.ts │ ├── interfaces.ts │ ├── request-utils.ts │ ├── sizing.ts │ └── widget-loader-utils.ts ├── widget-modules │ ├── README.md │ ├── covid19WidgetModule.ts │ ├── kitchenSinkWidgetModule.ts │ ├── simpleAnalyticsWidgetModule.ts │ └── stickyWidgetModule.ts └── widgetLoader.ts ├── eslint-config ├── eslintrc.json ├── index.js ├── package.json └── readme.md ├── jsconfig.json ├── package.json ├── rollup.config.js └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # Scriptable TS Boilerplate 2 | 3 | Makes creating iOS widgets with the [Scriptable App](https://scriptable.app/) even more fun! 4 | 5 | - 🔥 Hot-loading widgets served by Next.js 6 | - 🔨 The safety of TypeScript 7 | - 🍭 Build, compile, rollup and other configs 8 | - 🚀 Deploy to Vercel with ease 9 | - ✨ Roll out updates to live widgets automatically 10 | 11 | _Note: This is a work in progress_ 12 | 13 | **[Scriptable TS Boilerplate Website →](https://scriptable-ts-boilerplate.vercel.app)** 14 | 15 | ## Project structure 16 | 17 | This boilerplate consists of two separate packages **widgets** and **scriptable-api**. 18 | 19 | ### Widgets 20 | 21 | Houses the dependencies, rollup config and code to write hot-loading widgets in TypeScript. 22 | 23 | All created `WidgetModules` are currently build to be loaded and bootstrapped by the `WidgetLoader`. 24 | 25 | ### Scriptable-api 26 | 27 | A Nextjs project to serve the compiled `WidgetModules` and `WidgetLoader`. Also houses the demo website and can also be used to create custom data endpoints for the widgets. 28 | 29 | ### Data exchange between projects 30 | 31 | The only way data is shared between the two is through the compilation process of **widgets**: this outputs compiled js files into a subdirectory of **scriptable-api**. 32 | 33 | 34 | # Local development 35 | 36 | ## Writing and compiling a new widget 37 | 38 | Start with creating a new widgetModule in `./widgets/code/widget-module` that complies to the following convention: 39 | 40 | ```js camelCasedNameEndingOnWidgetModule.ts 41 | import { IWidgetModule } from "code/utils/interfaces"; 42 | 43 | const widgetModule: IWidgetModule = { 44 | createWidget: async (params) => { 45 | const widget = new ListWidget() 46 | // await data 47 | // do something with `params.widgetParameter` 48 | // create the widget 49 | return widget 50 | } 51 | } 52 | 53 | module.exports = widgetModule; 54 | ``` 55 | 56 | Compile your widget by running either `yarn build` or `yarn watch` in `./widgets` (any file ending with `WidgetModule.ts` will automatically be picked up). 57 | 58 | Read the awesome [official Scriptable Documentation](https://docs.scriptable.app). 59 | 60 | ## Serving the widget 61 | 62 | Serve your widget by running `yarn dev` in `./scriptable-api`. 63 | 64 | Your compiled widget should now be available on `YOUR_LOCAL_DNS_NAME:3000/compiled-widgets/widget-modules/camelCasedName.js`. 65 | 66 | The demo page is also available on `YOUR_LOCAL_DNS_NAME:3000` (without your widget, that requires some additional steps). 67 | 68 | ## Loading the widget on your device for the first time 69 | 70 | Paste a compiled `WidgetLoader` (can be found in `./scriptable-api/public/compiled-widgets/widgetLoader.js`, or on the demo site) into Scriptable with the following `argsConfig` and press play. 71 | ```js 72 | const argsConfig = { 73 | fileName: "camelCasedNameEndingOnWidgetModule", 74 | rootUrl: "http://macbook-pro.local:3000/compiled-widgets/widget-modules/", 75 | defaultWidgetParameter: "", 76 | downloadQueryString: "", 77 | }; 78 | ``` 79 | 80 | Note that the `widgetParameter` is the default parameter to be sent into `createWidget`, this parameter can be overruled by filling it into the setting of a widget. 81 | 82 | ## Iterating the widget 83 | 84 | With `yarn watch` running in `./widgets` and `yarn dev` running in `./scriptable-api` you'll now always run the latest code on your device: 85 | 86 | - `yarn watch` compiles the .ts into a public .js widget 87 | - `yarn dev` will serve this new version of the widget with a new ETag 88 | - the `WidgetLoader` wil notice to ETag change on run and pull in the new version of the code 89 | 90 | # Deployment 91 | 92 | Deployment to Vercel is easiest for the Nextjs app: 93 | 94 | - Clone this project 95 | - Link Github to Vercel and make the new project available to Vercel 96 | - During setup pick the subdirectory `scriptable-api` 97 | - Done! 98 | 99 | ## Updating widgets 100 | 101 | If people installed widgets with the `WidgetLoader` pointing to your deployed instance, simply pushing code to the `main` branch would already let them have the updated code again on next refresh of their device. 102 | 103 | 104 | # Credits 105 | 106 | - The [Scriptable App](https://scriptable.app/) of course 107 | - The idea of bootstrapping widgets: https://gitlab.com/sillium-scriptable-projects/universal-scriptable-widget 108 | - The Sticky example https://github.com/drewkerr/scriptable/blob/main/Sticky%20widget.js 109 | - The Covid19 example: https://gist.github.com/planecore/e7b4c1e5db2dd28b1a023860e831355e -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jasper Hartong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /scriptable-api/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /scriptable-api/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /scriptable-api/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /scriptable-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptable-api", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@material-ui/core": "^4.12.3", 12 | "@material-ui/icons": "^4.11.2", 13 | "@material-ui/lab": "^4.0.0-alpha.60", 14 | "clsx": "^1.1.1", 15 | "highlight.js": "^11.3.1", 16 | "next": "12.3.1", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "use-clipboard-copy": "^0.2.0", 20 | "use-debounce": "^7.0.1" 21 | }, 22 | "devDependencies": { 23 | "@types/highlight.js": "^10.1.0", 24 | "@types/node": "^17.0.5", 25 | "@types/react": "^17.0.38", 26 | "typescript": "^4.5.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /scriptable-api/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from "@material-ui/core/CssBaseline"; 2 | import { createTheme, ThemeProvider } from "@material-ui/core/styles"; 3 | import "highlight.js/styles/monokai-sublime.css"; 4 | 5 | const theme = createTheme({ 6 | palette: { 7 | type: "light", 8 | }, 9 | shape: { 10 | borderRadius: 16, 11 | }, 12 | }); 13 | 14 | function MyApp({ Component, pageProps }) { 15 | return ( 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default MyApp; 24 | -------------------------------------------------------------------------------- /scriptable-api/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { ServerStyleSheets } from '@material-ui/core/styles'; 2 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 3 | import React from 'react'; 4 | import theme from '../src/theme'; 5 | 6 | export default class MyDocument extends Document { 7 | render() { 8 | return ( 9 | 10 | 11 | {/* PWA primary color */} 12 | 13 | 17 | 18 | 19 |
20 | 21 | {/* Start Simple Analytics */} 22 | 23 | 26 | {/* End Simple Analytics */} 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | // `getInitialProps` belongs to `_document` (instead of `_app`), 34 | // it's compatible with server-side generation (SSG). 35 | MyDocument.getInitialProps = async (ctx) => { 36 | // Resolution order 37 | // 38 | // On the server: 39 | // 1. app.getInitialProps 40 | // 2. page.getInitialProps 41 | // 3. document.getInitialProps 42 | // 4. app.render 43 | // 5. page.render 44 | // 6. document.render 45 | // 46 | // On the server with error: 47 | // 1. document.getInitialProps 48 | // 2. app.render 49 | // 3. page.render 50 | // 4. document.render 51 | // 52 | // On the client 53 | // 1. app.getInitialProps 54 | // 2. page.getInitialProps 55 | // 3. app.render 56 | // 4. page.render 57 | 58 | // Render app and page and get the context of the page with collected side effects. 59 | const sheets = new ServerStyleSheets(); 60 | const originalRenderPage = ctx.renderPage; 61 | 62 | ctx.renderPage = () => 63 | originalRenderPage({ 64 | enhanceApp: (App) => (props) => sheets.collect(), 65 | }); 66 | 67 | const initialProps = await Document.getInitialProps(ctx); 68 | 69 | return { 70 | ...initialProps, 71 | // Styles fragment is rendered after the app and page rendering finish. 72 | styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()], 73 | }; 74 | }; -------------------------------------------------------------------------------- /scriptable-api/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | res.json({ name: 'John Doe' }) 6 | } 7 | -------------------------------------------------------------------------------- /scriptable-api/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Container, Grid, Link, makeStyles, Paper, TextField, Typography } from "@material-ui/core" 2 | import LaunchIcon from '@material-ui/icons/Launch' 3 | import { Alert, AlertTitle } from "@material-ui/lab" 4 | import { readFileSync } from "fs" 5 | import { GetStaticProps } from "next" 6 | import Head from 'next/head' 7 | import { resolve } from "path" 8 | import { useEffect, useMemo, useState } from "react" 9 | import { useDebounce } from 'use-debounce' 10 | import { CodeWithClipboard } from "../src/components/CodeWithClipboard" 11 | import { WidgetModuleCard } from "../src/components/WidgetModuleCard" 12 | import { WidgetModule } from "../src/interfaces" 13 | 14 | 15 | 16 | 17 | interface PageProps { 18 | widgetLoader: string, 19 | widgetModules: WidgetModule[] 20 | } 21 | 22 | 23 | 24 | const useStyles = makeStyles(theme => ({ 25 | header: { 26 | background: theme.palette.primary.main, 27 | color: "white" 28 | }, 29 | headerText: { 30 | paddingTop: theme.spacing(6), 31 | paddingBottom: theme.spacing(6), 32 | }, 33 | heroImage: { 34 | backgroundImage: "url(/scriptable-hero.png)", 35 | backgroundSize: "cover", 36 | width: "100%", 37 | paddingBottom: "109%", 38 | }, 39 | alert: { 40 | marginTop: theme.spacing(4), 41 | marginBottom: theme.spacing(4) 42 | }, 43 | cardsContainer: { 44 | display: "flex", 45 | overflow: "auto", 46 | flexWrap: "nowrap", 47 | alignItems: "center", 48 | marginBottom: theme.spacing(4), 49 | marginLeft: theme.spacing(-1), 50 | }, 51 | paperComingSoon: { 52 | height: 154, 53 | minWidth: 280, 54 | textAlign: "center" 55 | }, 56 | textField: { 57 | marginTop: theme.spacing(2), 58 | marginBottom: theme.spacing(4), 59 | width: "100%", 60 | maxWidth: 420 61 | }, 62 | graySection: { 63 | background: theme.palette.action.hover, 64 | paddingTop: theme.spacing(8), 65 | paddingBottom: theme.spacing(6), 66 | }, 67 | whiteSection: { 68 | background: theme.palette.background.default, 69 | paddingTop: theme.spacing(6), 70 | paddingBottom: theme.spacing(6), 71 | } 72 | })) 73 | 74 | 75 | const setWidgetModule = (widgetLoader: string, rootUrl: string, widgetModule?: WidgetModule, widgetParameter?: string) => { 76 | if (!widgetModule) { 77 | return widgetLoader; 78 | } 79 | let filled = widgetLoader; 80 | const { moduleName, meta } = widgetModule; 81 | const args = { moduleName, ...meta.loaderArgs, rootUrl: rootUrl, defaultWidgetParameter: widgetParameter }; 82 | 83 | for (let arg of Object.keys(args)) { 84 | const reg = new RegExp(`__${arg}__`, "gim") 85 | filled = filled.replace(reg, args[arg]) 86 | } 87 | return filled; 88 | } 89 | 90 | export default function Page({ widgetLoader, widgetModules }: PageProps) { 91 | const [rootUrl, setRootUrl] = useState(""); 92 | const [widgetParameterValue, setWidgetParameterValue] = useState(""); 93 | const [widgetParameter] = useDebounce(widgetParameterValue, 300); 94 | const [selectedModule, setSelectedModule] = useState(widgetModules[0]); 95 | const classes = useStyles() 96 | 97 | const widgetLoaderWithModule = useMemo(() => setWidgetModule( 98 | widgetLoader, 99 | rootUrl, 100 | selectedModule, 101 | widgetParameter 102 | ), [widgetLoader, selectedModule, rootUrl, widgetParameter]) 103 | 104 | useEffect(() => { 105 | setRootUrl((_) => `${window.location.origin}/compiled-widgets/widget-modules/`) 106 | }, []) 107 | 108 | return ( 109 |
110 | 111 | Scriptable TS Boilerplate 112 | 113 | 114 |
115 | 116 | 117 | 118 |
119 | Scriptable TS Boilerplate 120 | 121 | A boilerplate for creating remote-updatable Scriptable widgets. Includes setup, components, utils and examples to develop in the comfort of TypeScript. 122 | 123 | 124 |
125 | Github repo → 126 |
127 |
128 |
129 | 130 |
131 | 132 | 133 | 134 |
135 | 136 |
137 | 138 | Try the examples 139 | 140 | Before you continue 141 | Make sure to first download the awesome Scriptable App from the Apple App Store. 142 | 143 | 144 | 1. Pick an example widget 145 | 146 | These widget examples are included in the boilerplate. 147 | 148 |
149 | {widgetModules.map(wm => 150 | setSelectedModule(wm)} 154 | isSelected={wm.moduleName === selectedModule?.moduleName} /> 155 | )} 156 | 157 | 158 | More examples coming soon! 159 | 160 | 161 |
162 | 163 | 2. Provide its default input 164 | 165 | This is not required and can also be filled into the Widget Setting after adding the widget 166 | 167 | setWidgetParameterValue(event.currentTarget.value)} 179 | /> 180 | 181 | 3. Copy the snippet 182 | 183 | {/* The key is just a quick hack to remount the code on every change to make highlight work */} 184 | 190 | 191 |
192 | 4. Paste the snippet 193 | 197 |
198 | 199 | And paste the snippet in a new Script. 200 | 201 | 202 | Now it's ready to be added as a widget. Just go in wiggle mode and add it to your homescreen! 203 | 204 |
205 | 206 | 207 |
208 | 209 |
210 | 211 | A bit of background 212 | 213 | Intrigued by the possibilities offered by the Scriptable App to create custom iOS Widgets in Javascript, I wondered whether this would also be useful for prototyping product-services requiring real widget interactions. The other route, publishing a actual native iOS app to TestFlight, just felt way to convoluted. 214 | 215 | 216 | I decided to set up this boilerplate to create such prototypes in a developer and end-user friendly manner. 217 |
    218 |
  • One time setup, continuous updates: To allow rapid prototyping, only an initial setup is required for the end-user. After this setup any new widget code deployed is downloaded the next time the widget refreshes (inspiration). This is also great for when all is still in flux (e.g. the UX, the API).
  • 219 |
  • Minimize code failures: During prototyping enough soft failures will, and should, already happen, it's an experiment. But minimizing the noise of hard/code failures is something to always strive for. Using TypeScript helps with this (I think), ensuring you don't mistakingly put in a Foo where a Bar was expected.
  • 220 |
  • Even more rapid local prototyping: Loading the code from a (local) server also helps to make the roundabout between your editor and your phone also a lot faster. No longer you need to wait on iCloud to sync on both sides.
  • 221 |
  • Offloading to the server: As Nextjs is included, custom API's are also simple to implement. This can keep the data-wrangling on the server and the actual widget code simple.
  • 222 |
223 |
224 | 225 | Of course, there are also some drawbacks. Regular widgets for instance can be informed by their related app that they should update. For widgets created in Scriptable, this only happens periodically. But besides such minor points there's just a lot you can do with Scriptable! 226 | 227 |
228 |
229 | 230 |
231 | 232 | Try the boilerplate 233 | 234 | Visit the Github repo or the official Scriptable Documentation. Follow any updates on Twitter @jasperhartong. 235 | 236 | 237 | This boilerplate is very much still a work in progress 238 | It works for my purpose at the moment :) 239 | 240 | 241 |
242 |
243 | ) 244 | } 245 | 246 | 247 | export const getStaticProps: GetStaticProps<{}, {}> = async ({ params }) => { 248 | const widgetLoaderPath = resolve('./public/compiled-widgets/widgetLoader.js'); 249 | const widgetModuleModuleNames = [ 250 | "stickyWidgetModule", 251 | "simpleAnalyticsWidgetModule", 252 | "covid19WidgetModule", 253 | "kitchenSinkWidgetModule" 254 | ] 255 | const props: PageProps = { 256 | widgetLoader: readFileSync(widgetLoaderPath).toString("utf-8"), 257 | widgetModules: widgetModuleModuleNames.map(moduleName => { 258 | const rawScript = readFileSync(resolve(`./public/compiled-widgets/widget-modules/${moduleName}.js`)).toString("utf-8"); 259 | const meta = JSON.parse(readFileSync(resolve(`./public/compiled-widgets/widget-modules/${moduleName}.meta.json`)).toString("utf-8")) as WidgetModule["meta"]; 260 | return ({ 261 | rawScript, 262 | moduleName, 263 | imageSrc: `/compiled-widgets/widget-modules/${moduleName}.png`, 264 | meta 265 | }) 266 | }) 267 | } 268 | 269 | 270 | return { props } 271 | } -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/covid19WidgetModule.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | const addFlexSpacer = ({ to }) => { 4 | to.addSpacer(); 5 | }; 6 | 7 | const SimpleTextWidget = (pretitle, title, subtitle, color) => { 8 | let w = new ListWidget(); 9 | w.backgroundColor = new Color(color, 1); 10 | let preTxt = w.addText(pretitle); 11 | preTxt.textColor = Color.white(); 12 | preTxt.textOpacity = 0.8; 13 | preTxt.font = Font.systemFont(10); 14 | w.addSpacer(5); 15 | let titleTxt = w.addText(title); 16 | titleTxt.textColor = Color.white(); 17 | titleTxt.font = Font.systemFont(16); 18 | w.addSpacer(5); 19 | let subTxt = w.addText(subtitle); 20 | subTxt.textColor = Color.white(); 21 | subTxt.textOpacity = 0.8; 22 | subTxt.font = Font.systemFont(12); 23 | addFlexSpacer({ to: w }); 24 | let a = w.addText(""); 25 | a.textColor = Color.white(); 26 | a.textOpacity = 0.8; 27 | a.font = Font.systemFont(12); 28 | return w; 29 | }; 30 | 31 | const ErrorWidget = (subtitle) => { 32 | return SimpleTextWidget("ERROR", "Widget Error", subtitle, "#000"); 33 | }; 34 | 35 | const RequestWithTimeout = (url, timeoutSeconds = 5) => { 36 | const request = new Request(url); 37 | request.timeoutInterval = timeoutSeconds; 38 | return request; 39 | }; 40 | 41 | // Based on https://gist.github.com/planecore/e7b4c1e5db2dd28b1a023860e831355e 42 | const createWidget = async (country) => { 43 | if (!country) { 44 | return ErrorWidget("No country"); 45 | } 46 | const url = `https://coronavirus-19-api.herokuapp.com/countries/${country}`; 47 | const req = RequestWithTimeout(url); 48 | const res = await req.loadJSON(); 49 | return SimpleTextWidget("Coronavirus", `${res.todayCases} Today`, `${res.cases} Total`, "#53d769"); 50 | }; 51 | const widgetModule = { 52 | createWidget: async (params) => { 53 | return createWidget(params.widgetParameter); 54 | } 55 | }; 56 | module.exports = widgetModule; 57 | 58 | })(); 59 | -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/covid19WidgetModule.meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Covid19", 3 | "description": "Coronavirus stats in a country", 4 | "paramLabel": "The country you're interested in", 5 | "paramPlaceHolder": "Israel", 6 | "loaderArgs": { 7 | "iconColor": "deep-green", 8 | "iconGlyph": "user-md" 9 | } 10 | } -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/covid19WidgetModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/compiled-widgets/widget-modules/covid19WidgetModule.png -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/kitchenSinkWidgetModule.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | const DynamicColor = ({ lightColor, darkColor }) => Color.dynamic(lightColor, darkColor); 4 | const DefaultColor = () => DynamicColor({ lightColor: Color.white(), darkColor: Color.black() }); 5 | 6 | // From https://talk.automators.fm/t/get-available-widget-height-and-width-depending-on-the-devices-screensize/9258/4 7 | const getWidgetSizeInPoint = (widgetSize = (config.widgetFamily ? config.widgetFamily : null)) => { 8 | // stringify device screen size 9 | const devSize = `${Device.screenSize().width}x${Device.screenSize().height}`; 10 | // screen size to widget size mapping for iPhone, excluding the latest iPhone 12 series. iPad size 11 | const sizeMap = { 12 | // iPad Mini 2/3/4, iPad 3/4, iPad Air 1/2. 9.7" iPad Pro 13 | // '768x1024': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 14 | // 10.2" iPad 15 | // '1080x810': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 16 | // 10.5" iPad Pro, 10.5" iPad Air 3rd Gen 17 | // '1112x834': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 18 | // 10.9" iPad Air 4th Gen 19 | // '1180x820': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 20 | // 11" iPad Pro 21 | '1194x834': { small: [155, 155], medium: [329, 155], large: [345, 329] }, 22 | // 12.9" iPad Pro 23 | '1366x1024': { small: [170, 170], medium: [332, 170], large: [382, 332] }, 24 | // 12 Pro Max 25 | // '428x926': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 26 | // XR, 11, 11 Pro Max 27 | '414x896': { small: [169, 169], medium: [360, 169], large: [360, 376] }, 28 | // 12, 12 Pro 29 | // '390x844': : { small: [0, 0], medium: [0, 0], large: [0, 0] }, 30 | // X, XS, 11 Pro, 12 Mini 31 | '375x812': { small: [155, 155], medium: [329, 155], large: [329, 345] }, 32 | // 6/7/8(S) Plus 33 | '414x736': { small: [159, 159], medium: [348, 159], large: [348, 357] }, 34 | // 6/7/8(S) and 2nd Gen SE 35 | '375x667': { small: [148, 148], medium: [322, 148], large: [322, 324] }, 36 | // 1st Gen SE 37 | '320x568': { small: [141, 141], medium: [291, 141], large: [291, 299] } 38 | }; 39 | let widgetSizeInPoint = null; 40 | if (widgetSize) { 41 | let mappedSize = sizeMap[devSize]; 42 | if (mappedSize) { 43 | widgetSizeInPoint = new Size(...mappedSize[widgetSize]); 44 | } 45 | } 46 | return widgetSizeInPoint; 47 | }; 48 | 49 | const ErrorImage = ({ error, width, height }) => { 50 | const text = `${(error === null || error === void 0 ? void 0 : error.message) || error}`; 51 | const dc = new DrawContext(); 52 | dc.size = new Size(width || 200, height || 200); 53 | dc.respectScreenScale = true; 54 | dc.opaque = false; 55 | dc.setTextColor(Color.red()); 56 | dc.setFont(Font.semiboldSystemFont(dc.size.width / 10)); 57 | dc.drawText(text, new Point(dc.size.width / 10, 8)); 58 | return dc.getImage(); 59 | }; 60 | 61 | const SparkBarImage = ({ series, width, height, color = DefaultColor(), lastBarColor = Color.orange() }) => { 62 | if (series.length === 0) { 63 | return ErrorImage({ error: "No Data", width, height }); 64 | } 65 | const widgetSize = getWidgetSizeInPoint(); 66 | const dc = new DrawContext(); 67 | dc.size = new Size(width || (widgetSize === null || widgetSize === void 0 ? void 0 : widgetSize.width) || 200, height || (widgetSize === null || widgetSize === void 0 ? void 0 : widgetSize.height) || 200); 68 | dc.respectScreenScale = true; 69 | dc.opaque = false; 70 | const barColor = color; 71 | const barWidth = (dc.size.width) / series.length - 4; 72 | const maxValue = Math.max(...series); 73 | // Calculate the rendered height of the bars, make sure they're at least 1 pixel 74 | const pixelMultiplier = dc.size.height / maxValue; 75 | const pixelValues = series.map(v => Math.max(v * pixelMultiplier, 1)); 76 | // Draw the bars 77 | pixelValues.forEach((v, i) => { 78 | dc.setFillColor(i === pixelValues.length - 1 ? lastBarColor : barColor); 79 | const x = (dc.size.width * i / pixelValues.length); 80 | const barHeight = v; 81 | const y = dc.size.height - barHeight; 82 | dc.fillRect(new Rect(x, y, barWidth, barHeight)); 83 | }); 84 | const image = dc.getImage(); 85 | return image; 86 | }; 87 | 88 | const RequestWithTimeout = (url, timeoutSeconds = 5) => { 89 | const request = new Request(url); 90 | request.timeoutInterval = timeoutSeconds; 91 | return request; 92 | }; 93 | 94 | const UnsplashImage = async ({ id = "random", width = 600, height = 600 }) => { 95 | const req = RequestWithTimeout(`https://source.unsplash.com/${id}/${width}x${height}`); 96 | try { 97 | return await req.loadImage(); 98 | } 99 | catch (error) { 100 | return ErrorImage({ width, height, error }); 101 | } 102 | }; 103 | 104 | const addFlexSpacer = ({ to }) => { 105 | to.addSpacer(); 106 | }; 107 | 108 | const addSymbol = ({ to, symbol = 'applelogo', color = DefaultColor(), size = 20, }) => { 109 | const _sym = SFSymbol.named(symbol); 110 | const wImg = to.addImage(_sym.image); 111 | wImg.tintColor = color; 112 | wImg.imageSize = new Size(size, size); 113 | }; 114 | 115 | const addTextWithSymbolStack = ({ to, text, symbol, textColor = DefaultColor(), symbolColor = DefaultColor(), fontSize = 20, }) => { 116 | const _stack = to.addStack(); 117 | _stack.centerAlignContent(); 118 | addSymbol({ 119 | to: _stack, 120 | symbol, 121 | size: fontSize, 122 | color: symbolColor 123 | }); 124 | _stack.addSpacer(3); 125 | let _text = _stack.addText(text); 126 | _text.textColor = textColor; 127 | _text.font = Font.systemFont(fontSize); 128 | return _stack; 129 | }; 130 | 131 | const widgetModule = { 132 | createWidget: async (params) => { 133 | const widget = new ListWidget(); 134 | widget.setPadding(8, 0, 0, 0); 135 | widget.backgroundImage = await UnsplashImage({ id: "KuF8-6EbBMs", width: 500, height: 500 }); 136 | const mainStack = widget.addStack(); 137 | mainStack.layoutVertically(); 138 | addFlexSpacer({ to: mainStack }); 139 | // Start Content 140 | const contentStack = mainStack.addStack(); 141 | contentStack.layoutVertically(); 142 | contentStack.setPadding(0, 16, 0, 16); 143 | contentStack.addImage(SparkBarImage({ 144 | series: [800000, 780000, 760000, 738000, 680000, 650000, 600000, 554600, 500000, 438000], 145 | width: 400, 146 | height: 100, 147 | color: new Color(Color.white().hex, 0.6), 148 | lastBarColor: Color.orange() 149 | })); 150 | contentStack.addSpacer(8); 151 | let title = contentStack.addText("438.000 cases"); 152 | title.textColor = Color.orange(); 153 | title.font = Font.semiboldSystemFont(14); 154 | contentStack.addSpacer(2); 155 | let _text = contentStack.addText("A 50% decrease in the last 10 years"); 156 | _text.textColor = Color.white(); 157 | _text.font = Font.systemFont(12); 158 | // End Content 159 | addFlexSpacer({ to: mainStack }); 160 | // Footer 161 | addStatsStack({ stack: mainStack }); 162 | return widget; 163 | } 164 | }; 165 | const addStatsStack = ({ stack }) => { 166 | const statsStack = stack.addStack(); 167 | statsStack.centerAlignContent(); 168 | statsStack.backgroundColor = new Color(Color.black().hex, 0.85); 169 | statsStack.setPadding(6, 16, 6, 16); 170 | addTextWithSymbolStack({ 171 | to: statsStack, 172 | symbol: "person.crop.circle", 173 | text: "0,50", 174 | fontSize: 10, 175 | textColor: Color.lightGray(), 176 | symbolColor: Color.lightGray() 177 | }); 178 | addFlexSpacer({ to: statsStack }); 179 | addTextWithSymbolStack({ 180 | to: statsStack, 181 | symbol: "network", 182 | text: "11K", 183 | fontSize: 10, 184 | textColor: Color.lightGray(), 185 | symbolColor: Color.lightGray() 186 | }); 187 | return statsStack; 188 | }; 189 | module.exports = widgetModule; 190 | 191 | })(); 192 | -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/kitchenSinkWidgetModule.meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Kitchen Sink", 3 | "description": "Demoing included components", 4 | "paramLabel": "No parameter", 5 | "paramPlaceHolder": "Nothing required", 6 | "loaderArgs": { 7 | "iconColor": "deep-green", 8 | "iconGlyph": "user-md" 9 | } 10 | } -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/kitchenSinkWidgetModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/compiled-widgets/widget-modules/kitchenSinkWidgetModule.png -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/simpleAnalyticsWidgetModule.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | const DynamicColor = ({ lightColor, darkColor }) => Color.dynamic(lightColor, darkColor); 4 | const DefaultColor = () => DynamicColor({ lightColor: Color.white(), darkColor: Color.black() }); 5 | 6 | // From https://talk.automators.fm/t/get-available-widget-height-and-width-depending-on-the-devices-screensize/9258/4 7 | const getWidgetSizeInPoint = (widgetSize = (config.widgetFamily ? config.widgetFamily : null)) => { 8 | // stringify device screen size 9 | const devSize = `${Device.screenSize().width}x${Device.screenSize().height}`; 10 | // screen size to widget size mapping for iPhone, excluding the latest iPhone 12 series. iPad size 11 | const sizeMap = { 12 | // iPad Mini 2/3/4, iPad 3/4, iPad Air 1/2. 9.7" iPad Pro 13 | // '768x1024': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 14 | // 10.2" iPad 15 | // '1080x810': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 16 | // 10.5" iPad Pro, 10.5" iPad Air 3rd Gen 17 | // '1112x834': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 18 | // 10.9" iPad Air 4th Gen 19 | // '1180x820': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 20 | // 11" iPad Pro 21 | '1194x834': { small: [155, 155], medium: [329, 155], large: [345, 329] }, 22 | // 12.9" iPad Pro 23 | '1366x1024': { small: [170, 170], medium: [332, 170], large: [382, 332] }, 24 | // 12 Pro Max 25 | // '428x926': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 26 | // XR, 11, 11 Pro Max 27 | '414x896': { small: [169, 169], medium: [360, 169], large: [360, 376] }, 28 | // 12, 12 Pro 29 | // '390x844': : { small: [0, 0], medium: [0, 0], large: [0, 0] }, 30 | // X, XS, 11 Pro, 12 Mini 31 | '375x812': { small: [155, 155], medium: [329, 155], large: [329, 345] }, 32 | // 6/7/8(S) Plus 33 | '414x736': { small: [159, 159], medium: [348, 159], large: [348, 357] }, 34 | // 6/7/8(S) and 2nd Gen SE 35 | '375x667': { small: [148, 148], medium: [322, 148], large: [322, 324] }, 36 | // 1st Gen SE 37 | '320x568': { small: [141, 141], medium: [291, 141], large: [291, 299] } 38 | }; 39 | let widgetSizeInPoint = null; 40 | if (widgetSize) { 41 | let mappedSize = sizeMap[devSize]; 42 | if (mappedSize) { 43 | widgetSizeInPoint = new Size(...mappedSize[widgetSize]); 44 | } 45 | } 46 | return widgetSizeInPoint; 47 | }; 48 | 49 | const ErrorImage = ({ error, width, height }) => { 50 | const text = `${(error === null || error === void 0 ? void 0 : error.message) || error}`; 51 | const dc = new DrawContext(); 52 | dc.size = new Size(width || 200, height || 200); 53 | dc.respectScreenScale = true; 54 | dc.opaque = false; 55 | dc.setTextColor(Color.red()); 56 | dc.setFont(Font.semiboldSystemFont(dc.size.width / 10)); 57 | dc.drawText(text, new Point(dc.size.width / 10, 8)); 58 | return dc.getImage(); 59 | }; 60 | 61 | const SparkBarImage = ({ series, width, height, color = DefaultColor(), lastBarColor = Color.orange() }) => { 62 | if (series.length === 0) { 63 | return ErrorImage({ error: "No Data", width, height }); 64 | } 65 | const widgetSize = getWidgetSizeInPoint(); 66 | const dc = new DrawContext(); 67 | dc.size = new Size(width || (widgetSize === null || widgetSize === void 0 ? void 0 : widgetSize.width) || 200, height || (widgetSize === null || widgetSize === void 0 ? void 0 : widgetSize.height) || 200); 68 | dc.respectScreenScale = true; 69 | dc.opaque = false; 70 | const barColor = color; 71 | const barWidth = (dc.size.width) / series.length - 4; 72 | const maxValue = Math.max(...series); 73 | // Calculate the rendered height of the bars, make sure they're at least 1 pixel 74 | const pixelMultiplier = dc.size.height / maxValue; 75 | const pixelValues = series.map(v => Math.max(v * pixelMultiplier, 1)); 76 | // Draw the bars 77 | pixelValues.forEach((v, i) => { 78 | dc.setFillColor(i === pixelValues.length - 1 ? lastBarColor : barColor); 79 | const x = (dc.size.width * i / pixelValues.length); 80 | const barHeight = v; 81 | const y = dc.size.height - barHeight; 82 | dc.fillRect(new Rect(x, y, barWidth, barHeight)); 83 | }); 84 | const image = dc.getImage(); 85 | return image; 86 | }; 87 | 88 | const addFlexSpacer = ({ to }) => { 89 | to.addSpacer(); 90 | }; 91 | 92 | const SimpleSparkBarWidget = ({ series, header, title, description, backgroundColor, barColor, lastBarColor }) => { 93 | const widget = new ListWidget(); 94 | widget.backgroundColor = backgroundColor; 95 | // Header 96 | const headerTxt = widget.addText(header.text); 97 | headerTxt.textColor = header.color; 98 | headerTxt.font = Font.systemFont(10); 99 | // Vertical Space 100 | addFlexSpacer({ to: widget }); 101 | // BarChart (centered) 102 | const barStack = widget.addStack(); 103 | barStack.layoutHorizontally(); 104 | addFlexSpacer({ to: barStack }); 105 | if (series.length > 0) { 106 | barStack.addImage(SparkBarImage({ 107 | series, 108 | color: barColor, 109 | lastBarColor, 110 | height: 100, 111 | width: 400 112 | })); 113 | } 114 | addFlexSpacer({ to: barStack }); 115 | // Vertical Space 116 | widget.addSpacer(10); 117 | // Title 118 | const titleTxt = widget.addText(title.text); 119 | titleTxt.textColor = title.color; 120 | titleTxt.font = Font.boldSystemFont(16); 121 | // Vertical space 122 | widget.addSpacer(2); 123 | // Description 124 | const descriptionText = widget.addText(description.text); 125 | descriptionText.textColor = description.color; 126 | descriptionText.font = Font.systemFont(12); 127 | return widget; 128 | }; 129 | 130 | const RequestWithTimeout = (url, timeoutSeconds = 5) => { 131 | const request = new Request(url); 132 | request.timeoutInterval = timeoutSeconds; 133 | return request; 134 | }; 135 | 136 | const widgetModule = { 137 | createWidget: async (params) => { 138 | var _a; 139 | const { website, apiKey } = parseWidgetParameter(params.widgetParameter); 140 | // Styling 141 | const highlightColor = new Color("#b93545", 1.0); 142 | const textColor = new Color("#a4bdc0", 1.0); 143 | const backgroundColor = new Color("#20292a", 1); 144 | const barColor = new Color("#198c9f", 1); 145 | // Fallback data 146 | let series = []; 147 | let titleText = "No data"; 148 | let descriptionText = "Check the parameter settings"; 149 | // Load data 150 | const data = await requestSimpleAnalyticsData({ website, apiKey }); 151 | if (data) { 152 | const pageViewsToday = ((_a = data.visits[data.visits.length - 1]) === null || _a === void 0 ? void 0 : _a.pageviews) || 0; 153 | series = data.visits.map(visit => visit.pageviews); 154 | titleText = `${pageViewsToday} views`; 155 | descriptionText = `${data.pageviews} this month`; 156 | } 157 | const widget = SimpleSparkBarWidget({ 158 | series, 159 | header: { text: website, color: textColor }, 160 | title: { text: titleText, color: highlightColor }, 161 | description: { text: descriptionText, color: textColor }, 162 | backgroundColor, 163 | barColor, 164 | lastBarColor: highlightColor, 165 | }); 166 | if (website) { 167 | // Open Simple Analytics stats when tapped 168 | widget.url = `https://simpleanalytics.com/${website}`; 169 | } 170 | return widget; 171 | } 172 | }; 173 | module.exports = widgetModule; 174 | // SimpleAnalytics helpers 175 | const parseWidgetParameter = (param) => { 176 | // handles: @ || @ || 177 | const paramParts = param.toLowerCase().replace(/ /g, "").split("@"); 178 | let apiKey = ""; 179 | let website = ""; 180 | switch (paramParts.length) { 181 | case 1: 182 | [website] = paramParts; 183 | break; 184 | case 2: 185 | [apiKey, website] = paramParts; 186 | break; 187 | } 188 | return { apiKey, website }; 189 | }; 190 | const formatDateQueryParam = (date) => date.toISOString().split('T')[0]; 191 | const requestSimpleAnalyticsData = async ({ website, apiKey, daysAgo = 31 }) => { 192 | const today = new Date(); 193 | const startDate = new Date(new Date().setDate(today.getDate() - daysAgo)); 194 | const url = `https://simpleanalytics.com/${website}.json?version=2&start=${formatDateQueryParam(startDate)}&end=${formatDateQueryParam(today)}`; 195 | const req = RequestWithTimeout(url); 196 | if (apiKey) { 197 | req.headers = { "Api-Key": apiKey }; 198 | } 199 | const data = await req.loadJSON(); 200 | return req.response.statusCode === 200 ? data : null; 201 | }; 202 | 203 | })(); 204 | -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/simpleAnalyticsWidgetModule.meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Simple Analytics", 3 | "description": "Page views widget", 4 | "paramLabel": "website / api-key@website", 5 | "paramPlaceHolder": "simpleanalytics.com / sa_api_key_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@mywebsite.com", 6 | "loaderArgs": { 7 | "iconColor": "gray", 8 | "iconGlyph": "icon-graph" 9 | } 10 | } -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/simpleAnalyticsWidgetModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/compiled-widgets/widget-modules/simpleAnalyticsWidgetModule.png -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/stickyWidgetModule.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | // Based on https://github.com/drewkerr/scriptable/blob/main/Sticky%20widget.js 4 | const createWidget = (note) => { 5 | let widget = new ListWidget(); 6 | widget.setPadding(16, 16, 16, 8); 7 | let dark = Device.isUsingDarkAppearance(); 8 | let fgColor = Color.black(); 9 | if (dark) { 10 | fgColor = new Color("#FFCF00", 1); 11 | let bgColor = Color.black(); 12 | widget.backgroundColor = bgColor; 13 | } 14 | else { 15 | let startColor = new Color("#F8DE5F", 1); 16 | let endColor = new Color("#FFCF00", 1); 17 | let gradient = new LinearGradient(); 18 | gradient.colors = [startColor, endColor]; 19 | gradient.locations = [0.0, 1]; 20 | widget.backgroundGradient = gradient; 21 | } 22 | let noteText = widget.addText(note); 23 | noteText.textColor = fgColor; 24 | noteText.font = Font.mediumRoundedSystemFont(24); 25 | noteText.textOpacity = 0.8; 26 | noteText.minimumScaleFactor = 0.25; 27 | return widget; 28 | }; 29 | const widgetModule = { 30 | createWidget: async (params) => { 31 | return createWidget(params.widgetParameter); 32 | } 33 | }; 34 | module.exports = widgetModule; 35 | 36 | })(); 37 | -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/stickyWidgetModule.meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sticky Widget", 3 | "description": "Just a simple sticky widget", 4 | "paramLabel": "The text on the sticky", 5 | "paramPlaceHolder": "Scriptable is fun!", 6 | "loaderArgs": { 7 | "iconColor": "yellow", 8 | "iconGlyph": "sticky-note" 9 | } 10 | } -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widget-modules/stickyWidgetModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/compiled-widgets/widget-modules/stickyWidgetModule.png -------------------------------------------------------------------------------- /scriptable-api/public/compiled-widgets/widgetLoader.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: __iconColor__; icon-glyph: __iconGlyph__; 4 | 5 | const RequestWithTimeout = (url, timeoutSeconds = 5) => { 6 | const request = new Request(url); 7 | request.timeoutInterval = timeoutSeconds; 8 | return request; 9 | }; 10 | 11 | const ROOT_MODULE_PATH = "widget-loader"; 12 | const widgetModuleDownloadConfig = { 13 | moduleName: "__moduleName__", 14 | rootUrl: "__rootUrl__", 15 | defaultWidgetParameter: "__defaultWidgetParameter__", 16 | downloadQueryString: "__downloadQueryString__", 17 | }; 18 | async function getOrCreateWidgetModule({ moduleName, rootUrl, downloadQueryString }, forceDownload = false) { 19 | const fm = FileManager.local(); 20 | const rootModuleDir = fm.joinPath(fm.libraryDirectory(), ROOT_MODULE_PATH); 21 | enforceDir(fm, rootModuleDir); 22 | const widgetModuleDir = fm.joinPath(rootModuleDir, moduleName); 23 | enforceDir(fm, widgetModuleDir); 24 | const widgetModuleFilename = `${moduleName}.js`; 25 | const widgetModuleEtag = `${moduleName}.etag`; 26 | const widgetModulePath = fm.joinPath(widgetModuleDir, widgetModuleFilename); 27 | const widgetModuleEtagPath = fm.joinPath(widgetModuleDir, widgetModuleEtag); 28 | const widgetModuleDownloadUrl = rootUrl + widgetModuleFilename + (downloadQueryString.startsWith("?") ? downloadQueryString : ""); 29 | try { 30 | // Check if an etag was saved for this file 31 | if (fm.fileExists(widgetModuleEtagPath) && !forceDownload) { 32 | const lastEtag = fm.readString(widgetModuleEtagPath); 33 | const headerReq = RequestWithTimeout(widgetModuleDownloadUrl); 34 | headerReq.method = "HEAD"; 35 | await headerReq.load(); 36 | const etag = getResponseHeader(headerReq, "Etag"); 37 | if (lastEtag && etag && lastEtag === etag) { 38 | console.log(`ETag is same, return cached file for ${widgetModuleDownloadUrl}`); 39 | return widgetModulePath; 40 | } 41 | } 42 | console.log("Downloading library file '" + widgetModuleDownloadUrl + "' to '" + widgetModulePath + "'"); 43 | const req = RequestWithTimeout(widgetModuleDownloadUrl); 44 | const libraryFile = await req.load(); 45 | const etag = getResponseHeader(req, "Etag"); 46 | if (etag) { 47 | fm.writeString(widgetModuleEtagPath, etag); 48 | } 49 | fm.write(widgetModulePath, libraryFile); 50 | } 51 | catch (error) { 52 | console.error("Downloading module failed, return existing module"); 53 | console.error(error); 54 | } 55 | return widgetModulePath; 56 | } 57 | const getResponseHeader = (request, header) => { 58 | if (!request.response) { 59 | return undefined; 60 | } 61 | const key = Object.keys(request.response["headers"]) 62 | .find(key => key.toLowerCase() === header.toLowerCase()); 63 | return key ? request.response["headers"][key] : undefined; 64 | }; 65 | const enforceDir = (fm, path) => { 66 | if (fm.fileExists(path) && !fm.isDirectory(path)) { 67 | fm.remove(path); 68 | } 69 | if (!fm.fileExists(path)) { 70 | fm.createDirectory(path); 71 | } 72 | }; 73 | 74 | const DEBUG = false; 75 | const FORCE_DOWNLOAD = false; 76 | const widgetModulePath = await getOrCreateWidgetModule(widgetModuleDownloadConfig, FORCE_DOWNLOAD); 77 | const widgetModule = importModule(widgetModulePath); 78 | const widget = await widgetModule.createWidget({ 79 | widgetParameter: args.widgetParameter || widgetModuleDownloadConfig.defaultWidgetParameter, 80 | debug: DEBUG 81 | }); 82 | // preview the widget if in app 83 | if (!config.runsInWidget) { 84 | await widget.presentSmall(); 85 | } 86 | Script.setWidget(widget); 87 | Script.complete(); 88 | -------------------------------------------------------------------------------- /scriptable-api/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/favicon.ico -------------------------------------------------------------------------------- /scriptable-api/public/scriptable-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasperhartong/scriptable-ts-boilerplate/9af9afcab34d30dbbf1cd14662ebbb9159f3b237/scriptable-api/public/scriptable-hero.png -------------------------------------------------------------------------------- /scriptable-api/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /scriptable-api/src/components/CodeWithClipboard.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonBase, Collapse, Fab, Fade, makeStyles, useTheme } from "@material-ui/core"; 2 | import CheckCircleIcon from "@material-ui/icons/CheckCircle"; 3 | import ChevronRightIcon from "@material-ui/icons/ChevronRight"; 4 | import ClipboardIcon from "@material-ui/icons/FileCopy"; 5 | import clsx from 'clsx'; 6 | import hljs from 'highlight.js'; 7 | import javascript from 'highlight.js/lib/languages/javascript'; 8 | import React, { useCallback, useEffect, useRef, useState } from "react"; 9 | import { useClipboard } from "use-clipboard-copy"; 10 | 11 | 12 | 13 | interface Props { 14 | value: string; 15 | collapsedSize: number; 16 | inActive: boolean; 17 | } 18 | 19 | const useStyles = makeStyles(theme => ({ 20 | code: { 21 | padding: theme.spacing(3), 22 | paddingTop: theme.spacing(8), 23 | margin: 0, 24 | borderRadius: theme.shape.borderRadius, 25 | border: 'solid 1px ' + theme.palette.grey[700], 26 | }, 27 | root: { 28 | position: "relative", 29 | overflow: "hidden" 30 | }, 31 | button: { 32 | position: "absolute", 33 | left: theme.spacing(2), 34 | top: theme.spacing(2), 35 | color: "white" 36 | }, 37 | toggle: { 38 | position: "absolute", 39 | bottom: theme.spacing(1), 40 | textAlign: "center", 41 | paddingTop: theme.spacing(1), 42 | width: "100%", 43 | display: "block", 44 | opacity: 0.85, 45 | transition: "all 300ms ease-in-out", 46 | "&:hover": { 47 | opacity: 1 48 | } 49 | }, 50 | chevron: { 51 | transition: "all 300ms ease-in-out", 52 | transform: "rotate(90deg)" 53 | }, 54 | chevronUp: { 55 | transform: "rotate(-90deg)" 56 | } 57 | })) 58 | 59 | 60 | export const CodeWithClipboard = ( 61 | { value, collapsedSize, inActive }: Props 62 | ) => { 63 | const [isHighlighted, setIsHighlighted] = useState(false); 64 | const [isSupported, setIsSupported] = useState(false); 65 | const [isCollapsed, setIsCollapsed] = useState(true); 66 | const preElement = useRef(null); 67 | const clipboard = useClipboard({ copiedTimeout: 800 }) 68 | 69 | const theme = useTheme() 70 | const classes = useStyles() 71 | 72 | const handleCopy = useCallback(() => { 73 | clipboard.copy(value) 74 | }, [value]) 75 | 76 | const handleToggle = useCallback((event: React.MouseEvent) => { 77 | event.preventDefault(); 78 | setIsCollapsed(collapsed => !collapsed); 79 | }, [value]) 80 | 81 | useEffect(() => { 82 | setIsSupported((_) => clipboard.isSupported()) 83 | }, [clipboard]) 84 | 85 | useEffect(() => { 86 | hljs.registerLanguage("javascript", javascript) 87 | hljs.highlightBlock(preElement.current); 88 | setIsHighlighted(true) 89 | }, [value]); 90 | 91 | return ( 92 | 93 |
94 | 95 |
96 | {!inActive && isSupported && ( 97 | 101 | )} 102 |
103 |                             {value}
104 |                         
105 |
106 |
107 | {!inActive && 108 | 109 | } 110 |
111 |
112 | ) 113 | 114 | } -------------------------------------------------------------------------------- /scriptable-api/src/components/WidgetModuleCard.tsx: -------------------------------------------------------------------------------- 1 | import { alpha, Button } from "@material-ui/core"; 2 | import Card from "@material-ui/core/Card"; 3 | import CardContent from "@material-ui/core/CardContent"; 4 | import CardMedia from "@material-ui/core/CardMedia"; 5 | import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; 6 | import Typography from "@material-ui/core/Typography"; 7 | import clsx from "clsx"; 8 | import React from "react"; 9 | import { WidgetModule } from "../interfaces"; 10 | 11 | const useStyles = makeStyles((theme: Theme) => 12 | createStyles({ 13 | root: { 14 | display: "flex", 15 | maxWidth: 280, 16 | minWidth: 280, 17 | height: 150, 18 | margin: theme.spacing(2), 19 | marginLeft: theme.spacing(1), 20 | transition: "all 300ms ease-in-out", 21 | border: `3px transparent solid`, 22 | }, 23 | rootIsSelected: { 24 | transform: "transform(1.1)", 25 | boxShadow: theme.shadows[5], 26 | background: alpha(theme.palette.primary.main, 0.1), 27 | border: `3px ${theme.palette.primary.main} solid`, 28 | }, 29 | details: { 30 | display: "flex", 31 | flexDirection: "column", 32 | }, 33 | content: { 34 | flex: "1 0 auto", 35 | }, 36 | cover: { 37 | width: 150, 38 | backgroundSize: "cover", 39 | backgroundPositionX: -14, 40 | }, 41 | controls: { 42 | display: "flex", 43 | alignItems: "center", 44 | paddingLeft: theme.spacing(1), 45 | paddingBottom: theme.spacing(1), 46 | }, 47 | playIcon: { 48 | height: 38, 49 | width: 38, 50 | }, 51 | }) 52 | ); 53 | 54 | interface Props { 55 | widgetModule: WidgetModule; 56 | isSelected: boolean; 57 | onSelect: () => void; 58 | } 59 | 60 | export const WidgetModuleCard = ({ 61 | widgetModule, 62 | isSelected, 63 | onSelect, 64 | }: Props) => { 65 | const classes = useStyles(); 66 | 67 | return ( 68 | 75 |
76 | 77 | 78 | {widgetModule.meta.name} 79 | 80 | 81 | {widgetModule.meta.description} 82 | 83 | 84 |
85 | 93 |
94 |
95 | 100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /scriptable-api/src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface WidgetModule { 2 | rawScript: string; 3 | moduleName: string; 4 | imageSrc: string; 5 | meta: { 6 | name: string; 7 | paramLabel: string; 8 | paramPlaceHolder: string; 9 | description: string; 10 | loaderArgs: WidgetLoaderArgs; 11 | } 12 | } 13 | 14 | export interface WidgetLoaderArgs { 15 | iconColor: string; 16 | iconGlyph: string; 17 | } -------------------------------------------------------------------------------- /scriptable-api/src/theme.ts: -------------------------------------------------------------------------------- 1 | import { red } from "@material-ui/core/colors"; 2 | import { createTheme } from "@material-ui/core/styles"; 3 | 4 | // Create a theme instance. 5 | const theme = createTheme({ 6 | palette: { 7 | primary: { 8 | main: "#556cd6", 9 | }, 10 | secondary: { 11 | main: "#19857b", 12 | }, 13 | error: { 14 | main: red.A400, 15 | }, 16 | background: { 17 | default: "#fff", 18 | }, 19 | }, 20 | }); 21 | 22 | export default theme; 23 | -------------------------------------------------------------------------------- /scriptable-api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": false, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "incremental": true 21 | }, 22 | "include": [ 23 | "next-env.d.ts", 24 | "**/*.ts", 25 | "**/*.tsx" 26 | ], 27 | "exclude": [ 28 | "node_modules" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /scriptable-api/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": 6 | version "7.16.7" 7 | resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.16.7.tgz#03ff99f64106588c9c403c6ecb8c3bafbbdff1fa" 8 | integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== 9 | dependencies: 10 | regenerator-runtime "^0.13.4" 11 | 12 | "@emotion/hash@^0.8.0": 13 | version "0.8.0" 14 | resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" 15 | integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== 16 | 17 | "@material-ui/core@^4.12.3": 18 | version "4.12.3" 19 | resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" 20 | integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== 21 | dependencies: 22 | "@babel/runtime" "^7.4.4" 23 | "@material-ui/styles" "^4.11.4" 24 | "@material-ui/system" "^4.12.1" 25 | "@material-ui/types" "5.1.0" 26 | "@material-ui/utils" "^4.11.2" 27 | "@types/react-transition-group" "^4.2.0" 28 | clsx "^1.0.4" 29 | hoist-non-react-statics "^3.3.2" 30 | popper.js "1.16.1-lts" 31 | prop-types "^15.7.2" 32 | react-is "^16.8.0 || ^17.0.0" 33 | react-transition-group "^4.4.0" 34 | 35 | "@material-ui/icons@^4.11.2": 36 | version "4.11.2" 37 | resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" 38 | integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== 39 | dependencies: 40 | "@babel/runtime" "^7.4.4" 41 | 42 | "@material-ui/lab@^4.0.0-alpha.60": 43 | version "4.0.0-alpha.60" 44 | resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz#5ad203aed5a8569b0f1753945a21a05efa2234d2" 45 | integrity sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA== 46 | dependencies: 47 | "@babel/runtime" "^7.4.4" 48 | "@material-ui/utils" "^4.11.2" 49 | clsx "^1.0.4" 50 | prop-types "^15.7.2" 51 | react-is "^16.8.0 || ^17.0.0" 52 | 53 | "@material-ui/styles@^4.11.4": 54 | version "4.11.4" 55 | resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" 56 | integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== 57 | dependencies: 58 | "@babel/runtime" "^7.4.4" 59 | "@emotion/hash" "^0.8.0" 60 | "@material-ui/types" "5.1.0" 61 | "@material-ui/utils" "^4.11.2" 62 | clsx "^1.0.4" 63 | csstype "^2.5.2" 64 | hoist-non-react-statics "^3.3.2" 65 | jss "^10.5.1" 66 | jss-plugin-camel-case "^10.5.1" 67 | jss-plugin-default-unit "^10.5.1" 68 | jss-plugin-global "^10.5.1" 69 | jss-plugin-nested "^10.5.1" 70 | jss-plugin-props-sort "^10.5.1" 71 | jss-plugin-rule-value-function "^10.5.1" 72 | jss-plugin-vendor-prefixer "^10.5.1" 73 | prop-types "^15.7.2" 74 | 75 | "@material-ui/system@^4.12.1": 76 | version "4.12.1" 77 | resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c" 78 | integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw== 79 | dependencies: 80 | "@babel/runtime" "^7.4.4" 81 | "@material-ui/utils" "^4.11.2" 82 | csstype "^2.5.2" 83 | prop-types "^15.7.2" 84 | 85 | "@material-ui/types@5.1.0": 86 | version "5.1.0" 87 | resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2" 88 | integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== 89 | 90 | "@material-ui/utils@^4.11.2": 91 | version "4.11.2" 92 | resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a" 93 | integrity sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA== 94 | dependencies: 95 | "@babel/runtime" "^7.4.4" 96 | prop-types "^15.7.2" 97 | react-is "^16.8.0 || ^17.0.0" 98 | 99 | "@next/env@12.3.1": 100 | version "12.3.1" 101 | resolved "https://registry.yarnpkg.com/@next/env/-/env-12.3.1.tgz#18266bd92de3b4aa4037b1927aa59e6f11879260" 102 | integrity sha512-9P9THmRFVKGKt9DYqeC2aKIxm8rlvkK38V1P1sRE7qyoPBIs8l9oo79QoSdPtOWfzkbDAVUqvbQGgTMsb8BtJg== 103 | 104 | "@next/swc-android-arm-eabi@12.3.1": 105 | version "12.3.1" 106 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm-eabi/-/swc-android-arm-eabi-12.3.1.tgz#b15ce8ad376102a3b8c0f3c017dde050a22bb1a3" 107 | integrity sha512-i+BvKA8tB//srVPPQxIQN5lvfROcfv4OB23/L1nXznP+N/TyKL8lql3l7oo2LNhnH66zWhfoemg3Q4VJZSruzQ== 108 | 109 | "@next/swc-android-arm64@12.3.1": 110 | version "12.3.1" 111 | resolved "https://registry.yarnpkg.com/@next/swc-android-arm64/-/swc-android-arm64-12.3.1.tgz#85d205f568a790a137cb3c3f720d961a2436ac9c" 112 | integrity sha512-CmgU2ZNyBP0rkugOOqLnjl3+eRpXBzB/I2sjwcGZ7/Z6RcUJXK5Evz+N0ucOxqE4cZ3gkTeXtSzRrMK2mGYV8Q== 113 | 114 | "@next/swc-darwin-arm64@12.3.1": 115 | version "12.3.1" 116 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-12.3.1.tgz#b105457d6760a7916b27e46c97cb1a40547114ae" 117 | integrity sha512-hT/EBGNcu0ITiuWDYU9ur57Oa4LybD5DOQp4f22T6zLfpoBMfBibPtR8XktXmOyFHrL/6FC2p9ojdLZhWhvBHg== 118 | 119 | "@next/swc-darwin-x64@12.3.1": 120 | version "12.3.1" 121 | resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-12.3.1.tgz#6947b39082271378896b095b6696a7791c6e32b1" 122 | integrity sha512-9S6EVueCVCyGf2vuiLiGEHZCJcPAxglyckTZcEwLdJwozLqN0gtS0Eq0bQlGS3dH49Py/rQYpZ3KVWZ9BUf/WA== 123 | 124 | "@next/swc-freebsd-x64@12.3.1": 125 | version "12.3.1" 126 | resolved "https://registry.yarnpkg.com/@next/swc-freebsd-x64/-/swc-freebsd-x64-12.3.1.tgz#2b6c36a4d84aae8b0ea0e0da9bafc696ae27085a" 127 | integrity sha512-qcuUQkaBZWqzM0F1N4AkAh88lLzzpfE6ImOcI1P6YeyJSsBmpBIV8o70zV+Wxpc26yV9vpzb+e5gCyxNjKJg5Q== 128 | 129 | "@next/swc-linux-arm-gnueabihf@12.3.1": 130 | version "12.3.1" 131 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm-gnueabihf/-/swc-linux-arm-gnueabihf-12.3.1.tgz#6e421c44285cfedac1f4631d5de330dd60b86298" 132 | integrity sha512-diL9MSYrEI5nY2wc/h/DBewEDUzr/DqBjIgHJ3RUNtETAOB3spMNHvJk2XKUDjnQuluLmFMloet9tpEqU2TT9w== 133 | 134 | "@next/swc-linux-arm64-gnu@12.3.1": 135 | version "12.3.1" 136 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-12.3.1.tgz#8863f08a81f422f910af126159d2cbb9552ef717" 137 | integrity sha512-o/xB2nztoaC7jnXU3Q36vGgOolJpsGG8ETNjxM1VAPxRwM7FyGCPHOMk1XavG88QZSQf+1r+POBW0tLxQOJ9DQ== 138 | 139 | "@next/swc-linux-arm64-musl@12.3.1": 140 | version "12.3.1" 141 | resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-12.3.1.tgz#0038f07cf0b259d70ae0c80890d826dfc775d9f3" 142 | integrity sha512-2WEasRxJzgAmP43glFNhADpe8zB7kJofhEAVNbDJZANp+H4+wq+/cW1CdDi8DqjkShPEA6/ejJw+xnEyDID2jg== 143 | 144 | "@next/swc-linux-x64-gnu@12.3.1": 145 | version "12.3.1" 146 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-12.3.1.tgz#c66468f5e8181ffb096c537f0dbfb589baa6a9c1" 147 | integrity sha512-JWEaMyvNrXuM3dyy9Pp5cFPuSSvG82+yABqsWugjWlvfmnlnx9HOQZY23bFq3cNghy5V/t0iPb6cffzRWylgsA== 148 | 149 | "@next/swc-linux-x64-musl@12.3.1": 150 | version "12.3.1" 151 | resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-12.3.1.tgz#c6269f3e96ac0395bc722ad97ce410ea5101d305" 152 | integrity sha512-xoEWQQ71waWc4BZcOjmatuvPUXKTv6MbIFzpm4LFeCHsg2iwai0ILmNXf81rJR+L1Wb9ifEke2sQpZSPNz1Iyg== 153 | 154 | "@next/swc-win32-arm64-msvc@12.3.1": 155 | version "12.3.1" 156 | resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-12.3.1.tgz#83c639ee969cee36ce247c3abd1d9df97b5ecade" 157 | integrity sha512-hswVFYQYIeGHE2JYaBVtvqmBQ1CppplQbZJS/JgrVI3x2CurNhEkmds/yqvDONfwfbttTtH4+q9Dzf/WVl3Opw== 158 | 159 | "@next/swc-win32-ia32-msvc@12.3.1": 160 | version "12.3.1" 161 | resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-12.3.1.tgz#52995748b92aa8ad053440301bc2c0d9fbcf27c2" 162 | integrity sha512-Kny5JBehkTbKPmqulr5i+iKntO5YMP+bVM8Hf8UAmjSMVo3wehyLVc9IZkNmcbxi+vwETnQvJaT5ynYBkJ9dWA== 163 | 164 | "@next/swc-win32-x64-msvc@12.3.1": 165 | version "12.3.1" 166 | resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-12.3.1.tgz#27d71a95247a9eaee03d47adee7e3bd594514136" 167 | integrity sha512-W1ijvzzg+kPEX6LAc+50EYYSEo0FVu7dmTE+t+DM4iOLqgGHoW9uYSz9wCVdkXOEEMP9xhXfGpcSxsfDucyPkA== 168 | 169 | "@swc/helpers@0.4.11": 170 | version "0.4.11" 171 | resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.4.11.tgz#db23a376761b3d31c26502122f349a21b592c8de" 172 | integrity sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw== 173 | dependencies: 174 | tslib "^2.4.0" 175 | 176 | "@types/highlight.js@^10.1.0": 177 | version "10.1.0" 178 | resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-10.1.0.tgz#89bb0c202997d7a90a07bd2ec1f7d00c56bb90b4" 179 | integrity sha512-77hF2dGBsOgnvZll1vymYiNUtqJ8cJfXPD6GG/2M0aLRc29PkvB7Au6sIDjIEFcSICBhCh2+Pyq6WSRS7LUm6A== 180 | dependencies: 181 | highlight.js "*" 182 | 183 | "@types/node@^17.0.5": 184 | version "17.0.5" 185 | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.5.tgz#57ca67ec4e57ad9e4ef5a6bab48a15387a1c83e0" 186 | integrity sha512-w3mrvNXLeDYV1GKTZorGJQivK6XLCoGwpnyJFbJVK/aTBQUxOCaa/GlFAAN3OTDFcb7h5tiFG+YXCO2By+riZw== 187 | 188 | "@types/prop-types@*": 189 | version "15.7.4" 190 | resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" 191 | integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== 192 | 193 | "@types/react-transition-group@^4.2.0": 194 | version "4.4.4" 195 | resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e" 196 | integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug== 197 | dependencies: 198 | "@types/react" "*" 199 | 200 | "@types/react@*", "@types/react@^17.0.38": 201 | version "17.0.38" 202 | resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd" 203 | integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== 204 | dependencies: 205 | "@types/prop-types" "*" 206 | "@types/scheduler" "*" 207 | csstype "^3.0.2" 208 | 209 | "@types/scheduler@*": 210 | version "0.16.2" 211 | resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" 212 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 213 | 214 | caniuse-lite@^1.0.30001406: 215 | version "1.0.30001431" 216 | resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001431.tgz#e7c59bd1bc518fae03a4656be442ce6c4887a795" 217 | integrity sha512-zBUoFU0ZcxpvSt9IU66dXVT/3ctO1cy4y9cscs1szkPlcWb6pasYM144GqrUygUbT+k7cmUCW61cvskjcv0enQ== 218 | 219 | clipboard-copy@^3.0.0: 220 | version "3.2.0" 221 | resolved "https://registry.yarnpkg.com/clipboard-copy/-/clipboard-copy-3.2.0.tgz#3c5b8651d3512dcfad295d77a9eb09e7fac8d5fb" 222 | integrity sha512-vooFaGFL6ulEP1liiaWFBmmfuPm3cY3y7T9eB83ZTnYc/oFeAKsq3NcDrOkBC8XaauEE8zHQwI7k0+JSYiVQSQ== 223 | 224 | clsx@^1.0.4, clsx@^1.1.1: 225 | version "1.1.1" 226 | resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" 227 | integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== 228 | 229 | css-vendor@^2.0.8: 230 | version "2.0.8" 231 | resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d" 232 | integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ== 233 | dependencies: 234 | "@babel/runtime" "^7.8.3" 235 | is-in-browser "^1.0.2" 236 | 237 | csstype@^2.5.2: 238 | version "2.6.19" 239 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.19.tgz#feeb5aae89020bb389e1f63669a5ed490e391caa" 240 | integrity sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ== 241 | 242 | csstype@^3.0.2: 243 | version "3.0.10" 244 | resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5" 245 | integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== 246 | 247 | dom-helpers@^5.0.1: 248 | version "5.2.1" 249 | resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" 250 | integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== 251 | dependencies: 252 | "@babel/runtime" "^7.8.7" 253 | csstype "^3.0.2" 254 | 255 | highlight.js@*, highlight.js@^11.3.1: 256 | version "11.3.1" 257 | resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-11.3.1.tgz#813078ef3aa519c61700f84fe9047231c5dc3291" 258 | integrity sha512-PUhCRnPjLtiLHZAQ5A/Dt5F8cWZeMyj9KRsACsWT+OD6OP0x6dp5OmT5jdx0JgEyPxPZZIPQpRN2TciUT7occw== 259 | 260 | hoist-non-react-statics@^3.3.2: 261 | version "3.3.2" 262 | resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" 263 | integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== 264 | dependencies: 265 | react-is "^16.7.0" 266 | 267 | hyphenate-style-name@^1.0.3: 268 | version "1.0.4" 269 | resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" 270 | integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== 271 | 272 | is-in-browser@^1.0.2, is-in-browser@^1.1.3: 273 | version "1.1.3" 274 | resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" 275 | integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= 276 | 277 | "js-tokens@^3.0.0 || ^4.0.0": 278 | version "4.0.0" 279 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 280 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 281 | 282 | jss-plugin-camel-case@^10.5.1: 283 | version "10.9.0" 284 | resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7" 285 | integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww== 286 | dependencies: 287 | "@babel/runtime" "^7.3.1" 288 | hyphenate-style-name "^1.0.3" 289 | jss "10.9.0" 290 | 291 | jss-plugin-default-unit@^10.5.1: 292 | version "10.9.0" 293 | resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991" 294 | integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w== 295 | dependencies: 296 | "@babel/runtime" "^7.3.1" 297 | jss "10.9.0" 298 | 299 | jss-plugin-global@^10.5.1: 300 | version "10.9.0" 301 | resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f" 302 | integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ== 303 | dependencies: 304 | "@babel/runtime" "^7.3.1" 305 | jss "10.9.0" 306 | 307 | jss-plugin-nested@^10.5.1: 308 | version "10.9.0" 309 | resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3" 310 | integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA== 311 | dependencies: 312 | "@babel/runtime" "^7.3.1" 313 | jss "10.9.0" 314 | tiny-warning "^1.0.2" 315 | 316 | jss-plugin-props-sort@^10.5.1: 317 | version "10.9.0" 318 | resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d" 319 | integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw== 320 | dependencies: 321 | "@babel/runtime" "^7.3.1" 322 | jss "10.9.0" 323 | 324 | jss-plugin-rule-value-function@^10.5.1: 325 | version "10.9.0" 326 | resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67" 327 | integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg== 328 | dependencies: 329 | "@babel/runtime" "^7.3.1" 330 | jss "10.9.0" 331 | tiny-warning "^1.0.2" 332 | 333 | jss-plugin-vendor-prefixer@^10.5.1: 334 | version "10.9.0" 335 | resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a" 336 | integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA== 337 | dependencies: 338 | "@babel/runtime" "^7.3.1" 339 | css-vendor "^2.0.8" 340 | jss "10.9.0" 341 | 342 | jss@10.9.0, jss@^10.5.1: 343 | version "10.9.0" 344 | resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b" 345 | integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw== 346 | dependencies: 347 | "@babel/runtime" "^7.3.1" 348 | csstype "^3.0.2" 349 | is-in-browser "^1.1.3" 350 | tiny-warning "^1.0.2" 351 | 352 | loose-envify@^1.1.0, loose-envify@^1.4.0: 353 | version "1.4.0" 354 | resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 355 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 356 | dependencies: 357 | js-tokens "^3.0.0 || ^4.0.0" 358 | 359 | nanoid@^3.3.4: 360 | version "3.3.4" 361 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 362 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 363 | 364 | next@12.3.1: 365 | version "12.3.1" 366 | resolved "https://registry.yarnpkg.com/next/-/next-12.3.1.tgz#127b825ad2207faf869b33393ec8c75fe61e50f1" 367 | integrity sha512-l7bvmSeIwX5lp07WtIiP9u2ytZMv7jIeB8iacR28PuUEFG5j0HGAPnMqyG5kbZNBG2H7tRsrQ4HCjuMOPnANZw== 368 | dependencies: 369 | "@next/env" "12.3.1" 370 | "@swc/helpers" "0.4.11" 371 | caniuse-lite "^1.0.30001406" 372 | postcss "8.4.14" 373 | styled-jsx "5.0.7" 374 | use-sync-external-store "1.2.0" 375 | optionalDependencies: 376 | "@next/swc-android-arm-eabi" "12.3.1" 377 | "@next/swc-android-arm64" "12.3.1" 378 | "@next/swc-darwin-arm64" "12.3.1" 379 | "@next/swc-darwin-x64" "12.3.1" 380 | "@next/swc-freebsd-x64" "12.3.1" 381 | "@next/swc-linux-arm-gnueabihf" "12.3.1" 382 | "@next/swc-linux-arm64-gnu" "12.3.1" 383 | "@next/swc-linux-arm64-musl" "12.3.1" 384 | "@next/swc-linux-x64-gnu" "12.3.1" 385 | "@next/swc-linux-x64-musl" "12.3.1" 386 | "@next/swc-win32-arm64-msvc" "12.3.1" 387 | "@next/swc-win32-ia32-msvc" "12.3.1" 388 | "@next/swc-win32-x64-msvc" "12.3.1" 389 | 390 | object-assign@^4.1.1: 391 | version "4.1.1" 392 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 393 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 394 | 395 | picocolors@^1.0.0: 396 | version "1.0.0" 397 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 398 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 399 | 400 | popper.js@1.16.1-lts: 401 | version "1.16.1-lts" 402 | resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" 403 | integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== 404 | 405 | postcss@8.4.14: 406 | version "8.4.14" 407 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" 408 | integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== 409 | dependencies: 410 | nanoid "^3.3.4" 411 | picocolors "^1.0.0" 412 | source-map-js "^1.0.2" 413 | 414 | prop-types@^15.6.2, prop-types@^15.7.2: 415 | version "15.8.0" 416 | resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.0.tgz#d237e624c45a9846e469f5f31117f970017ff588" 417 | integrity sha512-fDGekdaHh65eI3lMi5OnErU6a8Ighg2KjcjQxO7m8VHyWjcPyj5kiOgV1LQDOOOgVy3+5FgjXvdSSX7B8/5/4g== 418 | dependencies: 419 | loose-envify "^1.4.0" 420 | object-assign "^4.1.1" 421 | react-is "^16.13.1" 422 | 423 | react-dom@17.0.2: 424 | version "17.0.2" 425 | resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" 426 | integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA== 427 | dependencies: 428 | loose-envify "^1.1.0" 429 | object-assign "^4.1.1" 430 | scheduler "^0.20.2" 431 | 432 | react-is@^16.13.1, react-is@^16.7.0: 433 | version "16.13.1" 434 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" 435 | integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 436 | 437 | "react-is@^16.8.0 || ^17.0.0": 438 | version "17.0.2" 439 | resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" 440 | integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== 441 | 442 | react-transition-group@^4.4.0: 443 | version "4.4.2" 444 | resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" 445 | integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== 446 | dependencies: 447 | "@babel/runtime" "^7.5.5" 448 | dom-helpers "^5.0.1" 449 | loose-envify "^1.4.0" 450 | prop-types "^15.6.2" 451 | 452 | react@17.0.2: 453 | version "17.0.2" 454 | resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" 455 | integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== 456 | dependencies: 457 | loose-envify "^1.1.0" 458 | object-assign "^4.1.1" 459 | 460 | regenerator-runtime@^0.13.4: 461 | version "0.13.9" 462 | resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" 463 | integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== 464 | 465 | scheduler@^0.20.2: 466 | version "0.20.2" 467 | resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91" 468 | integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ== 469 | dependencies: 470 | loose-envify "^1.1.0" 471 | object-assign "^4.1.1" 472 | 473 | source-map-js@^1.0.2: 474 | version "1.0.2" 475 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 476 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 477 | 478 | styled-jsx@5.0.7: 479 | version "5.0.7" 480 | resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.0.7.tgz#be44afc53771b983769ac654d355ca8d019dff48" 481 | integrity sha512-b3sUzamS086YLRuvnaDigdAewz1/EFYlHpYBP5mZovKEdQQOIIYq8lApylub3HHZ6xFjV051kkGU7cudJmrXEA== 482 | 483 | tiny-warning@^1.0.2: 484 | version "1.0.3" 485 | resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" 486 | integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== 487 | 488 | tslib@^2.4.0: 489 | version "2.4.1" 490 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" 491 | integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== 492 | 493 | typescript@^4.5.4: 494 | version "4.5.4" 495 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" 496 | integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== 497 | 498 | use-clipboard-copy@^0.2.0: 499 | version "0.2.0" 500 | resolved "https://registry.yarnpkg.com/use-clipboard-copy/-/use-clipboard-copy-0.2.0.tgz#e1f31f2b21e369bc79b5d7b358e2c8aece6ef264" 501 | integrity sha512-f0PMMwZ2/Hh9/54L12capx4s6ASdd6edNJxg2OcqWVNM8BPvtOSmNFIN1Dg/q//fPp8MpUZceHfr7cnWOS0RxA== 502 | dependencies: 503 | clipboard-copy "^3.0.0" 504 | 505 | use-debounce@^7.0.1: 506 | version "7.0.1" 507 | resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-7.0.1.tgz#380e6191cc13ad29f8e2149a12b5c37cc2891190" 508 | integrity sha512-fOrzIw2wstbAJuv8PC9Vg4XgwyTLEOdq4y/Z3IhVl8DAE4svRcgyEUvrEXu+BMNgMoc3YND6qLT61kkgEKXh7Q== 509 | 510 | use-sync-external-store@1.2.0: 511 | version "1.2.0" 512 | resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" 513 | integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== 514 | -------------------------------------------------------------------------------- /widgets/.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | node_modules 3 | yarn.lock 4 | .DS_Store -------------------------------------------------------------------------------- /widgets/README.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | -------------------------------------------------------------------------------- /widgets/code/components/images/ErrorImage.ts: -------------------------------------------------------------------------------- 1 | interface Props { 2 | error?: any; 3 | width?: number; 4 | height?: number; 5 | } 6 | 7 | export const ErrorImage = ({ error, width, height }: Props) => { 8 | const text = `${error?.message || error}` 9 | const dc = new DrawContext() 10 | dc.size = new Size(width || 200, height || 200) 11 | dc.respectScreenScale = true 12 | dc.opaque = false 13 | dc.setTextColor(Color.red()) 14 | dc.setFont(Font.semiboldSystemFont(dc.size.width / 10)) 15 | dc.drawText(text, new Point(dc.size.width / 10, 8)) 16 | return dc.getImage() 17 | 18 | } -------------------------------------------------------------------------------- /widgets/code/components/images/SparkBarImage.ts: -------------------------------------------------------------------------------- 1 | import { DefaultColor } from "code/utils/color"; 2 | import { getWidgetSizeInPoint } from "code/utils/sizing"; 3 | import { ErrorImage } from "./ErrorImage"; 4 | 5 | interface Props { 6 | series: number[], 7 | width?: number, 8 | height?: number, 9 | color?: Color, 10 | lastBarColor?: Color, 11 | } 12 | 13 | export const SparkBarImage = ( 14 | { 15 | series, 16 | width, 17 | height, 18 | color = DefaultColor(), 19 | lastBarColor = Color.orange() 20 | }: Props 21 | ) => { 22 | if (series.length === 0) { 23 | return ErrorImage({ error: "No Data", width, height }) 24 | } 25 | const widgetSize = getWidgetSizeInPoint() 26 | const dc = new DrawContext() 27 | dc.size = new Size(width || widgetSize?.width || 200, height || widgetSize?.height || 200) 28 | dc.respectScreenScale = true 29 | dc.opaque = false 30 | 31 | const barColor = color 32 | const barWidth = (dc.size.width) / series.length - 4 33 | const maxValue = Math.max(...series); 34 | // Calculate the rendered height of the bars, make sure they're at least 1 pixel 35 | const pixelMultiplier = dc.size.height / maxValue; 36 | const pixelValues = series.map(v => Math.max(v * pixelMultiplier, 1)); 37 | 38 | // Draw the bars 39 | pixelValues.forEach((v, i) => { 40 | dc.setFillColor(i === pixelValues.length - 1 ? lastBarColor : barColor) 41 | const x = (dc.size.width * i / pixelValues.length) 42 | const barHeight = v; 43 | const y = dc.size.height - barHeight 44 | dc.fillRect(new Rect(x, y, barWidth, barHeight)) 45 | }); 46 | 47 | const image = dc.getImage() 48 | 49 | return image 50 | } -------------------------------------------------------------------------------- /widgets/code/components/images/UnsplashImage.ts: -------------------------------------------------------------------------------- 1 | import { ErrorImage } from "code/components/images/ErrorImage"; 2 | import { RequestWithTimeout } from "code/utils/request-utils"; 3 | 4 | interface Props { 5 | id?: string; 6 | width?: number; 7 | height?: number; 8 | } 9 | 10 | export const UnsplashImage = async ( 11 | { 12 | id = "random", 13 | width = 600, 14 | height = 600 15 | }: Props 16 | ) => { 17 | const req = RequestWithTimeout(`https://source.unsplash.com/${id}/${width}x${height}`) 18 | try { 19 | return await req.loadImage(); 20 | } catch (error) { 21 | return ErrorImage({ width, height, error }) 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /widgets/code/components/stacks/addFlexSpacer.ts: -------------------------------------------------------------------------------- 1 | interface Props { 2 | to: ListWidget | WidgetStack; 3 | } 4 | 5 | export const addFlexSpacer = ({ to }: Props) => { 6 | to.addSpacer(); 7 | }; 8 | -------------------------------------------------------------------------------- /widgets/code/components/stacks/addSymbol.ts: -------------------------------------------------------------------------------- 1 | import { DefaultColor } from "code/utils/color" 2 | 3 | interface Props { 4 | to: ListWidget | WidgetStack; 5 | symbol?: string; 6 | color?: Color; 7 | size?: number 8 | } 9 | 10 | export const addSymbol = ( 11 | { 12 | to, 13 | symbol = 'applelogo', 14 | color = DefaultColor(), 15 | size = 20, 16 | }: Props 17 | ) => { 18 | const _sym = SFSymbol.named(symbol) 19 | const wImg = to.addImage(_sym.image) 20 | wImg.tintColor = color 21 | wImg.imageSize = new Size(size, size) 22 | } 23 | -------------------------------------------------------------------------------- /widgets/code/components/stacks/addTextWithSymbolStack.ts: -------------------------------------------------------------------------------- 1 | import { addSymbol } from "code/components/stacks/addSymbol" 2 | import { DefaultColor } from "code/utils/color" 3 | 4 | interface Props { 5 | to: ListWidget | WidgetStack; 6 | text: string; 7 | symbol: string; 8 | textColor?: Color; 9 | symbolColor?: Color; 10 | fontSize?: number 11 | } 12 | 13 | export const addTextWithSymbolStack = ( 14 | { 15 | to, 16 | text, 17 | symbol, 18 | textColor = DefaultColor(), 19 | symbolColor = DefaultColor(), 20 | fontSize = 20, 21 | }: Props 22 | ) => { 23 | const _stack = to.addStack() 24 | _stack.centerAlignContent() 25 | 26 | addSymbol({ 27 | to: _stack, 28 | symbol, 29 | size: fontSize, 30 | color: symbolColor 31 | }) 32 | 33 | _stack.addSpacer(3) 34 | 35 | let _text = _stack.addText(text) 36 | _text.textColor = textColor; 37 | _text.font = Font.systemFont(fontSize) 38 | 39 | return _stack; 40 | } 41 | -------------------------------------------------------------------------------- /widgets/code/components/widgets/ErrorWidget.ts: -------------------------------------------------------------------------------- 1 | import { SimpleTextWidget } from "code/components/widgets/SimpleTextWidget" 2 | 3 | export const ErrorWidget = (subtitle: string) => { 4 | return SimpleTextWidget("ERROR", "Widget Error", subtitle, "#000") 5 | } 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /widgets/code/components/widgets/SimpleSparkBarWidget.ts: -------------------------------------------------------------------------------- 1 | import { SparkBarImage } from "../images/SparkBarImage" 2 | import { addFlexSpacer } from "../stacks/addFlexSpacer" 3 | 4 | interface TextProp { 5 | text: string; 6 | color: Color; 7 | } 8 | interface Props { 9 | series: number[]; 10 | header: TextProp; 11 | title: TextProp; 12 | description: TextProp; 13 | backgroundColor: Color; 14 | barColor: Color 15 | lastBarColor: Color; 16 | } 17 | 18 | export const SimpleSparkBarWidget = ({ series, header, title, description, backgroundColor, barColor, lastBarColor }: Props) => { 19 | const widget = new ListWidget() 20 | widget.backgroundColor = backgroundColor 21 | 22 | // Header 23 | const headerTxt = widget.addText(header.text) 24 | headerTxt.textColor = header.color 25 | headerTxt.font = Font.systemFont(10) 26 | 27 | // Vertical Space 28 | addFlexSpacer({ to: widget }) 29 | 30 | // BarChart (centered) 31 | const barStack = widget.addStack(); 32 | barStack.layoutHorizontally() 33 | addFlexSpacer({ to: barStack }) 34 | if (series.length > 0) { 35 | barStack.addImage(SparkBarImage({ 36 | series, 37 | color: barColor, 38 | lastBarColor, 39 | height: 100, 40 | width: 400 41 | })) 42 | } 43 | addFlexSpacer({ to: barStack }) 44 | 45 | // Vertical Space 46 | widget.addSpacer(10) 47 | 48 | // Title 49 | const titleTxt = widget.addText(title.text) 50 | titleTxt.textColor = title.color 51 | titleTxt.font = Font.boldSystemFont(16) 52 | 53 | // Vertical space 54 | widget.addSpacer(2) 55 | 56 | // Description 57 | const descriptionText = widget.addText(description.text) 58 | descriptionText.textColor = description.color 59 | descriptionText.font = Font.systemFont(12) 60 | 61 | return widget 62 | } -------------------------------------------------------------------------------- /widgets/code/components/widgets/SimpleTextWidget.ts: -------------------------------------------------------------------------------- 1 | import { addFlexSpacer } from "code/components/stacks/addFlexSpacer" 2 | 3 | export const SimpleTextWidget = (pretitle: string, title: string, subtitle: string, color: string) => { 4 | let w = new ListWidget() 5 | w.backgroundColor = new Color(color, 1) 6 | let preTxt = w.addText(pretitle) 7 | preTxt.textColor = Color.white() 8 | preTxt.textOpacity = 0.8 9 | preTxt.font = Font.systemFont(10) 10 | w.addSpacer(5) 11 | let titleTxt = w.addText(title) 12 | titleTxt.textColor = Color.white() 13 | titleTxt.font = Font.systemFont(16) 14 | w.addSpacer(5) 15 | let subTxt = w.addText(subtitle) 16 | subTxt.textColor = Color.white() 17 | subTxt.textOpacity = 0.8 18 | subTxt.font = Font.systemFont(12) 19 | addFlexSpacer({ to: w }); 20 | let a = w.addText("") 21 | a.textColor = Color.white() 22 | a.textOpacity = 0.8 23 | a.font = Font.systemFont(12) 24 | return w 25 | } -------------------------------------------------------------------------------- /widgets/code/utils/color.ts: -------------------------------------------------------------------------------- 1 | export const DynamicColor = ({ lightColor, darkColor }: { lightColor: Color, darkColor: Color }): Color => 2 | Color.dynamic(lightColor, darkColor); 3 | 4 | export const DefaultColor = () => DynamicColor({ lightColor: Color.white(), darkColor: Color.black() }) -------------------------------------------------------------------------------- /widgets/code/utils/debug-utils.ts: -------------------------------------------------------------------------------- 1 | export const logToWidget = (widget: ListWidget, message: string) => { 2 | console.log(message) 3 | let a = widget.addText(message) 4 | a.textColor = Color.red() 5 | a.textOpacity = 0.8 6 | a.font = Font.systemFont(10) 7 | } -------------------------------------------------------------------------------- /widgets/code/utils/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface IWidgetModuleParams { 2 | widgetParameter: string; 3 | debug: boolean 4 | } 5 | export interface IWidgetModule { 6 | createWidget: (params: IWidgetModuleParams) => Promise; 7 | } 8 | 9 | export interface IWidgetModuleDownloadConfig { 10 | moduleName: string; 11 | rootUrl: string; 12 | defaultWidgetParameter: string; 13 | downloadQueryString: string; 14 | } -------------------------------------------------------------------------------- /widgets/code/utils/request-utils.ts: -------------------------------------------------------------------------------- 1 | export const RequestWithTimeout = (url: string, timeoutSeconds = 5) => { 2 | const request = new Request(url) 3 | request.timeoutInterval = timeoutSeconds 4 | return request 5 | } -------------------------------------------------------------------------------- /widgets/code/utils/sizing.ts: -------------------------------------------------------------------------------- 1 | // From https://talk.automators.fm/t/get-available-widget-height-and-width-depending-on-the-devices-screensize/9258/4 2 | 3 | export type WidgetSize = "small" | "medium" | "large" | null; 4 | 5 | type SizeMap = { 6 | [key: string]: { 7 | "small": [number, number], 8 | "medium": [number, number], 9 | "large": [number, number] 10 | } 11 | } 12 | 13 | export const getWidgetSizeInPoint = ( 14 | widgetSize: WidgetSize = (config.widgetFamily ? config.widgetFamily : null) as WidgetSize 15 | ) => { 16 | // stringify device screen size 17 | const devSize = `${Device.screenSize().width}x${Device.screenSize().height}` 18 | // screen size to widget size mapping for iPhone, excluding the latest iPhone 12 series. iPad size 19 | const sizeMap: SizeMap = { 20 | // iPad Mini 2/3/4, iPad 3/4, iPad Air 1/2. 9.7" iPad Pro 21 | // '768x1024': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 22 | // 10.2" iPad 23 | // '1080x810': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 24 | // 10.5" iPad Pro, 10.5" iPad Air 3rd Gen 25 | // '1112x834': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 26 | // 10.9" iPad Air 4th Gen 27 | // '1180x820': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 28 | // 11" iPad Pro 29 | '1194x834': { small: [155, 155], medium: [329, 155], large: [345, 329] }, 30 | // 12.9" iPad Pro 31 | '1366x1024': { small: [170, 170], medium: [332, 170], large: [382, 332] }, 32 | // 12 Pro Max 33 | // '428x926': { small: [0, 0], medium: [0, 0], large: [0, 0] }, 34 | // XR, 11, 11 Pro Max 35 | '414x896': { small: [169, 169], medium: [360, 169], large: [360, 376] }, 36 | // 12, 12 Pro 37 | // '390x844': : { small: [0, 0], medium: [0, 0], large: [0, 0] }, 38 | // X, XS, 11 Pro, 12 Mini 39 | '375x812': { small: [155, 155], medium: [329, 155], large: [329, 345] }, 40 | // 6/7/8(S) Plus 41 | '414x736': { small: [159, 159], medium: [348, 159], large: [348, 357] }, 42 | // 6/7/8(S) and 2nd Gen SE 43 | '375x667': { small: [148, 148], medium: [322, 148], large: [322, 324] }, 44 | // 1st Gen SE 45 | '320x568': { small: [141, 141], medium: [291, 141], large: [291, 299] } 46 | } 47 | let widgetSizeInPoint: Size | null = null 48 | 49 | if (widgetSize) { 50 | let mappedSize = sizeMap[devSize] 51 | if (mappedSize) { 52 | widgetSizeInPoint = new Size(...mappedSize[widgetSize]) 53 | } 54 | } 55 | return widgetSizeInPoint 56 | } -------------------------------------------------------------------------------- /widgets/code/utils/widget-loader-utils.ts: -------------------------------------------------------------------------------- 1 | import { IWidgetModuleDownloadConfig } from "code/utils/interfaces"; 2 | import { RequestWithTimeout } from "./request-utils"; 3 | 4 | const ROOT_MODULE_PATH = "widget-loader"; 5 | 6 | 7 | export const widgetModuleDownloadConfig: IWidgetModuleDownloadConfig = { 8 | moduleName: "__moduleName__", 9 | rootUrl: "__rootUrl__", 10 | defaultWidgetParameter: "__defaultWidgetParameter__", 11 | downloadQueryString: "__downloadQueryString__", 12 | } 13 | 14 | async function getOrCreateWidgetModule( 15 | { moduleName, rootUrl, downloadQueryString }: IWidgetModuleDownloadConfig, 16 | forceDownload = false 17 | ) { 18 | const fm = FileManager.local() 19 | 20 | const rootModuleDir = fm.joinPath(fm.libraryDirectory(), ROOT_MODULE_PATH) 21 | enforceDir(fm, rootModuleDir); 22 | 23 | const widgetModuleDir = fm.joinPath(rootModuleDir, moduleName) 24 | enforceDir(fm, widgetModuleDir); 25 | 26 | const widgetModuleFilename = `${moduleName}.js` 27 | const widgetModuleEtag = `${moduleName}.etag` 28 | const widgetModulePath = fm.joinPath(widgetModuleDir, widgetModuleFilename) 29 | const widgetModuleEtagPath = fm.joinPath(widgetModuleDir, widgetModuleEtag) 30 | const widgetModuleDownloadUrl = rootUrl + widgetModuleFilename + (downloadQueryString.startsWith("?") ? downloadQueryString : "") 31 | 32 | try { 33 | // Check if an etag was saved for this file 34 | if (fm.fileExists(widgetModuleEtagPath) && !forceDownload) { 35 | const lastEtag = fm.readString(widgetModuleEtagPath) 36 | const headerReq = RequestWithTimeout(widgetModuleDownloadUrl); 37 | headerReq.method = "HEAD"; 38 | await headerReq.load() 39 | const etag = getResponseHeader(headerReq, "Etag"); 40 | if (lastEtag && etag && lastEtag === etag) { 41 | console.log(`ETag is same, return cached file for ${widgetModuleDownloadUrl}`) 42 | return widgetModulePath; 43 | } 44 | } 45 | 46 | console.log("Downloading library file '" + widgetModuleDownloadUrl + "' to '" + widgetModulePath + "'") 47 | const req = RequestWithTimeout(widgetModuleDownloadUrl) 48 | const libraryFile = await req.load() 49 | const etag = getResponseHeader(req, "Etag"); 50 | if (etag) { 51 | fm.writeString(widgetModuleEtagPath, etag) 52 | } 53 | fm.write(widgetModulePath, libraryFile) 54 | } catch (error) { 55 | console.error("Downloading module failed, return existing module") 56 | console.error(error) 57 | } 58 | 59 | return widgetModulePath 60 | } 61 | 62 | const getResponseHeader = (request: Request, header: string) => { 63 | if (!request.response) { 64 | return undefined; 65 | } 66 | const key = Object.keys(request.response["headers"]) 67 | .find(key => key.toLowerCase() === header.toLowerCase()); 68 | return key ? request.response["headers"][key] : undefined 69 | } 70 | 71 | const enforceDir = (fm: FileManager, path: string) => { 72 | if (fm.fileExists(path) && !fm.isDirectory(path)) { 73 | fm.remove(path) 74 | } 75 | if (!fm.fileExists(path)) { 76 | fm.createDirectory(path) 77 | } 78 | } 79 | 80 | export { getOrCreateWidgetModule }; 81 | 82 | 83 | -------------------------------------------------------------------------------- /widgets/code/widget-modules/README.md: -------------------------------------------------------------------------------- 1 | # Widget Module directory 2 | 3 | Any file ending with `WidgetModule.ts` will be compiled by rollup. -------------------------------------------------------------------------------- /widgets/code/widget-modules/covid19WidgetModule.ts: -------------------------------------------------------------------------------- 1 | // Based on https://gist.github.com/planecore/e7b4c1e5db2dd28b1a023860e831355e 2 | 3 | import { ErrorWidget } from "code/components/widgets/ErrorWidget" 4 | import { SimpleTextWidget } from "code/components/widgets/SimpleTextWidget" 5 | import { IWidgetModule } from "code/utils/interfaces" 6 | import { RequestWithTimeout } from "code/utils/request-utils" 7 | 8 | const createWidget = async (country?: string) => { 9 | if (!country) { 10 | return ErrorWidget("No country") 11 | } 12 | const url = `https://coronavirus-19-api.herokuapp.com/countries/${country}` 13 | const req = RequestWithTimeout(url) 14 | const res = await req.loadJSON() 15 | return SimpleTextWidget("Coronavirus", `${res.todayCases} Today`, `${res.cases} Total`, "#53d769") 16 | } 17 | 18 | const widgetModule: IWidgetModule = { 19 | createWidget: async (params) => { 20 | return createWidget(params.widgetParameter) 21 | } 22 | } 23 | 24 | 25 | module.exports = widgetModule; 26 | 27 | -------------------------------------------------------------------------------- /widgets/code/widget-modules/kitchenSinkWidgetModule.ts: -------------------------------------------------------------------------------- 1 | import { SparkBarImage } from "code/components/images/SparkBarImage"; 2 | import { UnsplashImage } from "code/components/images/UnsplashImage"; 3 | import { addFlexSpacer } from "code/components/stacks/addFlexSpacer"; 4 | import { addTextWithSymbolStack } from "code/components/stacks/addTextWithSymbolStack"; 5 | import { IWidgetModule } from "code/utils/interfaces"; 6 | 7 | 8 | const widgetModule: IWidgetModule = { 9 | createWidget: async (params) => { 10 | const widget = new ListWidget(); 11 | widget.setPadding(8, 0, 0, 0) 12 | widget.backgroundImage = await UnsplashImage({ id: "KuF8-6EbBMs", width: 500, height: 500 }); 13 | 14 | 15 | const mainStack = widget.addStack(); 16 | mainStack.layoutVertically(); 17 | 18 | addFlexSpacer({ to: mainStack }) 19 | 20 | // Start Content 21 | const contentStack = mainStack.addStack() 22 | contentStack.layoutVertically(); 23 | contentStack.setPadding(0, 16, 0, 16) 24 | 25 | contentStack.addImage(SparkBarImage({ 26 | series: [800_000, 780_000, 760_000, 738_000, 680_000, 650_000, 600_000, 554_600, 500_000, 438_000], 27 | width: 400, 28 | height: 100, 29 | color: new Color(Color.white().hex, 0.6), 30 | lastBarColor: Color.orange() 31 | })) 32 | 33 | contentStack.addSpacer(8) 34 | 35 | let title = contentStack.addText("438.000 cases") 36 | title.textColor = Color.orange() 37 | title.font = Font.semiboldSystemFont(14) 38 | 39 | contentStack.addSpacer(2) 40 | 41 | let _text = contentStack.addText("A 50% decrease in the last 10 years") 42 | _text.textColor = Color.white() 43 | _text.font = Font.systemFont(12) 44 | // End Content 45 | 46 | addFlexSpacer({ to: mainStack }) 47 | 48 | // Footer 49 | addStatsStack({ stack: mainStack }) 50 | 51 | 52 | return widget; 53 | 54 | } 55 | } 56 | 57 | const addStatsStack = ({ stack }: { stack: WidgetStack }) => { 58 | const statsStack = stack.addStack() 59 | statsStack.centerAlignContent(); 60 | statsStack.backgroundColor = new Color(Color.black().hex, 0.85) 61 | statsStack.setPadding(6, 16, 6, 16) 62 | 63 | addTextWithSymbolStack({ 64 | to: statsStack, 65 | symbol: "person.crop.circle", 66 | text: "0,50", 67 | fontSize: 10, 68 | textColor: Color.lightGray(), 69 | symbolColor: Color.lightGray() 70 | }) 71 | addFlexSpacer({ to: statsStack }); 72 | addTextWithSymbolStack({ 73 | to: statsStack, 74 | symbol: "network", 75 | text: "11K", 76 | fontSize: 10, 77 | textColor: Color.lightGray(), 78 | symbolColor: Color.lightGray() 79 | }) 80 | return statsStack; 81 | } 82 | 83 | 84 | module.exports = widgetModule; 85 | 86 | -------------------------------------------------------------------------------- /widgets/code/widget-modules/simpleAnalyticsWidgetModule.ts: -------------------------------------------------------------------------------- 1 | import { SimpleSparkBarWidget } from "code/components/widgets/SimpleSparkBarWidget"; 2 | import { IWidgetModule } from "code/utils/interfaces"; 3 | import { RequestWithTimeout } from "code/utils/request-utils"; 4 | 5 | const widgetModule: IWidgetModule = { 6 | createWidget: async (params) => { 7 | const { website, apiKey } = parseWidgetParameter(params.widgetParameter) 8 | 9 | // Styling 10 | const highlightColor = new Color("#b93545", 1.0) 11 | const textColor = new Color("#a4bdc0", 1.0) 12 | const backgroundColor = new Color("#20292a", 1) 13 | const barColor = new Color("#198c9f", 1) 14 | 15 | // Fallback data 16 | let series: number[] = [] 17 | let titleText = "No data" 18 | let descriptionText = "Check the parameter settings" 19 | 20 | // Load data 21 | const data = await requestSimpleAnalyticsData({ website, apiKey }) 22 | if (data) { 23 | const pageViewsToday = data.visits[data.visits.length - 1]?.pageviews || 0 24 | series = data.visits.map(visit => visit.pageviews) 25 | titleText = `${pageViewsToday} views` 26 | descriptionText = `${data.pageviews} this month` 27 | } 28 | 29 | const widget = SimpleSparkBarWidget({ 30 | series, 31 | header: { text: website, color: textColor }, 32 | title: { text: titleText, color: highlightColor }, 33 | description: { text: descriptionText, color: textColor }, 34 | backgroundColor, 35 | barColor, 36 | lastBarColor: highlightColor, 37 | }) 38 | 39 | if (website) { 40 | // Open Simple Analytics stats when tapped 41 | widget.url = `https://simpleanalytics.com/${website}` 42 | } 43 | 44 | return widget 45 | } 46 | } 47 | 48 | module.exports = widgetModule; 49 | 50 | // SimpleAnalytics helpers 51 | const parseWidgetParameter = (param: string) => { 52 | // handles: @ || @ || 53 | const paramParts = param.toLowerCase().replace(/ /g, "").split("@") 54 | let apiKey: string = ""; 55 | let website: string = ""; 56 | 57 | switch (paramParts.length) { 58 | case 1: [website] = paramParts; break; 59 | case 2: [apiKey, website] = paramParts; break; 60 | } 61 | 62 | return { apiKey, website } 63 | } 64 | 65 | const formatDateQueryParam = (date: Date) => 66 | date.toISOString().split('T')[0] 67 | 68 | interface SimpleAnalyticsDataRequest { 69 | website: string; 70 | apiKey: string; 71 | daysAgo?: number 72 | } 73 | const requestSimpleAnalyticsData = async ( 74 | { website, apiKey, daysAgo = 31 }: SimpleAnalyticsDataRequest 75 | ): Promise => { 76 | const today = new Date() 77 | const startDate = new Date(new Date().setDate(today.getDate() - daysAgo)) 78 | const url = `https://simpleanalytics.com/${website}.json?version=2&start=${formatDateQueryParam(startDate)}&end=${formatDateQueryParam(today)}` 79 | const req = RequestWithTimeout(url) 80 | if (apiKey) { 81 | req.headers = { "Api-Key": apiKey } 82 | } 83 | const data = await req.loadJSON(); 84 | return req.response.statusCode === 200 ? data as SimpleanalyticsData : null; 85 | } 86 | 87 | // interfaces 88 | type DeviceType = "mobile" | "desktop" | "tablet"; 89 | 90 | interface SimpleanalyticsData { 91 | docs: string; 92 | hostname: string; 93 | url: string; 94 | start: string; 95 | end: string; 96 | ok: boolean; 97 | error: null; 98 | version: number; 99 | path: string; 100 | pageviews: number; 101 | agents: { 102 | count: number; 103 | browser_name: string; 104 | browser_version: string; 105 | os_name: string; 106 | os_version: string; 107 | type: DeviceType; 108 | }; 109 | visits: { 110 | date: string; 111 | pageviews: number; 112 | uniques: number; 113 | }[]; 114 | pages: { 115 | value: string; 116 | visits: number; 117 | uniqueVisits: number; 118 | }[]; 119 | countries: { 120 | value: string; 121 | visits: number; 122 | uniqueVisits: number; 123 | }[]; 124 | referrers: { 125 | value: string; 126 | visits: number; 127 | uniqueVisits: number; 128 | }[]; 129 | browser_names: { 130 | value: string; 131 | visits: number; 132 | uniqueVisits: number; 133 | }[]; 134 | os_names: { 135 | value: string; 136 | visits: number; 137 | uniqueVisits: number; 138 | }[]; 139 | device_types: { 140 | value: DeviceType; 141 | visits: number; 142 | uniqueVisits: number; 143 | }[]; 144 | // missing utm stuff 145 | } -------------------------------------------------------------------------------- /widgets/code/widget-modules/stickyWidgetModule.ts: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/drewkerr/scriptable/blob/main/Sticky%20widget.js 2 | 3 | import { IWidgetModule } from "code/utils/interfaces" 4 | 5 | const createWidget = (note: string) => { 6 | let widget = new ListWidget() 7 | widget.setPadding(16, 16, 16, 8) 8 | let dark = Device.isUsingDarkAppearance() 9 | let fgColor = Color.black() 10 | if (dark) { 11 | fgColor = new Color("#FFCF00", 1) 12 | let bgColor = Color.black() 13 | widget.backgroundColor = bgColor 14 | } else { 15 | let startColor = new Color("#F8DE5F", 1) 16 | let endColor = new Color("#FFCF00", 1) 17 | let gradient = new LinearGradient() 18 | gradient.colors = [startColor, endColor] 19 | gradient.locations = [0.0, 1] 20 | widget.backgroundGradient = gradient 21 | } 22 | let noteText = widget.addText(note) 23 | noteText.textColor = fgColor 24 | noteText.font = Font.mediumRoundedSystemFont(24) 25 | noteText.textOpacity = 0.8 26 | noteText.minimumScaleFactor = 0.25 27 | return widget 28 | } 29 | 30 | const widgetModule: IWidgetModule = { 31 | createWidget: async (params) => { 32 | return createWidget(params.widgetParameter) 33 | } 34 | } 35 | 36 | module.exports = widgetModule; -------------------------------------------------------------------------------- /widgets/code/widgetLoader.ts: -------------------------------------------------------------------------------- 1 | import { logToWidget } from "code/utils/debug-utils"; 2 | import { IWidgetModule } from "code/utils/interfaces"; 3 | import { getOrCreateWidgetModule, widgetModuleDownloadConfig } from "code/utils/widget-loader-utils"; 4 | 5 | const DEBUG = false; 6 | const FORCE_DOWNLOAD = false; 7 | const VERSION = "0.2"; 8 | 9 | const widgetModulePath = await getOrCreateWidgetModule(widgetModuleDownloadConfig, FORCE_DOWNLOAD) 10 | const widgetModule: IWidgetModule = importModule(widgetModulePath) 11 | const widget = await widgetModule.createWidget({ 12 | widgetParameter: args.widgetParameter || widgetModuleDownloadConfig.defaultWidgetParameter, 13 | debug: DEBUG 14 | }); 15 | 16 | if (DEBUG) { 17 | logToWidget(widget, args.widgetParameter) 18 | } 19 | 20 | // preview the widget if in app 21 | if (!config.runsInWidget) { 22 | await widget.presentSmall() 23 | } 24 | 25 | Script.setWidget(widget) 26 | Script.complete() 27 | -------------------------------------------------------------------------------- /widgets/eslint-config/eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true 4 | }, 5 | "parserOptions": { 6 | "globalReturn": true, 7 | "impliedStrict": true 8 | }, 9 | "globals": { 10 | "Alert": "readonly", 11 | "Calendar": "readonly", 12 | "CalendarEvent": "readonly", 13 | "CallbackURL": "readonly", 14 | "Color": "readonly", 15 | "Contact": "readonly", 16 | "ContactsContainer": "readonly", 17 | "ContactsGroup": "readonly", 18 | "Data": "readonly", 19 | "DateFormatter": "readonly", 20 | "DatePicker": "readonly", 21 | "Device": "readonly", 22 | "Dictation": "readonly", 23 | "DocumentPicker": "readonly", 24 | "DrawContext": "readonly", 25 | "FileManager": "readonly", 26 | "Font": "readonly", 27 | "Image": "readonly", 28 | "Keychain": "readonly", 29 | "LinearGradient": "readonly", 30 | "ListWidget": "readonly", 31 | "Location": "readonly", 32 | "Mail": "readonly", 33 | "Message": "readonly", 34 | "Notification": "readonly", 35 | "Pasteboard": "readonly", 36 | "Path": "readonly", 37 | "Photos": "readonly", 38 | "Point": "readonly", 39 | "QuickLook": "readonly", 40 | "Rect": "readonly", 41 | "RecurrenceRule": "readonly", 42 | "RelativeDateTimeFormatter": "readonly", 43 | "Reminder": "readonly", 44 | "Request": "readonly", 45 | "SFSymbol": "readonly", 46 | "Safari": "readonly", 47 | "Script": "readonly", 48 | "ShareSheet": "readonly", 49 | "Size": "readonly", 50 | "Speech": "readonly", 51 | "Timer": "readonly", 52 | "UITable": "readonly", 53 | "UITableCell": "readonly", 54 | "UITableRow": "readonly", 55 | "URLScheme": "readonly", 56 | "UUID": "readonly", 57 | "WebView": "readonly", 58 | "WidgetDate": "readonly", 59 | "WidgetImage": "readonly", 60 | "WidgetSpacer": "readonly", 61 | "WidgetStack": "readonly", 62 | "WidgetText": "readonly", 63 | "XMLParser": "readonly", 64 | "args": "readonly", 65 | "atob": "readonly", 66 | "await": "readonly", 67 | "btoa": "readonly", 68 | "config": "readonly", 69 | "console": "readonly", 70 | "importModule": "readonly", 71 | "log": "readonly", 72 | "logError": "readonly", 73 | "logWarning": "readonly", 74 | "module": "readonly" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /widgets/eslint-config/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./eslintrc.json"); 2 | -------------------------------------------------------------------------------- /widgets/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scriptable-ios/eslint-config", 3 | "version": "1.5.1", 4 | "description": "ESLint config for iOS Scriptable scripts", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/schl3ck/ios-scriptable-types.git" 9 | }, 10 | "keywords": [ 11 | "eslint", 12 | "eslintconfig", 13 | "scriptable" 14 | ], 15 | "author": "schl3ck", 16 | "license": "GPL-3.0", 17 | "peerDependencies": { 18 | "eslint": "^6.8.0" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/schl3ck/ios-scriptable-types/issues" 22 | }, 23 | "homepage": "https://github.com/schl3ck/ios-scriptable-types#readme" 24 | } 25 | -------------------------------------------------------------------------------- /widgets/eslint-config/readme.md: -------------------------------------------------------------------------------- 1 | # [ESLint](https://eslint.org) config for iOS app Scriptable 2 | 3 | This config was generated from the [online documentation of Scriptable](https://docs.scriptable.app) by the project [ios-scriptable-types](https://github.com/schl3ck/ios-scriptable-types) 4 | 5 | If there are any problems, issues or questions, please [open an issue](https://github.com/schl3ck/ios-scriptable-types/issues) at [ios-scriptable-types](https://github.com/schl3ck/ios-scriptable-types) -------------------------------------------------------------------------------- /widgets/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "ES2018" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /widgets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scriptable", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup --config rollup.config.js", 6 | "watch": "rollup --config rollup.config.js --watch" 7 | }, 8 | "eslintConfig": { 9 | "extends": "@scriptable-ios" 10 | }, 11 | "devDependencies": { 12 | "@rollup/plugin-typescript": "^8.3.0", 13 | "@scriptable-ios/eslint-config": "file:eslint-config", 14 | "eslint": "8.5.0", 15 | "rollup": "^2.62.0", 16 | "tslib": "^2.3.1", 17 | "typescript": "^4.5.4" 18 | }, 19 | "dependencies": { 20 | "@types/scriptable-ios": "^1.6.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /widgets/rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import { readdirSync } from "fs"; 3 | import { parse } from "path"; 4 | 5 | const WIDGET_LOADER_BANNER = `// Variables used by Scriptable. 6 | // These must be at the very top of the file. Do not edit. 7 | // icon-color: __iconColor__; icon-glyph: __iconGlyph__; 8 | `; 9 | 10 | const widgetModuleFilenames = readdirSync("code/widget-modules/") 11 | .filter(fileName => fileName.endsWith("WidgetModule.ts")); 12 | 13 | export default [ 14 | { 15 | input: 'code/widgetLoader.ts', 16 | output: { 17 | dir: '../scriptable-api/public/compiled-widgets/', 18 | format: 'es', 19 | strict: false, 20 | banner: WIDGET_LOADER_BANNER, 21 | }, 22 | plugins: [typescript()] 23 | }, 24 | ...(widgetModuleFilenames.map(fileName => ({ 25 | input: `code/widget-modules/${fileName}`, 26 | output: { 27 | dir: '../scriptable-api/public/compiled-widgets/widget-modules', 28 | format: 'iife', 29 | strict: false, 30 | name: parse(fileName).name 31 | }, 32 | plugins: [typescript()] 33 | 34 | }))), 35 | ]; -------------------------------------------------------------------------------- /widgets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "lib": [ 5 | "ES2018" 6 | ], 7 | "baseUrl": "./", 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": false, 11 | "strictNullChecks": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noEmit": false, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": false, 19 | "jsx": "preserve" 20 | }, 21 | "exclude": [ 22 | "node_modules" 23 | ], 24 | "include": [ 25 | "**/*.ts", 26 | ] 27 | } --------------------------------------------------------------------------------