├── 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 |
24 |
25 |
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 |
194 | Open the Scriptable App
195 |
196 |
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 |
98 | Copy
99 | {clipboard.copied ? : }
100 |
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 |
91 | {isSelected ? "Selected" : "Select"}
92 |
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 | }
--------------------------------------------------------------------------------