├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── ui ├── favicon.ico ├── images │ └── icons │ │ ├── apple-touch-icon.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-180x180.png │ │ ├── icon-192x192.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── icon-72x72.png │ │ └── icon-96x96.png ├── index.html ├── manifest.webmanifest ├── package-lock.json ├── package.json ├── postcss.config.js ├── robots.txt ├── src │ ├── UpRadioApi.ts │ ├── UpRadioPeer │ │ ├── UpRadioPeer.ts │ │ ├── UpRadioPeerRpc.ts │ │ └── index.ts │ ├── UpRadioState.ts │ ├── app.html │ ├── app.ts │ ├── components │ │ ├── Channel │ │ │ ├── ChannelEdit.component.html │ │ │ ├── ChannelEdit.component.ts │ │ │ ├── ChannelInfo.component.html │ │ │ └── ChannelInfo.component.ts │ │ ├── Connect │ │ │ ├── Connect.component.html │ │ │ ├── Connect.component.ts │ │ │ └── index.ts │ │ ├── Identicon │ │ │ └── Identicon.component.ts │ │ ├── LevelMeter │ │ │ ├── LevelMeter.component.ts │ │ │ └── index.ts │ │ ├── ModeSwitch │ │ │ ├── ModeSwitch.component.html │ │ │ ├── ModeSwitch.component.ts │ │ │ └── index.ts │ │ ├── Status │ │ │ ├── Status.component.html │ │ │ ├── Status.component.ts │ │ │ └── index.ts │ │ ├── Streams │ │ │ ├── LocalStream.component.html │ │ │ ├── LocalStream.component.ts │ │ │ ├── RemoteStream.component.html │ │ │ ├── RemoteStream.component.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── logger.ts │ ├── main.js │ ├── styles.css │ └── sw.js ├── tailwind.config.js ├── tsconfig.json ├── typings.d.ts ├── webpack.config.js ├── webpack.dev.js ├── webpack.prod.js └── zondicons │ ├── .DS_Store │ ├── add-outline.svg │ ├── add-solid.svg │ ├── adjust.svg │ ├── airplane.svg │ ├── album.svg │ ├── align-center.svg │ ├── align-justified.svg │ ├── align-left.svg │ ├── align-right.svg │ ├── anchor.svg │ ├── announcement.svg │ ├── apparel.svg │ ├── arrow-down.svg │ ├── arrow-left.svg │ ├── arrow-outline-down.svg │ ├── arrow-outline-left.svg │ ├── arrow-outline-right.svg │ ├── arrow-outline-up.svg │ ├── arrow-right.svg │ ├── arrow-thick-down.svg │ ├── arrow-thick-left.svg │ ├── arrow-thick-right.svg │ ├── arrow-thick-up.svg │ ├── arrow-thin-down.svg │ ├── arrow-thin-left.svg │ ├── arrow-thin-right.svg │ ├── arrow-thin-up.svg │ ├── arrow-up.svg │ ├── artist.svg │ ├── at-symbol.svg │ ├── attachment.svg │ ├── backspace.svg │ ├── backward-step.svg │ ├── backward.svg │ ├── badge.svg │ ├── battery-full.svg │ ├── battery-half.svg │ ├── battery-low.svg │ ├── beverage.svg │ ├── block.svg │ ├── bluetooth.svg │ ├── bolt.svg │ ├── book-reference.svg │ ├── bookmark copy 2.svg │ ├── bookmark copy 3.svg │ ├── bookmark-outline-add.svg │ ├── bookmark-outline.svg │ ├── bookmark.svg │ ├── border-all.svg │ ├── border-bottom.svg │ ├── border-horizontal.svg │ ├── border-inner.svg │ ├── border-left.svg │ ├── border-none.svg │ ├── border-outer.svg │ ├── border-right.svg │ ├── border-top.svg │ ├── border-vertical.svg │ ├── box.svg │ ├── brightness-down.svg │ ├── brightness-up.svg │ ├── browser-window-new.svg │ ├── browser-window-open.svg │ ├── browser-window.svg │ ├── bug.svg │ ├── buoy.svg │ ├── calculator.svg │ ├── calendar.svg │ ├── camera.svg │ ├── chart-bar.svg │ ├── chart-pie.svg │ ├── chart.svg │ ├── chat-bubble-dots.svg │ ├── checkmark-outline.svg │ ├── checkmark.svg │ ├── cheveron-down.svg │ ├── cheveron-left.svg │ ├── cheveron-outline-down.svg │ ├── cheveron-outline-left.svg │ ├── cheveron-outline-right.svg │ ├── cheveron-outline-up.svg │ ├── cheveron-right.svg │ ├── cheveron-up.svg │ ├── clipboard.svg │ ├── close-outline.svg │ ├── close-solid.svg │ ├── close.svg │ ├── cloud-upload.svg │ ├── cloud.svg │ ├── code.svg │ ├── coffee.svg │ ├── cog.svg │ ├── color-palette.svg │ ├── compose.svg │ ├── computer-desktop.svg │ ├── computer-laptop.svg │ ├── conversation.svg │ ├── copy.svg │ ├── credit-card.svg │ ├── currency-dollar.svg │ ├── dashboard.svg │ ├── date-add.svg │ ├── dial-pad.svg │ ├── directions.svg │ ├── document-add.svg │ ├── document.svg │ ├── dots-horizontal-double.svg │ ├── dots-horizontal-triple.svg │ ├── download.svg │ ├── duplicate.svg │ ├── edit-copy.svg │ ├── edit-crop.svg │ ├── edit-cut.svg │ ├── edit-pencil.svg │ ├── education.svg │ ├── envelope.svg │ ├── exclamation-outline.svg │ ├── exclamation-solid.svg │ ├── explore.svg │ ├── factory.svg │ ├── fast-forward.svg │ ├── fast-rewind.svg │ ├── film.svg │ ├── filter.svg │ ├── flag.svg │ ├── flashlight.svg │ ├── folder-outline-add.svg │ ├── folder-outline.svg │ ├── folder.svg │ ├── format-bold.svg │ ├── format-font-size.svg │ ├── format-italic.svg │ ├── format-text-size.svg │ ├── format-underline.svg │ ├── forward-step.svg │ ├── forward.svg │ ├── gift.svg │ ├── globe.svg │ ├── hand-stop.svg │ ├── hard-drive.svg │ ├── headphones.svg │ ├── heart.svg │ ├── home.svg │ ├── hot.svg │ ├── hour-glass.svg │ ├── inbox-check.svg │ ├── inbox-download.svg │ ├── inbox-full.svg │ ├── inbox.svg │ ├── indent-decrease.svg │ ├── indent-increase.svg │ ├── information-outline.svg │ ├── information-solid.svg │ ├── key.svg │ ├── keyboard.svg │ ├── layers.svg │ ├── library.svg │ ├── light-bulb.svg │ ├── link.svg │ ├── list-add.svg │ ├── list-bullet.svg │ ├── list.svg │ ├── load-balancer.svg │ ├── location-current.svg │ ├── location-food.svg │ ├── location-gas-station.svg │ ├── location-hotel.svg │ ├── location-marina.svg │ ├── location-park.svg │ ├── location-restroom.svg │ ├── location-shopping.svg │ ├── location.svg │ ├── lock-closed.svg │ ├── lock-open.svg │ ├── map.svg │ ├── menu.svg │ ├── mic.svg │ ├── minus-outline.svg │ ├── minus-solid.svg │ ├── mobile-devices.svg │ ├── mood-happy-outline.svg │ ├── mood-happy-solid.svg │ ├── mood-neutral-outline.svg │ ├── mood-neutral-solid.svg │ ├── mood-sad-outline.svg │ ├── mood-sad-solid.svg │ ├── mouse.svg │ ├── music-album.svg │ ├── music-artist.svg │ ├── music-notes.svg │ ├── music-playlist.svg │ ├── navigation-more.svg │ ├── network.svg │ ├── news-paper.svg │ ├── notification.svg │ ├── notifications-outline.svg │ ├── notifications.svg │ ├── paste.svg │ ├── pause-outline.svg │ ├── pause-solid.svg │ ├── pause.svg │ ├── pen-tool.svg │ ├── phone.svg │ ├── photo.svg │ ├── php-elephant.svg │ ├── pin.svg │ ├── play-outline.svg │ ├── play.svg │ ├── playlist.svg │ ├── plugin.svg │ ├── portfolio.svg │ ├── printer.svg │ ├── pylon.svg │ ├── question.svg │ ├── queue.svg │ ├── radar copy 2.svg │ ├── radar.svg │ ├── radio.svg │ ├── refresh.svg │ ├── reload.svg │ ├── reply-all.svg │ ├── reply.svg │ ├── repost.svg │ ├── save-disk.svg │ ├── screen-full.svg │ ├── search.svg │ ├── send.svg │ ├── servers.svg │ ├── share-01.svg │ ├── share-alt.svg │ ├── share.svg │ ├── shield.svg │ ├── shopping-cart.svg │ ├── show-sidebar.svg │ ├── shuffle.svg │ ├── stand-by.svg │ ├── star-full.svg │ ├── station.svg │ ├── step-backward.svg │ ├── step-forward.svg │ ├── stethoscope.svg │ ├── store-front.svg │ ├── stroke-width.svg │ ├── subdirectory-left.svg │ ├── subdirectory-right.svg │ ├── swap.svg │ ├── tablet.svg │ ├── tag.svg │ ├── target.svg │ ├── text-box.svg │ ├── text-decoration.svg │ ├── thermometer.svg │ ├── thumbs-down.svg │ ├── thumbs-up.svg │ ├── ticket.svg │ ├── time.svg │ ├── timer.svg │ ├── tools copy.svg │ ├── translate.svg │ ├── trash.svg │ ├── travel-bus.svg │ ├── travel-car.svg │ ├── travel-case.svg │ ├── travel-taxi-cab.svg │ ├── travel-train.svg │ ├── travel-walk.svg │ ├── travel.svg │ ├── trophy.svg │ ├── tuning.svg │ ├── upload.svg │ ├── usb.svg │ ├── user-add.svg │ ├── user-group.svg │ ├── user-solid-circle.svg │ ├── user-solid-square.svg │ ├── user.svg │ ├── vector.svg │ ├── video-camera.svg │ ├── view-carousel.svg │ ├── view-column.svg │ ├── view-hide.svg │ ├── view-list.svg │ ├── view-show.svg │ ├── view-tile.svg │ ├── volume-down.svg │ ├── volume-mute.svg │ ├── volume-off.svg │ ├── volume-up.svg │ ├── wallet.svg │ ├── watch.svg │ ├── window-new.svg │ ├── window-open.svg │ ├── window.svg │ ├── wrench.svg │ ├── yin-yang.svg │ ├── zoom-in.svg │ └── zoom-out.svg ├── workers-site ├── .cargo-ok ├── .gitignore ├── package-lock.json ├── package.json ├── src │ ├── api.ts │ ├── auth.ts │ ├── channel.ts │ ├── index.ts │ └── kvstore.ts ├── tsconfig.json └── webpack.config.js └── wrangler.template.toml /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .cache 4 | .env 5 | wrangler.toml -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to UpRadio 2 | 3 | This project is very much in its infancy, so there's not much in the way of guidelines here. But contributions are welcome. 4 | 5 | ### How to get started 6 | 7 | Check the project [Issues](https://github.com/iangregson/upradio/issues) and [Kanban](https://github.com/users/iangregson/projects/1) and pick something to work on. Say hello in the [matrix chat](https://matrix.to/#/!tabwwSGkCTKBdIJgVw:matrix.org?via=matrix.org) and talk about what you're working on. 8 | 9 | ### Security vulnerabilities 10 | 11 | If you find a vulnerability please email ian at gregson dot me and don't file an issue right away. 12 | 13 | ### Bugs 14 | 15 | File bugs in the [Issues](https://github.com/iangregson/upradio/issues) area. Include the following information: 16 | 17 | * Browser (including version number) 18 | * Device (including make, model and OS) 19 | * What were you doing? 20 | * What did you expect? 21 | * What actually happened? 22 | 23 | Screenshots are good. Videos are better. 24 | 25 | ### Can I use React or ? 26 | 27 | Not right now. TypeScript, HTML and CSS are the tools we have for the moment. One day that might be different, but that's where I'm at right now. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 uprad.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install build deploy 2 | .PHONY: all 3 | 4 | clean: 5 | rm -rf dist/ 6 | 7 | install: 8 | npm --prefix ./ui install 9 | npm --prefix ./workers-site install 10 | 11 | dev-build: 12 | npm --prefix ./ui run build:dev 13 | 14 | dev-serve: 15 | wrangler dev --ip=0.0.0.0 16 | 17 | dev-deploy: 18 | wrangler publish --env=staging 19 | 20 | build: 21 | npm --prefix ./ui run build 22 | 23 | deploy: 24 | wrangler publish 25 | 26 | start: 27 | npm --prefix ./ui start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upradio 2 | 3 | ![UpRadio Icon](ui/images/icons/icon-144x144.png) 4 | 5 | [uprad.io](https://uprad.io) 6 | 7 | UpRadio is a web application that allows you to broadcast or listen to live audio streams across the internet. The audio is streamed peer-to-peer using WebRTC (with big thanks to [PeerJS](https://peerjs.com/)). 8 | 9 | The vision for the future of this project is much larger... but I'm still working on distilling that down to something I can write in a couple of paragraphs. 10 | 11 | ### Resources 12 | 13 | * [FAQ / Help](https://docs.google.com/document/d/1O_khZIHnonInaRL7oNV4jsLyZkSCR9aySwuQDOdMyns) 14 | * [Open Source Software](https://docs.google.com/document/d/1jesuw5drKeFhh6MQBQDzGBP2s5cWRhAt8emQq77Byuc) 15 | * [Feedback form](https://forms.gle/cYhSipMHRhbq4BUJA) 16 | 17 | ### Want to contribute? 18 | 19 | It's very early days here, but contributions are welcome. I could use the help! 20 | 21 | Check the [contributing guidelines](CONTRIBUTING.md) and say hello in the [matrix chat](https://matrix.to/#/!tabwwSGkCTKBdIJgVw:matrix.org?via=matrix.org). 22 | 23 | ### Want to run your own? 24 | 25 | You'll need a [Cloudflare Workers](https://workers.cloudflare.com/) account, the [wrangler cli](https://developers.cloudflare.com/workers/tooling/wrangler) and somewhere that's running the [PeerJS Server](https://github.com/peers/peerjs-server) (I'm using Heroku which was really easy). 26 | 27 | Next, you need to create a `.env` file in the /ui directory. It should contain 4 key value pairs: 28 | 29 | ``` 30 | PEER_PATH=/peer-server 31 | PEER_SERVER=some-peerjs-server.somewhere.com 32 | PEER_KEY=a-secret-key-that-the-peer-server-uses 33 | MAX_CONNECTIONS=5 34 | ``` 35 | 36 | * `PEER_PATH` is configuration variable on the PeerJS Server that instructs it to serve on the given base path 37 | * `PEER_SERVER` is the DNS or IP of your PeerJS signalling server 38 | * `PEER_KEY` is the secret key used to connect to the PeerJS signalling server 39 | * `MAX_CONNECTIONS` is the number of connections any node in the network should accept 40 | 41 | Next you need to copy `wrangler.template.toml` to `wrangler.toml` and add your Cloudflare Workers account details. 42 | 43 | Lastly, you can use the Makefile to build and deploy 44 | 45 | ``` 46 | make install 47 | make build 48 | make deploy 49 | ``` 50 | 51 | With any luck, that'll work. 52 | 53 | # Big :heart: 54 | 55 | * [Scots Whay Hae!](https://scotswhayhae.com/) for actually using the thing 56 | * [PeerJS](https://peerjs.com/) 57 | * [Crypto-js](https://www.npmjs.com/package/crypto-js) 58 | * [Zondicons](http://www.zondicons.com/) 59 | * [Hero Patterns](https://www.heropatterns.com/) 60 | * [identicon.js](https://github.com/stewartlord/identicon.js) -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | TODO 2 | ---- 3 | 4 | ### v0.1.0 5 | 6 | - [X] Reinstate state manager support; load peer ID / state from session storage 7 | - [X] Get it working on iangregson.workers.dev 8 | - [X] Add a waveform visulizer 9 | - [X] Add a frequency bar visualizer 10 | - [X] Add a noise stream for connecting in relay mode (so we don't have to send voice just to listen) 11 | - [X] Connect to own PeerJS server 12 | - [X] Add a level meter 13 | - [X] Make it work on mobile (ios safari) 14 | - [X] Implement max connections and handoff 15 | - [X] Remember input device in session store 16 | - [X] Put station name / description / image into session storage 17 | - [X] Add station name verification via CloudFlare KV 18 | - [X] Add station name / description / image component 19 | - [X] Add a status component + event bus 20 | - [X] Add resolve station names via CloudFlare KV 21 | - [X] Add resolve station names via CloudFlare KV from URL path 22 | - [X] Machine generated avatar 23 | - [X] Add status icon to status component 24 | - [X] Top right menu icon 25 | - [X] Top right menu: connect to different channel / start broadcast 26 | - [X] Sanitize inputs 27 | - [X] Channel info component + store in localhost 28 | - [X] Tailwind CSS styling, colors, fonts, etc 29 | - [X] Copy URL component 30 | - [X] Service worker (workbox / precache) 31 | - [X] Rework directory structure 32 | - [X] Build without parceljs 33 | - [X] Remove tailwind unused css 34 | - [X] Webpack dev server 35 | - [X] Lighthouse tests 36 | - [X] Ensure aria-labels 37 | - [X] Ensure unique ids 38 | - [X] Ensure good button names 39 | - [X] Ensure fast performance 40 | - [X] Don't log to the console when in prod 41 | - [X] Esnure correct PWA resources in place including service worker 42 | - [X] Fix channel ID not registering with cloudflare 43 | - [X] Set up local wrangler for better iteration on LISTEN experience 44 | - [X] Send channel info / status over RPC 45 | - [X] LISTEN: autoconnect 46 | - [X] LISTEN: channel info 47 | - [X] LISTEN: better interaction on play / pause controls 48 | - [X] LISTEN: ON_AIR indicator + level meter 49 | - [X] Bug fix: first entry of channel name doesn't take (need an onchange handler) 50 | - [X] [BUG][LISTEN] Can't connect in mobile browser 51 | - [X] [BUG][LISTEN] Refresh to reconnect doesn't work properly 52 | - [X] [BUG] Duplicate login and resolve calls 53 | - [X] Success indication on "Copy URL" button 54 | - [X] Disabled styles on buttons 55 | - [X] [BUG] Not keeping same session token between requests 56 | - [X] [LISTEN] When we get disconnected, hide the pause button (and show the disabled play button) 57 | - [X] Add chat URL 58 | - [X] Don't set the chat URL if it's null 59 | - [X] Button outlines 60 | - [X] Improve header menu area 61 | - [X] FAQ, Help, About, OSS, Privacy notice, Contact Form, etc 62 | - [X] Make ready for open source: github prep, license and contributor files, remove keys, etc 63 | - [ ] Demo videos into the FAQ doc 64 | - [ ] Fix PWA icons to have white, not transparent, backgrounds 65 | 66 | ### ### v1.0.0 67 | 68 | - [ ] Add recorder feature 69 | - [ ] Add listener count widget 70 | - [ ] Play file component with level control 71 | - [ ] Audio output chooser 72 | - [ ] Lobby -------------------------------------------------------------------------------- /ui/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/favicon.ico -------------------------------------------------------------------------------- /ui/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /ui/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /ui/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /ui/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /ui/images/icons/icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-180x180.png -------------------------------------------------------------------------------- /ui/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /ui/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-384x384.png -------------------------------------------------------------------------------- /ui/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /ui/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /ui/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UpRadio 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |

25 | 26 | UpRadio 27 |

28 |

Universal Public Radio

29 |
p2p audio broadcasting
30 |
31 |
32 | 35 |
36 |
37 | 38 |
39 | 40 | 57 | 58 | 94 | 95 | 99 | 100 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /ui/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Universal Public Radio", 3 | "short_name": "UpRadio", 4 | "lang": "en-US", 5 | "theme_color": "#000000", 6 | "background_color": "#000000", 7 | "display": "standalone", 8 | "orientation": "portrait", 9 | "description": "p2p audio broadcasting", 10 | "start_url": "/", 11 | "serviceworker": { 12 | "src": "./sw.js", 13 | "scope": "/", 14 | "type": "", 15 | "update_via_cache": "none" 16 | }, 17 | "icons": [ 18 | { 19 | "src": "images/icons/icon-72x72.png", 20 | "sizes": "72x72", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "images/icons/icon-96x96.png", 25 | "sizes": "96x96", 26 | "type": "image/png" 27 | }, 28 | { 29 | "src": "images/icons/icon-128x128.png", 30 | "sizes": "128x128", 31 | "type": "image/png" 32 | }, 33 | { 34 | "src": "images/icons/icon-144x144.png", 35 | "sizes": "144x144", 36 | "type": "image/png" 37 | }, 38 | { 39 | "src": "images/icons/icon-152x152.png", 40 | "sizes": "152x152", 41 | "type": "image/png" 42 | }, 43 | { 44 | "src": "images/icons/icon-180x180.png", 45 | "sizes": "180x180", 46 | "type": "image/png" 47 | }, 48 | { 49 | "src": "images/icons/apple-touch-icon.png", 50 | "sizes": "180x180", 51 | "type": "image/png" 52 | }, 53 | { 54 | "src": "images/icons/icon-192x192.png", 55 | "sizes": "192x192", 56 | "type": "image/png" 57 | }, 58 | { 59 | "src": "images/icons/icon-384x384.png", 60 | "sizes": "384x384", 61 | "type": "image/png" 62 | }, 63 | { 64 | "src": "images/icons/icon-512x512.png", 65 | "sizes": "512x512", 66 | "type": "image/png" 67 | } 68 | ], 69 | "splash_pages": null 70 | } -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "upradio_ui", 4 | "version": "0.1.0", 5 | "description": "Universal Public Radio | p2p audio broadcasting", 6 | "main": "src/main.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "NODE_ENV=production webpack --config webpack.prod.js", 10 | "build:dev": "webpack --config webpack.dev.js", 11 | "start": "webpack-dev-server --config webpack.dev.js" 12 | }, 13 | "keywords": [ 14 | "p2p", 15 | "audio", 16 | "peerjs", 17 | "cloudflare workers" 18 | ], 19 | "author": "Ian Gregson", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "@types/crypto-js": "^3.1.46", 23 | "@types/uuid": "^7.0.2", 24 | "awesome-typescript-loader": "^5.2.1", 25 | "clean-webpack-plugin": "^3.0.0", 26 | "copy-webpack-plugin": "^12.0.2", 27 | "css-loader": "^7.1.2", 28 | "dotenv": "^8.2.0", 29 | "html-webpack-plugin": "^4.3.0", 30 | "mini-css-extract-plugin": "^0.9.0", 31 | "postcss-loader": "^8.1.1", 32 | "raw-loader": "^4.0.1", 33 | "tailwindcss": "^4.0.6", 34 | "typescript": "^3.8.3", 35 | "webpack": "^5.94.0", 36 | "webpack-cli": "^3.3.11", 37 | "webpack-dev-server": "^4.11.1", 38 | "workbox-webpack-plugin": "^7.1.0" 39 | }, 40 | "browserslist": [ 41 | "last 1 Chrome versions", 42 | "last 1 Firefox versions" 43 | ], 44 | "dependencies": { 45 | "crypto-js": "^4.2.0", 46 | "eventemitter3": "^4.0.4", 47 | "idb-keyval": "^3.2.0", 48 | "identicon.js": "^2.3.3", 49 | "peerjs": "^1.3.1", 50 | "uuid": "^7.0.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('tailwindcss') 4 | ] 5 | }; 6 | -------------------------------------------------------------------------------- /ui/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / -------------------------------------------------------------------------------- /ui/src/UpRadioApi.ts: -------------------------------------------------------------------------------- 1 | import { UpRadioPeerId } from "./UpRadioPeer/UpRadioPeer"; 2 | import Base64 from 'crypto-js/enc-base64'; 3 | import Utf8 from 'crypto-js/enc-utf8'; 4 | 5 | export class UpRadioApiError extends Error { 6 | status: number 7 | message: string; 8 | 9 | constructor(status: number, message: string) { 10 | super(message); 11 | this.message = message; 12 | this.status = status; 13 | } 14 | 15 | toResponse(): Response { 16 | return new Response(this.message, { status: this.status }); 17 | } 18 | } 19 | 20 | export type UpRadioChannelName = string; 21 | export type UpRadioApiSessionToken = string; 22 | 23 | export const API_KEY_HEADER_NAME = 'X-UpRadio-Api-Token' 24 | 25 | export interface IUpRadioApiService { 26 | login(peerId: UpRadioPeerId): Promise; 27 | channelVerify(token: UpRadioApiSessionToken, peerId: UpRadioPeerId, channelName: UpRadioChannelName): Promise; 28 | channelResolve(token: UpRadioApiSessionToken, peerId: UpRadioPeerId, channelName: UpRadioChannelName): Promise; 29 | heartbeat(token: UpRadioApiSessionToken, channelName?: UpRadioChannelName): Promise; 30 | } 31 | 32 | export class UpRadioApiService { 33 | static async login(peerId: UpRadioPeerId): Promise { 34 | const words = Utf8.parse(`${Date.now()}:${process.env.PEER_KEY}:${peerId}`); 35 | const token = Base64.stringify(words); 36 | return fetch('/api/login', { 37 | method: 'POST', 38 | body: JSON.stringify({ token }), 39 | headers: { 40 | 'Accept': 'text/html', 41 | 'Content-Type': 'application/json' 42 | } 43 | }).then((r: Response) => { 44 | if (!r.ok) { 45 | throw new UpRadioApiError(r.status, r.statusText); 46 | } 47 | return r.text(); 48 | }) 49 | } 50 | static async channelVerify(token: UpRadioApiSessionToken, channelName: UpRadioChannelName): Promise { 51 | return fetch('/api/channel/verify', { 52 | method: 'PUT', 53 | body: JSON.stringify({ channelName }), 54 | headers: { 55 | 'Accept': 'text/html', 56 | 'Content-Type': 'application/json', 57 | [API_KEY_HEADER_NAME]: token 58 | } 59 | }); 60 | } 61 | static async channelResolve(token: UpRadioApiSessionToken, channelName: UpRadioChannelName): Promise { 62 | return fetch('/api/channel/resolve', { 63 | method: 'PUT', 64 | body: JSON.stringify({ channelName }), 65 | headers: { 66 | 'Accept': 'text/html', 67 | 'Content-Type': 'application/json', 68 | [API_KEY_HEADER_NAME]: token 69 | } 70 | }); 71 | } 72 | static async heartbeat(token: UpRadioApiSessionToken, channelName?: UpRadioChannelName): Promise { 73 | return fetch('/api/heartbeat', { 74 | method: 'PUT', 75 | body: JSON.stringify({ channelName }), 76 | headers: { 77 | 'Accept': 'text/html', 78 | 'Content-Type': 'application/json', 79 | [API_KEY_HEADER_NAME]: token 80 | } 81 | }); 82 | } 83 | } 84 | 85 | export class UpRadioApi { 86 | public peerId: UpRadioPeerId; 87 | public token: UpRadioApiSessionToken | null | undefined; 88 | 89 | constructor(peerId: UpRadioPeerId) { 90 | this.peerId = peerId; 91 | } 92 | 93 | public async init(sessionToken?: UpRadioApiSessionToken): Promise { 94 | // Get a valid token with exponential backoff 95 | this.token = sessionToken; 96 | 97 | let delaySeconds = 0; 98 | let retries = 0; 99 | const MAX_RETRIES = 3; 100 | 101 | const backoff = () => new Promise((resolve) => { 102 | setTimeout(resolve, delaySeconds * 1000); 103 | }); 104 | 105 | while (!this.token) { 106 | await backoff(); 107 | 108 | await this.login(this.peerId).catch(err => window.logger.error(err)); 109 | 110 | if (!this.token) { 111 | delaySeconds += delaySeconds === 0 ? 1 : delaySeconds; 112 | retries++; 113 | } 114 | 115 | if (retries > MAX_RETRIES) break; 116 | } 117 | 118 | if (!this.token) { 119 | throw new UpRadioApiError(401, 'Could not start a session. Please check your connection.'); 120 | } 121 | } 122 | public async login(peerId: UpRadioPeerId): Promise { 123 | this.token = await UpRadioApiService.login(peerId); 124 | return this.token; 125 | } 126 | public async heartbeat(channelName?: UpRadioChannelName): Promise { 127 | if (!this.token) await this.login(this.peerId); 128 | 129 | let response = await UpRadioApiService.heartbeat(this.token, channelName); 130 | 131 | if (!response.ok && response.status === 401) { 132 | await this.login(this.peerId); 133 | response = await UpRadioApiService.heartbeat(this.token); 134 | } 135 | 136 | if (!response.ok && response.status === 409) { 137 | throw new UpRadioApiError(409, 'Channel name conflict'); 138 | } 139 | } 140 | public async channelVerify(channelName: UpRadioChannelName): Promise { 141 | if (!this.token) await this.login(this.peerId); 142 | 143 | let response = await UpRadioApiService.channelVerify(this.token, channelName); 144 | 145 | if (!response.ok && response.status === 401) { 146 | await this.login(this.peerId); 147 | response = await UpRadioApiService.channelVerify(this.token, channelName); 148 | } 149 | 150 | if (!response.ok && response.status === 409) { 151 | throw new UpRadioApiError(409, 'Channel name conflict'); 152 | } 153 | 154 | if (!response.ok) { 155 | throw new UpRadioApiError(response.status, response.statusText); 156 | } 157 | } 158 | public async channelResolve(channelName: UpRadioChannelName): Promise { 159 | if (!this.token) await this.login(this.peerId); 160 | 161 | let response = await UpRadioApiService.channelResolve(this.token, channelName); 162 | 163 | if (!response.ok && response.status === 404) { 164 | throw new UpRadioApiError(404, 'Channel not found'); 165 | } 166 | 167 | return response.text(); 168 | } 169 | } -------------------------------------------------------------------------------- /ui/src/UpRadioPeer/UpRadioPeerRpc.ts: -------------------------------------------------------------------------------- 1 | import { UpRadioPeerId, UpRadioPeer } from './UpRadioPeer'; 2 | import { DataConnection } from 'peerjs'; 3 | import { v4 as uuid } from 'uuid'; 4 | import { UpRadioChannelInfo } from '@upradio-client/components/Channel/ChannelInfo.component'; 5 | import { UpRadioOnAirStatus } from '@upradio-client/components/Channel/ChannelEdit.component'; 6 | 7 | export enum UpRadioPeerRPCMessageType { 8 | ack = 'ack', 9 | nextPeer = 'nextPeer', 10 | setChannelInfo = 'setChannelInfo', 11 | setChannelOnAirStatus = 'setChannelOnAirStatus', 12 | chat = 'chat' 13 | } 14 | 15 | export interface IUpRadioPeerRPCMessage { 16 | id: string, 17 | peerId: UpRadioPeerId, 18 | type: UpRadioPeerRPCMessageType, 19 | params: any[] 20 | } 21 | 22 | export class UpRadioPeerRpcMsg implements IUpRadioPeerRPCMessage { 23 | public id: string; 24 | public peerId: UpRadioPeerId; 25 | public type: UpRadioPeerRPCMessageType; 26 | public params: any[]; 27 | 28 | constructor(peerId: UpRadioPeerId) { 29 | this.id = uuid(); 30 | this.peerId = peerId; 31 | this.type = UpRadioPeerRPCMessageType.ack; 32 | this.params = []; 33 | } 34 | 35 | toJSON(): IUpRadioPeerRPCMessage { 36 | return { 37 | id: this.id, 38 | peerId: this.peerId, 39 | type: this.type, 40 | params: this.params 41 | }; 42 | } 43 | 44 | ack(msg?: IUpRadioPeerRPCMessage): IUpRadioPeerRPCMessage { 45 | this.type = UpRadioPeerRPCMessageType.ack; 46 | if (msg) { 47 | this.params = [msg.id]; 48 | } 49 | return this; 50 | } 51 | 52 | static isAck(msg: IUpRadioPeerRPCMessage) { 53 | return msg.type === UpRadioPeerRPCMessageType.ack; 54 | } 55 | 56 | nextPeer(nextPeerId: UpRadioPeerId): IUpRadioPeerRPCMessage { 57 | this.type = UpRadioPeerRPCMessageType.nextPeer; 58 | this.params = [nextPeerId]; 59 | return this; 60 | } 61 | 62 | setChannelInfo(channelInfo: UpRadioChannelInfo): IUpRadioPeerRPCMessage { 63 | this.type = UpRadioPeerRPCMessageType.setChannelInfo; 64 | this.params = [channelInfo]; 65 | return this; 66 | } 67 | 68 | setChannelOnAirStatus(status: UpRadioOnAirStatus): IUpRadioPeerRPCMessage { 69 | this.type = UpRadioPeerRPCMessageType.setChannelOnAirStatus; 70 | this.params = [status]; 71 | return this; 72 | } 73 | } 74 | 75 | export class UpRadioPeerRpcService { 76 | static parseMessage(data: string): IUpRadioPeerRPCMessage | null { 77 | if (!data || !data.length) return null; 78 | try { 79 | const parsed: IUpRadioPeerRPCMessage = JSON.parse(data); 80 | if (parsed.id && parsed.type) { 81 | return parsed; 82 | } else { 83 | return null; 84 | } 85 | } catch (_) { 86 | return null; 87 | } 88 | } 89 | static handleMessage(peer: UpRadioPeer, msg: IUpRadioPeerRPCMessage): void { 90 | window.logger.debug(`RPC::receive::${msg.type}`, msg); 91 | peer.events.emit(msg.type, msg.params); 92 | } 93 | static nextPeer(peer: UpRadioPeer, connection: DataConnection, nextPeerId: UpRadioPeerId): void { 94 | const msg = new UpRadioPeerRpcMsg(peer.id).nextPeer(nextPeerId); 95 | connection.send(JSON.stringify(msg)); 96 | } 97 | static setChannelInfo(peer: UpRadioPeer, connection: DataConnection, channelInfo: UpRadioChannelInfo): void { 98 | const msg = new UpRadioPeerRpcMsg(peer.id).setChannelInfo(channelInfo); 99 | window.logger.debug('RPC::send::setChannelInfo', channelInfo); 100 | connection.send(JSON.stringify(msg)); 101 | } 102 | static setChannelOnAirStatus(peer: UpRadioPeer, connection: DataConnection, status: UpRadioOnAirStatus): void { 103 | const msg = new UpRadioPeerRpcMsg(peer.id).setChannelOnAirStatus(status); 104 | window.logger.debug('RPC::send::setChannelOnAirStatus', status); 105 | connection.send(JSON.stringify(msg)); 106 | } 107 | } -------------------------------------------------------------------------------- /ui/src/UpRadioPeer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UpRadioPeer'; -------------------------------------------------------------------------------- /ui/src/app.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /ui/src/components/Channel/ChannelEdit.component.html: -------------------------------------------------------------------------------- 1 |
2 | 25 | -------------------------------------------------------------------------------- /ui/src/components/Channel/ChannelEdit.component.ts: -------------------------------------------------------------------------------- 1 | import template from './ChannelEdit.component.html'; 2 | import { Component } from ".."; 3 | import { UpRadioApi } from '@upradio-client/UpRadioApi'; 4 | import { UpRadioApiError } from '../../UpRadioApi'; 5 | import { ChannelInfo, ChannelInfoMode } from './ChannelInfo.component'; 6 | import { UpRadioPeer } from '@upradio-client/UpRadioPeer'; 7 | 8 | export type UpRadioChannelId = string; 9 | export type UpRadioChannelName = string; 10 | export enum UpRadioChannelStatus { 11 | valid = 'VALID', 12 | invalid = 'INVALID' 13 | } 14 | 15 | export enum UpRadioOnAirStatus { 16 | ON_AIR = 'ON_AIR', 17 | OFF_AIR = 'OFF_AIR' 18 | } 19 | 20 | export class ChannelEditComponent extends Component { 21 | public parent: HTMLElement; 22 | public api: UpRadioApi; 23 | public peer: UpRadioPeer; 24 | public channelInfo: ChannelInfo; 25 | 26 | private _onAirStatus: UpRadioOnAirStatus = UpRadioOnAirStatus.OFF_AIR; 27 | private _status: UpRadioChannelStatus; 28 | private nameInput: HTMLInputElement; 29 | private chatInput: HTMLInputElement; 30 | private descriptionInput: HTMLTextAreaElement; 31 | private verifyBtn: HTMLButtonElement; 32 | private copyUrlBtn: HTMLButtonElement; 33 | private channelEditBox: HTMLDivElement; 34 | private channelInfoBox: HTMLDivElement; 35 | private channelImageUpload: HTMLInputElement; 36 | 37 | constructor(parent: HTMLElement, api: UpRadioApi, peer: UpRadioPeer) { 38 | super(parent, 'ChannelEditComponent', template); 39 | this.api = api; 40 | this.peer = peer; 41 | 42 | this.parent.classList.add('flex'); 43 | this.parent.classList.add('flex-col'); 44 | this.parent.classList.add('flex-grow'); 45 | this.container.classList.add('flex'); 46 | this.container.classList.add('flex-col'); 47 | this.container.classList.add('flex-grow'); 48 | 49 | this.nameInput = this.container.querySelector('input#channelName'); 50 | this.nameInput.onchange = () => { 51 | this.name = this.nameInput.value; 52 | } 53 | this.descriptionInput = this.container.querySelector('textarea#channelDescription'); 54 | this.chatInput = this.container.querySelector('input#channelChat'); 55 | 56 | this.verifyBtn = this.container.querySelector('button#channelVerify'); 57 | this.verifyBtn.onclick = this.verifyChannelName.bind(this); 58 | this.copyUrlBtn = this.container.querySelector('button#copyUrl'); 59 | this.copyUrlBtn.onclick = this.copyUrl.bind(this); 60 | this.channelEditBox = this.container.querySelector('div#channelEditor'); 61 | this.channelInfoBox = this.container.querySelector('div#channelInfo'); 62 | this.channelInfo = new ChannelInfo(this.channelInfoBox, 'UpRadioChannelInfo-Listen'); 63 | this.channelInfo.init({ peerId: this.peer.id }); 64 | this.channelInfo.mode = ChannelInfoMode.READ_WRITE; 65 | this.channelInfo.editBtn.onclick = () => this.channelEditBox.classList.toggle('hidden'); 66 | this.channelImageUpload = this.container.querySelector('input#channelImageUpload'); 67 | this.channelImageUpload.onchange = () => { 68 | const reader = new FileReader(); 69 | const self = this; 70 | reader.addEventListener("load", function () { 71 | const result = this.result.toString().split(',').pop(); 72 | self.image = result; 73 | }, false); 74 | 75 | reader.readAsDataURL(this.channelImageUpload.files[0]); 76 | }; 77 | } 78 | 79 | public static htmlEscape(s: string): string { 80 | if (!s) return ''; 81 | return s.trim() 82 | .replace('&', '&') 83 | .replace('<', '<') 84 | .replace('>', '>'); 85 | } 86 | 87 | public static toUrlSlug(s: string): string { 88 | if (!s) return ''; 89 | return encodeURIComponent(s.trim() 90 | .replace(/\s/g, '') 91 | .replace(/[.!~*'()]/g, '') 92 | .toLowerCase()) 93 | 94 | } 95 | 96 | public async copyUrl() { 97 | const url = new URL(location.origin); 98 | url.pathname = '/' + this.channelId; 99 | await navigator.clipboard.writeText(url.toString()) 100 | .catch(err => { 101 | this.copyUrlBtn.classList.add('border-red-500'); 102 | window.logger.error(err) 103 | }); 104 | this.copyUrlBtn.classList.add('border-green-500'); 105 | setTimeout(() => { 106 | this.copyUrlBtn.classList.remove('border-green-500'); 107 | this.copyUrlBtn.classList.remove('border-red-500'); 108 | }, 3000); 109 | } 110 | 111 | public async verifyChannelName() { 112 | await this.api.channelVerify(this.channelId) 113 | .then(() => { 114 | this.channelStatus = UpRadioChannelStatus.valid; 115 | }) 116 | .catch((err: UpRadioApiError) => { 117 | this.channelStatus = UpRadioChannelStatus.invalid; 118 | }); 119 | } 120 | 121 | get onAirStatus(): UpRadioOnAirStatus { 122 | return this._onAirStatus; 123 | } 124 | set onAirStatus(status: UpRadioOnAirStatus) { 125 | this._onAirStatus = status; 126 | } 127 | 128 | get channelStatus(): UpRadioChannelStatus { 129 | return this._status; 130 | } 131 | set channelStatus(status: UpRadioChannelStatus) { 132 | this._status = status; 133 | switch (status) { 134 | case UpRadioChannelStatus.invalid: 135 | this.nameInput.classList.add('border-red-500'); 136 | this.verifyBtn.classList.add('border-red-500'); 137 | this.emit('status::message', { text: 'Channel name invalid.', level: 'error' }); 138 | break; 139 | case UpRadioChannelStatus.valid: 140 | this.nameInput.classList.add('border-green-500'); 141 | this.verifyBtn.classList.add('border-green-500'); 142 | this.emit('status::message', { text: 'Channel verified.', level: 'success' }); 143 | break; 144 | } 145 | } 146 | 147 | get chatUrl(): string { 148 | return encodeURI(this.chatInput.value); 149 | } 150 | set chatUrl(chatUrl: string) { 151 | if (!chatUrl || !chatUrl.length) return; 152 | this.chatInput.value = encodeURI(chatUrl); 153 | this.channelInfo.chatUrl = encodeURI(chatUrl); 154 | } 155 | 156 | get name(): UpRadioChannelName { 157 | return this.nameInput.value; 158 | } 159 | set name(name: string) { 160 | this.nameInput.value = name; 161 | this.channelInfo.name = ChannelEditComponent.htmlEscape(name); 162 | this.channelInfo.channelId = ChannelEditComponent.toUrlSlug(name); 163 | } 164 | 165 | get description(): string { 166 | return this.descriptionInput.value; 167 | } 168 | set description(description: string) { 169 | this.descriptionInput.textContent = description; 170 | this.channelInfo.description = ChannelEditComponent.htmlEscape(description); 171 | } 172 | 173 | get image(): string { 174 | return this.channelInfo.image; 175 | } 176 | set image(imageBase64: string) { 177 | if (!imageBase64) return; 178 | this.channelInfo.image = imageBase64; 179 | } 180 | 181 | get channelId(): string { 182 | return this.channelInfo.channelId; 183 | } 184 | set channelId(channelId: UpRadioChannelId) { 185 | this.channelInfo.channelId = ChannelEditComponent.toUrlSlug(channelId); 186 | } 187 | } -------------------------------------------------------------------------------- /ui/src/components/Channel/ChannelInfo.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/components/Connect/Connect.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 |
6 | 7 | 8 |
9 |
-------------------------------------------------------------------------------- /ui/src/components/Connect/Connect.component.ts: -------------------------------------------------------------------------------- 1 | import template from './Connect.component.html'; 2 | import { Component } from '..'; 3 | 4 | export enum EConnectComponentState { 5 | connected = 'connected', 6 | disconnected = 'disconnected' 7 | } 8 | 9 | export default class ConnectComponent extends Component { 10 | public input: HTMLInputElement; 11 | public connectBtn: HTMLButtonElement; 12 | 13 | constructor(parent: HTMLElement) { 14 | super(parent, 'Connect', template); 15 | 16 | this.input = this.container.querySelector('#ConnectInput'); 17 | this.connectBtn = this.container.querySelector('button#ConnectButton'); 18 | this.connectBtn.onclick = () => { 19 | location.href = location.origin + '/' + this.input.value; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /ui/src/components/Connect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Connect.component'; -------------------------------------------------------------------------------- /ui/src/components/Identicon/Identicon.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '..'; 2 | import { UpRadioPeerId } from '@upradio-client/UpRadioPeer'; 3 | const Ideniticon = require('identicon.js'); 4 | 5 | const AVATAR_SIZE = 128; 6 | 7 | export class IdenticonComponent extends Component { 8 | private identiconContainer: HTMLDivElement; 9 | public identicon: String; 10 | 11 | constructor(container: HTMLElement) { 12 | super(container, 'Connect', '
'); 13 | this.identiconContainer = container.querySelector('div#UpRadioIdenticon'); 14 | } 15 | draw(id: UpRadioPeerId) { 16 | const options = { size: AVATAR_SIZE, format: 'svg' }; 17 | this.identicon = new Ideniticon(id, options).toString(); 18 | this.identiconContainer.innerHTML = ``; 19 | } 20 | } -------------------------------------------------------------------------------- /ui/src/components/LevelMeter/LevelMeter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from ".."; 2 | 3 | export interface IFreqMeterOptions { 4 | width: number, 5 | height: number, 6 | fftSize: number 7 | } 8 | 9 | const DEFAULT_METER_OPTIONS: IFreqMeterOptions = { 10 | width: 200, 11 | height: 200, 12 | fftSize: 256 13 | } 14 | 15 | export class FreqMeter extends Component { 16 | public source: MediaStreamAudioSourceNode; 17 | public audioCtx: AudioContext; 18 | public analyser: AnalyserNode; 19 | public canvas: HTMLCanvasElement; 20 | private options: IFreqMeterOptions; 21 | private drawFrame: number; 22 | 23 | constructor(parent: HTMLElement, options: IFreqMeterOptions = DEFAULT_METER_OPTIONS) { 24 | super(parent, 'FreqMeter', ''); 25 | this.options = options; 26 | this.canvas = this.parent.querySelector('canvas#FreqMeterOutput'); 27 | } 28 | 29 | public init(stream: MediaStream): this { 30 | this.audioCtx = new UpRadioAudioService.AudioContext(); 31 | this.analyser = this.audioCtx.createAnalyser(); 32 | this.analyser.fftSize = this.options.fftSize; 33 | this.source = this.audioCtx.createMediaStreamSource(stream); 34 | this.source.connect(this.analyser); 35 | this.audioCtx.resume(); 36 | this.draw(); 37 | this.show(); 38 | return this; 39 | } 40 | 41 | public stop(): this { 42 | this.source = null; 43 | cancelAnimationFrame(this.drawFrame); 44 | this.drawFrame = null; 45 | const WIDTH = this.canvas.width; 46 | const HEIGHT = this.canvas.height; 47 | const canvasCtx = this.canvas.getContext('2d'); 48 | canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); 49 | return this; 50 | } 51 | 52 | public draw() { 53 | this.drawFrame = requestAnimationFrame(this.draw.bind(this)); 54 | const WIDTH = this.canvas.width; 55 | const HEIGHT = this.canvas.height; 56 | 57 | const canvasCtx = this.canvas.getContext('2d'); 58 | canvasCtx.clearRect(0, 0, WIDTH, HEIGHT); 59 | 60 | const bufferLength = this.analyser.frequencyBinCount; 61 | const dataArray = new Uint8Array(bufferLength); 62 | this.analyser.getByteFrequencyData(dataArray); 63 | 64 | var barWidth = (WIDTH / bufferLength) * 2.5; 65 | var barHeight; 66 | var x = 0; 67 | 68 | for(var i = 0; i < bufferLength; i++) { 69 | barHeight = dataArray[i]/2; 70 | 71 | // canvasCtx.fillStyle = 'rgb(' + (barHeight+100) + ',255,255)'; 72 | canvasCtx.fillStyle = 'rgb(255,255,255)'; 73 | canvasCtx.fillRect(x,HEIGHT-barHeight/2,barWidth,barHeight); 74 | 75 | x += barWidth + 1; 76 | } 77 | } 78 | } 79 | 80 | export class LevelMeter extends Component { 81 | public source: MediaStreamAudioSourceNode; 82 | public audioCtx: AudioContext; 83 | public analyser: ScriptProcessorNode; 84 | public meter: HTMLMeterElement; 85 | public value: number; 86 | private drawFrame: number; 87 | 88 | constructor(parent: HTMLElement) { 89 | super(parent, 'LevelMeter', ''); 90 | this.meter = this.parent.querySelector('meter#LevelMeterOutput'); 91 | this.meter.high = 0.25; 92 | this.meter.max = 1; 93 | this.meter.value = 0; 94 | this.value = 0.0; 95 | } 96 | 97 | public init(stream: MediaStream): this { 98 | this.audioCtx = new UpRadioAudioService.AudioContext(); 99 | this.source = this.audioCtx.createMediaStreamSource(stream); 100 | this.analyser = this.audioCtx.createScriptProcessor(2048, 1, 1); 101 | this.analyser.onaudioprocess = (event: AudioProcessingEvent) => { 102 | const input = event.inputBuffer.getChannelData(0); 103 | let i; 104 | let sum = 0.0; 105 | for (i = 0; i < input.length; ++i) { 106 | sum += input[i] * input[i]; 107 | } 108 | this.value = Math.sqrt(sum / input.length); 109 | }; 110 | this.source.connect(this.analyser); 111 | this.analyser.connect(this.audioCtx.destination); 112 | this.audioCtx.resume(); 113 | this.draw(); 114 | this.show(); 115 | return this; 116 | } 117 | 118 | public stop(): this { 119 | this.source = null; 120 | cancelAnimationFrame(this.drawFrame); 121 | this.drawFrame = null; 122 | this.hide(); 123 | return this; 124 | } 125 | 126 | public draw() { 127 | this.drawFrame = requestAnimationFrame(this.draw.bind(this)); 128 | this.meter.value = this.value; 129 | } 130 | } 131 | 132 | export class UpRadioAudioService { 133 | static get AudioContext() { 134 | const audioContextProvider = window.AudioContext || window.webkitAudioContext; 135 | return audioContextProvider; 136 | } 137 | static createToneGeneratorAndStream(tone: number = 440 /* value for middle A */): [OscillatorNode, MediaStreamAudioDestinationNode] { 138 | const audioCtx = new UpRadioAudioService.AudioContext(); 139 | audioCtx.resume(); 140 | 141 | const oscillator = audioCtx.createOscillator(); 142 | oscillator.type = 'sine'; 143 | oscillator.frequency.setValueAtTime(tone, audioCtx.currentTime); 144 | 145 | const outputStream = audioCtx.createMediaStreamDestination(); 146 | oscillator.connect(outputStream); 147 | 148 | return [oscillator, outputStream]; 149 | } 150 | } -------------------------------------------------------------------------------- /ui/src/components/LevelMeter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LevelMeter.component'; -------------------------------------------------------------------------------- /ui/src/components/ModeSwitch/ModeSwitch.component.html: -------------------------------------------------------------------------------- 1 | 7 |
8 | 9 |
-------------------------------------------------------------------------------- /ui/src/components/ModeSwitch/ModeSwitch.component.ts: -------------------------------------------------------------------------------- 1 | import template from './ModeSwitch.component.html'; 2 | import { Component } from '..'; 3 | 4 | export enum UpRadioMode { 5 | LISTEN = 'LISTEN', 6 | BROADCAST = 'BROADCAST' 7 | } 8 | 9 | export class ModeSwitchComponent extends Component { 10 | public broadcastInput: HTMLInputElement; 11 | public listenInput: HTMLInputElement; 12 | public broadcastBtnBox: HTMLDivElement; 13 | public broadcastBtn: HTMLButtonElement; 14 | 15 | constructor(container: HTMLElement) { 16 | super(container, 'ModeSwtich', template); 17 | 18 | this.broadcastInput = container.querySelector('input#UpRadioModeSwitch-Broadcast'); 19 | this.broadcastInput.onchange = () => { 20 | this.emit('MODE_SWITCH', { mode: this.value }); 21 | } 22 | 23 | this.listenInput = container.querySelector('input#UpRadioModeSwitch-Listen'); 24 | this.listenInput.onchange = () => { 25 | this.emit('MODE_SWITCH', { mode: this.value }); 26 | } 27 | 28 | this.broadcastBtnBox = container.querySelector('div#UpRadioModeSwitch-Btn'); 29 | this.broadcastBtn = container.querySelector('button#UpRadioModeSwitchBtn-Broadcast'); 30 | this.broadcastBtn.onclick = () => (location.href = location.origin); 31 | } 32 | 33 | get value(): UpRadioMode { 34 | if (this.broadcastInput.checked) { 35 | return UpRadioMode.BROADCAST; 36 | } 37 | return UpRadioMode.LISTEN; 38 | } 39 | set value(mode: UpRadioMode) { 40 | this.broadcastInput.checked = mode === UpRadioMode.BROADCAST; 41 | this.listenInput.checked = mode === UpRadioMode.LISTEN; 42 | if (mode === UpRadioMode.BROADCAST) { 43 | this.broadcastBtnBox.classList.add('hidden'); 44 | } 45 | this.emit('MODE_SWITCH', { mode }); 46 | } 47 | 48 | static createUpdateEvent(mode: UpRadioMode) { 49 | const updateEvent = new CustomEvent('UpRadio:MODE_SWITCH', { detail: mode, bubbles: true }); 50 | return updateEvent; 51 | } 52 | } -------------------------------------------------------------------------------- /ui/src/components/ModeSwitch/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModeSwitch.component'; -------------------------------------------------------------------------------- /ui/src/components/Status/Status.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /ui/src/components/Status/Status.component.ts: -------------------------------------------------------------------------------- 1 | import template from './Status.component.html'; 2 | import { Component } from ".."; 3 | import EventEmitter from 'eventemitter3'; 4 | 5 | export enum UpRadioStatusMsgLevel { 6 | debug = 'log', 7 | info = 'log', 8 | log = 'log', 9 | warn = 'warn', 10 | error = 'error', 11 | success = 'success' 12 | } 13 | 14 | export interface UpRadioStatusMsg { 15 | text: string 16 | level: UpRadioStatusMsgLevel, 17 | } 18 | 19 | const icons = { 20 | 'debug': ``, 21 | 'info': ``, 22 | 'log': ``, 23 | 'warn': ``, 24 | 'error': ``, 25 | 'success': `` 26 | } 27 | 28 | export class UpRadioStatusBar extends Component { 29 | public output: HTMLDivElement; 30 | public icon: HTMLDivElement; 31 | public events: EventEmitter; 32 | 33 | constructor(parent: HTMLElement, eventBus: EventEmitter) { 34 | super(parent, 'UpRadioStatus', template); 35 | this.icon = parent.querySelector('#UpRadioStatusOutput-icon'); 36 | this.output = parent.querySelector('#UpRadioStatusOutput-text'); 37 | this.output.innerText = ''; 38 | this.events = eventBus; 39 | this.events.on('status::message', this.displayMessage.bind(this)); 40 | } 41 | 42 | private displayMessage(msg: UpRadioStatusMsg) { 43 | 44 | this.icon.innerHTML = icons[msg.level]; 45 | this.output.innerText = msg.text; 46 | 47 | switch (msg.level) { 48 | case UpRadioStatusMsgLevel.debug: 49 | window.logger.debug(msg.text); 50 | break; 51 | case UpRadioStatusMsgLevel.info: 52 | window.logger.info(msg.text); 53 | break; 54 | case UpRadioStatusMsgLevel.log: 55 | window.logger.log(msg.text); 56 | break; 57 | case UpRadioStatusMsgLevel.warn: 58 | window.logger.warn(msg.text); 59 | break; 60 | case UpRadioStatusMsgLevel.success: 61 | window.logger.log(msg.text); 62 | break; 63 | case UpRadioStatusMsgLevel.error: 64 | window.logger.error(msg.text); 65 | break; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ui/src/components/Status/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Status.component'; -------------------------------------------------------------------------------- /ui/src/components/Streams/LocalStream.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 |
8 | 11 | 14 |
15 |
16 |
17 | OFF AIR 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /ui/src/components/Streams/LocalStream.component.ts: -------------------------------------------------------------------------------- 1 | import template from './LocalStream.component.html'; 2 | import { Component } from ".."; 3 | import { LevelMeter, FreqMeter } from '../LevelMeter'; 4 | import { UpRadioOnAirStatus } from '../Channel/ChannelEdit.component'; 5 | 6 | export interface IUpRadioStream { 7 | stream: MediaStream; 8 | start(stream?: MediaStream): Promise; 9 | stop(): Promise; 10 | } 11 | 12 | export class LocalStreamComponent extends Component implements IUpRadioStream { 13 | private devices: MediaDeviceInfo[]; 14 | private audioInputSelect: any; 15 | public freqMeter: FreqMeter; 16 | public broadcastBtn: HTMLButtonElement; 17 | public stopBroadcastingBtn: HTMLButtonElement; 18 | private statusPanel: HTMLDivElement; 19 | private broadcastStatusText: HTMLSpanElement; 20 | private broadcastStatus = UpRadioOnAirStatus.OFF_AIR; 21 | 22 | public stream: MediaStream; 23 | 24 | constructor(container: HTMLElement) { 25 | super(container, 'LocalStream', template); 26 | 27 | this.broadcastBtn = this.container.querySelector('button#BroadcastButton'); 28 | this.stopBroadcastingBtn = this.container.querySelector('button#StopBroadcastingButton'); 29 | this.broadcastStatusText = this.container.querySelector('span#broadcastStatus'); 30 | this.statusPanel = this.container.querySelector('div#broadcastStatusPanel'); 31 | this.freqMeter = new FreqMeter(this.statusPanel); 32 | this.audioInputSelect = container.querySelector('select#audioSource'); 33 | this.initDeviceList(); 34 | } 35 | 36 | async initDeviceList(): Promise { 37 | this.devices = await UpRadioStreamService.enumerateAudioDevices(); 38 | 39 | // Keep previous value but clear out existing options 40 | const selectedDeviceId = this.audioInputSelect.value; 41 | while (this.audioInputSelect.firstChild) { 42 | this.audioInputSelect.removeChild(this.audioInputSelect.firstChild); 43 | } 44 | 45 | for (let audioDevice of this.devices) { 46 | const option = document.createElement('option'); 47 | option.value = audioDevice.deviceId; 48 | option.text = audioDevice.label || `microphone ${this.audioInputSelect.length + 1}`; 49 | this.audioInputSelect.appendChild(option); 50 | } 51 | 52 | // Reinstate previous value if there was one 53 | const selectedValueinOptions = Array.prototype.slice.call(this.audioInputSelect.childNodes) 54 | .some(n => n.value === selectedDeviceId); 55 | if (selectedValueinOptions) { 56 | this.audioInputSelect.value = selectedDeviceId; 57 | } 58 | } 59 | 60 | public get onAirStatus(): UpRadioOnAirStatus { 61 | return this.broadcastStatus; 62 | } 63 | public set onAirStatus(status: UpRadioOnAirStatus) { 64 | this.broadcastStatus = status; 65 | switch (status) { 66 | case UpRadioOnAirStatus.ON_AIR: 67 | this.broadcastStatusText.classList.remove('text-red-500'); 68 | this.broadcastStatusText.classList.add('text-green-500'); 69 | this.broadcastStatusText.innerText = 'ON AIR'; 70 | break; 71 | case UpRadioOnAirStatus.OFF_AIR: 72 | this.broadcastStatusText.classList.add('text-red-500'); 73 | this.broadcastStatusText.classList.remove('text-green-500'); 74 | this.broadcastStatusText.innerText = 'OFF AIR'; 75 | break; 76 | } 77 | } 78 | 79 | public async stop(): Promise { 80 | if (!this.stream) return; 81 | this.stream.getTracks().forEach(track => { 82 | track.stop(); 83 | }); 84 | this.freqMeter.stop(); 85 | this.stopBroadcastingBtn.classList.add('hidden'); 86 | this.broadcastBtn.classList.remove('hidden'); 87 | this.onAirStatus = UpRadioOnAirStatus.OFF_AIR; 88 | } 89 | 90 | public get selectedDevice(): MediaDeviceInfo | undefined { 91 | if (!this.devices) return; 92 | return this.devices.find(d => d.deviceId === this.audioInputSelect.value); 93 | } 94 | 95 | public setSelectedDeviceId(id: string): void { 96 | this.audioInputSelect.value = id; 97 | } 98 | 99 | public async start(): Promise { 100 | if (this.stream) this.stop(); 101 | 102 | this.stream = await UpRadioStreamService.getAudioStream(this.selectedDevice); 103 | this.freqMeter.init(this.stream); 104 | this.stopBroadcastingBtn.classList.remove('hidden'); 105 | this.broadcastBtn.classList.add('hidden'); 106 | this.onAirStatus = UpRadioOnAirStatus.ON_AIR; 107 | 108 | // In-case of proper labels becoming available 109 | this.initDeviceList(); 110 | } 111 | } 112 | 113 | export class UpRadioStreamService { 114 | static async enumerateAudioDevices(w: Window = window): Promise { 115 | return w.navigator.mediaDevices.enumerateDevices() 116 | .then(ds => ds.filter(d => d.kind === 'audioinput')); 117 | } 118 | static async getAudioStream(audioDevice: MediaDeviceInfo, w: Window = window): Promise { 119 | const video = false; 120 | let audio; 121 | 122 | if (audioDevice && audioDevice.deviceId) { 123 | audio = { deviceId: { exact: audioDevice.deviceId } } 124 | } else { 125 | audio = true; 126 | } 127 | 128 | return w.navigator.mediaDevices.getUserMedia({ video, audio }); 129 | } 130 | } -------------------------------------------------------------------------------- /ui/src/components/Streams/RemoteStream.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 9 | 12 | 15 |
16 | 17 |
18 |
19 | OFF AIR 20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /ui/src/components/Streams/RemoteStream.component.ts: -------------------------------------------------------------------------------- 1 | import template from './RemoteStream.component.html'; 2 | import { Component } from ".."; 3 | import { IUpRadioStream } from "./LocalStream.component"; 4 | import { UpRadioAudioService, FreqMeter } from '../LevelMeter'; 5 | import { UpRadioOnAirStatus } from '../Channel/ChannelEdit.component'; 6 | 7 | export class RemoteStreamComponent extends Component implements IUpRadioStream { 8 | public audioOutput: HTMLAudioElement; 9 | public connectBtn: HTMLButtonElement; 10 | public playBtn: HTMLButtonElement; 11 | public stopBtn: HTMLButtonElement; 12 | 13 | public stream: MediaStream; 14 | private dialToneGenerator: OscillatorNode; 15 | public dialTone: MediaStream; 16 | 17 | public freqMeter: FreqMeter; 18 | private statusPanel: HTMLDivElement; 19 | private statusText: HTMLSpanElement; 20 | private _onAirStatus: UpRadioOnAirStatus; 21 | 22 | constructor(container: HTMLElement) { 23 | super(container, 'RemoteStream', template); 24 | 25 | this.audioOutput = container.querySelector('audio#UpRadioAudioOutput'); 26 | this.connectBtn = container.querySelector('button#UpRadioAudioOutput-connect'); 27 | this.playBtn = container.querySelector('button#UpRadioAudioOutput-play'); 28 | this.playBtn.disabled = true; 29 | this.stopBtn = container.querySelector('button#UpRadioAudioOutput-stop'); 30 | this.hideBtn(this.stopBtn); 31 | 32 | this.statusPanel = this.container.querySelector('div#remoteBroadcastStatusPanel'); 33 | this.statusText = this.container.querySelector('span#remoteBroadcastStatus'); 34 | this.freqMeter = new FreqMeter(this.statusPanel); 35 | 36 | this.playBtn.onclick = () => { 37 | this.freqMeter.init(this.stream); 38 | this.audioOutput.play(); 39 | }; 40 | this.stopBtn.onclick = () => { 41 | this.freqMeter.stop(); 42 | this.audioOutput.pause(); 43 | }; 44 | 45 | this.audioOutput.onplay = () => { 46 | this.hideBtn(this.playBtn); 47 | this.showBtn(this.stopBtn); 48 | }; 49 | this.audioOutput.onpause = () => { 50 | this.hideBtn(this.stopBtn); 51 | this.showBtn(this.playBtn); 52 | } 53 | } 54 | 55 | public get onAirStatus(): UpRadioOnAirStatus { 56 | return this._onAirStatus; 57 | } 58 | public set onAirStatus(status: UpRadioOnAirStatus) { 59 | this._onAirStatus = status; 60 | switch (status) { 61 | case UpRadioOnAirStatus.ON_AIR: 62 | this.statusText.classList.remove('text-red-500'); 63 | this.statusText.classList.add('text-green-500'); 64 | this.connectBtn.classList.add('border-green-500'); 65 | this.statusText.innerText = 'ON AIR'; 66 | break; 67 | case UpRadioOnAirStatus.OFF_AIR: 68 | this.statusText.classList.add('text-red-500'); 69 | this.statusText.classList.remove('text-green-500'); 70 | this.connectBtn.classList.remove('border-green-500'); 71 | this.hideBtn(this.stopBtn); 72 | this.playBtn.disabled = true; 73 | this.showBtn(this.playBtn); 74 | this.statusText.innerText = 'OFF AIR'; 75 | break; 76 | } 77 | } 78 | 79 | private hideBtn(btn: HTMLButtonElement) { 80 | btn.classList.add('hidden'); 81 | } 82 | 83 | private showBtn(btn: HTMLButtonElement) { 84 | btn.classList.remove('hidden'); 85 | } 86 | 87 | public async start(stream: MediaStream): Promise { 88 | this.stream = stream; 89 | this.audioOutput.srcObject = stream; 90 | this.dialToneGenerator.start(); 91 | } 92 | public async stop(): Promise { 93 | this.stream = null; 94 | this.audioOutput.srcObject = null; 95 | this.dialToneGenerator.stop(); 96 | } 97 | 98 | public getDialTone(): void { 99 | const [oscillator, outputStream] = UpRadioAudioService.createToneGeneratorAndStream(); 100 | this.dialToneGenerator = oscillator; 101 | this.dialTone = outputStream.stream; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /ui/src/components/Streams/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LocalStream.component'; -------------------------------------------------------------------------------- /ui/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'eventemitter3'; 2 | 3 | export interface IComponent { 4 | container: HTMLElement; 5 | hide(): void; 6 | show(): void; 7 | } 8 | 9 | export class Component extends EventEmitter implements IComponent { 10 | public parent: HTMLElement; 11 | public container: HTMLElement; 12 | public id: string; 13 | 14 | constructor(parent: HTMLElement, id: string, template: string) { 15 | super(); 16 | this.parent = parent; 17 | this.id = id; 18 | this.container = document.createElement('div'); 19 | this.container.id = id; 20 | this.container.classList.add('upradio-component'); 21 | this.container.innerHTML = template; 22 | this.parent.appendChild(this.container); 23 | } 24 | 25 | show(): void { 26 | this.container.hidden = false; 27 | this.container.classList.remove('hidden'); 28 | } 29 | hide(): void { 30 | this.container.hidden = true; 31 | this.container.classList.add('hidden'); 32 | } 33 | } 34 | 35 | export * from './Connect'; 36 | export * from './ModeSwitch'; 37 | export * from './Streams'; -------------------------------------------------------------------------------- /ui/src/components/logger.ts: -------------------------------------------------------------------------------- 1 | // With big thanks to PeerJS 2 | 3 | const LOG_PREFIX = 'UpRadio'; 4 | 5 | /* 6 | Prints log messages depending on the debug level passed in. Defaults to 0. 7 | 0 Prints no logs. 8 | 1 Prints only errors. 9 | 2 Prints errors and warnings. 10 | 3 Prints all logs. 11 | */ 12 | export enum LogLevel { 13 | Disabled, 14 | Errors, 15 | Warnings, 16 | All 17 | } 18 | 19 | export class Logger { 20 | private logLevel = LogLevel.Disabled; 21 | 22 | constructor(logLevel?: LogLevel) { 23 | this.logLevel = logLevel || LogLevel.Disabled; 24 | } 25 | 26 | log(...args: any[]) { 27 | if (this.logLevel >= LogLevel.All) { 28 | this._print('log', ...args); 29 | } 30 | } 31 | 32 | debug(...args: any[]) { 33 | if (this.logLevel >= LogLevel.All) { 34 | this._print('debug', ...args); 35 | } 36 | } 37 | 38 | info(...args: any[]) { 39 | if (this.logLevel >= LogLevel.All) { 40 | this._print('info', ...args); 41 | } 42 | } 43 | 44 | warn(...args: any[]) { 45 | if (this.logLevel >= LogLevel.Warnings) { 46 | this._print('warn', ...args); 47 | } 48 | } 49 | 50 | error(...args: any[]) { 51 | if (this.logLevel >= LogLevel.Errors) { 52 | this._print('error', ...args); 53 | } 54 | } 55 | 56 | private _print(level: string, ...rest: any[]): void { 57 | const copy = [`${LOG_PREFIX} ${level.toUpperCase()} ::`, ...rest]; 58 | 59 | for (let i in copy) { 60 | if (copy[i] instanceof Error) { 61 | copy[i] = "(" + copy[i].name + ") " + copy[i].message; 62 | 63 | } 64 | } 65 | console[level](...copy); 66 | } 67 | } 68 | 69 | export default new Logger(); -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | const { App } = require('./app.ts'); 2 | const { UpRadioAppState, Idb } = require('./UpRadioState.ts'); 3 | 4 | main().catch(console.error); 5 | 6 | async function main() { 7 | let savedState = await Idb.get('UpRadio::savedState'); 8 | if (savedState) { 9 | savedState = JSON.parse(savedState); 10 | } 11 | 12 | const root = document.getElementById('root'); 13 | const channelName = getChannelName(); 14 | 15 | const StateManager = new UpRadioAppState(savedState); 16 | if (channelName) { 17 | StateManager.mode = 'LISTEN'; 18 | StateManager.targetChannelName = channelName; 19 | } else { 20 | StateManager.mode = 'BROADCAST'; 21 | } 22 | const app = new App(root, StateManager.toJSON()); 23 | StateManager.init(app); 24 | initNav(); 25 | initHelp(); 26 | } 27 | 28 | function getChannelName() { 29 | const firstPathParam = location.pathname.split('/')[1]; 30 | return firstPathParam || null; 31 | } 32 | 33 | function initNav() { 34 | const nav = document.querySelector('nav'); 35 | const menuBtn = document.querySelector('button#headerMenuBtn'); 36 | menuBtn.onclick = () => { 37 | nav.classList.toggle('hidden'); 38 | } 39 | } 40 | 41 | function initHelp() { 42 | const helpText = document.querySelector('div#helpText'); 43 | const helpBtn = document.querySelector('button#helpBtn'); 44 | helpBtn.onclick = () => { 45 | helpText.classList.toggle('hidden'); 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /ui/src/sw.js: -------------------------------------------------------------------------------- 1 | workbox.core.skipWaiting(); 2 | workbox.core.clientsClaim(); 3 | 4 | workbox.routing.registerRoute( 5 | ({url}) => url.origin === 'https://hacker-news.firebaseio.com', 6 | new workbox.strategies.StaleWhileRevalidate() 7 | ); 8 | 9 | self.addEventListener('push', (event) => { 10 | const title = 'Get Started With Workbox'; 11 | const options = { 12 | body: event.data.text() 13 | }; 14 | event.waitUntil(self.registration.showNotification(title, options)); 15 | }); 16 | 17 | workbox.precaching.precacheAndRoute(self.__WB_MANIFEST); -------------------------------------------------------------------------------- /ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [ 3 | './src/**/*.js', 4 | './src/**/*.ts', 5 | './src/**/*.html', 6 | './index.html' 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | variants: {}, 12 | plugins: [], 13 | } 14 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | // /* Strict Type-Checking Options */ 26 | // "strict": true, 27 | // /* Enable all strict type-checking options. */ 28 | // "noImplicitAny": true, 29 | // /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, 31 | // /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, 33 | // /* Enable strict checking of function types. */ 34 | // "strictBindCallApply": true, 35 | // /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, 37 | // /* Enable strict checking of property initialization in classes. */ 38 | // "noImplicitThis": true, 39 | // /* Raise error on 'this' expressions with an implied 'any' type. */ 40 | // "alwaysStrict": true, 41 | // /* Parse in strict mode and emit "use strict" for each source file. */ 42 | 43 | // /* Additional Checks */ 44 | // "noUnusedLocals": true, 45 | // /* Report errors on unused locals. */ 46 | // "noUnusedParameters": true, 47 | // /* Report errors on unused parameters. */ 48 | // "noImplicitReturns": true, 49 | // /* Report error when not all code paths in function return a value. */ 50 | // "noFallthroughCasesInSwitch": true, 51 | // /* Report errors for fallthrough cases in switch statement. */ 52 | 53 | /* Module Resolution Options */ 54 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 55 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 56 | "paths": { 57 | "@upradio-client/*": [ 58 | "./src/*" 59 | ] 60 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 61 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 62 | // "typeRoots": [], /* List of folders to include type definitions from. */ 63 | // "types": [], /* Type declaration files to be included in compilation. */ 64 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 65 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 66 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 67 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 68 | 69 | /* Source Map Options */ 70 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 71 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 72 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 73 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 74 | 75 | /* Experimental Options */ 76 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 77 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 78 | 79 | /* Advanced Options */ 80 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.html' { 2 | const content: string; 3 | export default content; 4 | } 5 | 6 | interface Window { 7 | webkitAudioContext: typeof AudioContext; 8 | logger: any; 9 | } 10 | -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const CopyPlugin = require('copy-webpack-plugin'); 3 | const dotenv = require('dotenv').config(); 4 | const workboxPlugin = require('workbox-webpack-plugin'); 5 | const htmlPlugin = require('html-webpack-plugin'); 6 | const { CleanWebpackPlugin } = require('clean-webpack-plugin'); 7 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 8 | const path = require('path'); 9 | 10 | module.exports = { 11 | entry: ['./src/main.js','./src/styles.css'], 12 | output: { 13 | publicPath: "/", 14 | filename: 'main.js', 15 | path: path.resolve(__dirname, '..', 'dist'), 16 | }, 17 | resolve: { 18 | extensions: ['.ts', '.js'], 19 | alias: { 20 | '@upradio-client': path.resolve(__dirname, 'src') 21 | } 22 | }, 23 | module: { 24 | rules: [ 25 | // all files with a `.ts` or `.tsx` extension will be handled by `ts-loader` 26 | { test: /\.tsx?$/, loader: "awesome-typescript-loader", exclude: /node_modules/ }, 27 | { test: /\.html?$/, loader: "raw-loader" }, 28 | { test: /\.css$/, use: [ 29 | MiniCssExtractPlugin.loader, 30 | 'css-loader', 31 | 'postcss-loader' 32 | ] 33 | } 34 | ] 35 | }, 36 | optimization: { 37 | splitChunks: { 38 | chunks: 'all', 39 | }, 40 | }, 41 | performance: { 42 | maxEntrypointSize: 1048576, 43 | maxAssetSize: 1048576 44 | }, 45 | plugins: [ 46 | new CleanWebpackPlugin(), 47 | new htmlPlugin({ filename: 'index.html', template: './index.html' }), 48 | new MiniCssExtractPlugin({ filename: 'main.css' }), 49 | new webpack.DefinePlugin({ 50 | 'process.env.PEER_SERVER': JSON.stringify(dotenv.parsed.PEER_SERVER), 51 | 'process.env.PEER_PATH': JSON.stringify(dotenv.parsed.PEER_PATH), 52 | 'process.env.PEER_KEY': JSON.stringify(dotenv.parsed.PEER_KEY), 53 | 'process.env.MAX_CONNECTIONS': JSON.stringify(dotenv.parsed.MAX_CONNECTIONS) 54 | }), 55 | new CopyPlugin({ 56 | patterns: [ 57 | { from: 'manifest.webmanifest', to: 'manifest.webmanifest' }, 58 | { from: 'images', to: 'images' }, 59 | { from: 'favicon.ico', to: 'favicon.ico' }, 60 | { from: 'robots.txt', to: 'robots.txt' } 61 | ] 62 | }), 63 | new workboxPlugin.GenerateSW({ 64 | swDest: 'sw.js', 65 | clientsClaim: true, 66 | skipWaiting: true, 67 | }) 68 | // new workboxPlugin.InjectManifest({ 69 | // swSrc: './src/sw.js', 70 | // swDest: 'sw.js' 71 | // }) 72 | ] 73 | }; 74 | -------------------------------------------------------------------------------- /ui/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const config = require('./webpack.config'); 4 | 5 | config.devtool = 'source-map'; 6 | config.mode = 'development'; 7 | config.devServer = { 8 | contentBase: path.join(__dirname, 'dist'), 9 | historyApiFallback: true, 10 | compress: true, 11 | port: 1234, 12 | host: '0.0.0.0', 13 | proxy: { 14 | '/api': 'https://upradio.iangregson.workers.dev' 15 | } 16 | }; 17 | config.plugins.unshift( 18 | new webpack.DefinePlugin({ 19 | 'process.env.DEBUG_LEVEL': JSON.stringify(3) 20 | }) 21 | ); 22 | 23 | module.exports = config; -------------------------------------------------------------------------------- /ui/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const config = require('./webpack.config'); 3 | 4 | config.mode = 'production'; 5 | config.plugins.unshift( 6 | new webpack.DefinePlugin({ 7 | 'process.env.DEBUG_LEVEL': JSON.stringify(Number(process.env.DEBUG_LEVEL || 0)) 8 | }) 9 | ); 10 | 11 | module.exports = config; -------------------------------------------------------------------------------- /ui/zondicons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/ui/zondicons/.DS_Store -------------------------------------------------------------------------------- /ui/zondicons/add-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/add-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/adjust.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/airplane.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/album.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/align-center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/align-justified.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/align-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/align-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/anchor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/announcement.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/apparel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-outline-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-outline-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-outline-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-outline-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thick-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thick-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thick-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thick-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thin-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thin-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thin-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-thin-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/artist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/at-symbol.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/attachment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/backspace.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/backward-step.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/backward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/badge.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/battery-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/battery-half.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/battery-low.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/beverage.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/block.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bluetooth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bolt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/book-reference.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bookmark copy 2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bookmark copy 3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bookmark-outline-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bookmark-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bookmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-horizontal.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-inner.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-none.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-outer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/border-vertical.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/brightness-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/brightness-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/browser-window-new.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/browser-window-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/browser-window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/bug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/buoy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/calculator.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/calendar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/chart-bar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/chart-pie.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/chart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/chat-bubble-dots.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/checkmark-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/checkmark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-outline-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-outline-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-outline-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-outline-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cheveron-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/close-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/close-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cloud-upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cloud.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/coffee.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/cog.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/color-palette.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/compose.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/computer-desktop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/computer-laptop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/conversation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/credit-card.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/currency-dollar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/date-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/dial-pad.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/directions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/document-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/document.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/dots-horizontal-double.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/dots-horizontal-triple.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/duplicate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/edit-copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/edit-crop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/edit-cut.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/edit-pencil.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/education.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/envelope.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/exclamation-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/exclamation-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/explore.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/factory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/fast-forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/fast-rewind.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/film.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/filter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/flag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/flashlight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/folder-outline-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/folder-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/folder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/format-bold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/format-font-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/format-italic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/format-text-size.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/format-underline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/forward-step.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/gift.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/hand-stop.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/hard-drive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/headphones.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/heart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/hot.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/hour-glass.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/inbox-check.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/inbox-download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/inbox-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/inbox.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/indent-decrease.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/indent-increase.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/information-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/information-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/key.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/keyboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/layers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/library.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/light-bulb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/list-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/list-bullet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/load-balancer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-current.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-food.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-gas-station.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-hotel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-marina.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-park.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-restroom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location-shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/location.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/lock-closed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/lock-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/map.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/menu.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/minus-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/minus-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mobile-devices.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-happy-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-happy-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-neutral-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-neutral-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-sad-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mood-sad-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/mouse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/music-album.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/music-artist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/music-notes.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/music-playlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/navigation-more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/network.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/news-paper.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/notification.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/notifications-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/notifications.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/paste.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pause-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pause-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pen-tool.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/phone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/photo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/php-elephant.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/play-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/playlist.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/plugin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/portfolio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/printer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/pylon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/question.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/queue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/radar copy 2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/radar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/radio.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/reload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/reply-all.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/reply.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/repost.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/save-disk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/screen-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/send.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/servers.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/share-01.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/share-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/shield.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/shopping-cart.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/show-sidebar.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/shuffle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/stand-by.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/star-full.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/station.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/step-backward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/step-forward.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/stethoscope.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/store-front.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/stroke-width.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/subdirectory-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/subdirectory-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/swap.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/tablet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/tag.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/target.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/text-box.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/text-decoration.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/thermometer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/thumbs-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/ticket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/time.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/timer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/tools copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/translate.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-bus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-car.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-case.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-taxi-cab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-train.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel-walk.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/travel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/trophy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/tuning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/upload.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/usb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/user-add.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/user-group.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/user-solid-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/user-solid-square.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/vector.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/video-camera.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-carousel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-column.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-hide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-show.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/view-tile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/volume-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/volume-mute.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/volume-off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/volume-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/wallet.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/watch.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/window-new.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/window-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/wrench.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/yin-yang.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/zoom-in.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/zondicons/zoom-out.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /workers-site/.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iangregson/upradio/88fa6ff7c21f7f9e20e8e5c2bab12c0518ff0f50/workers-site/.cargo-ok -------------------------------------------------------------------------------- /workers-site/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | worker 3 | -------------------------------------------------------------------------------- /workers-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "upradio_workers-site", 4 | "version": "0.1.0", 5 | "description": "Universal Public Radio | p2p audio broadcasting", 6 | "main": "src/index.js", 7 | "author": "Ian Gregson", 8 | "license": "MIT", 9 | "dependencies": { 10 | "@cloudflare/kv-asset-handler": "0.0.5", 11 | "crypto-js": "^4.2.0" 12 | }, 13 | "devDependencies": { 14 | "@cloudflare/workers-types": "^1.0.9", 15 | "@types/crypto-js": "^3.1.45", 16 | "@types/node": "^14.0.13", 17 | "ts-loader": "^9.4.2", 18 | "types-cloudflare-worker": "^1.2.0", 19 | "typescript": "^3.9.2", 20 | "webpack": "^5.94.0", 21 | "webpack-cli": "^5.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workers-site/src/api.ts: -------------------------------------------------------------------------------- 1 | 2 | import { UpRadioAuthService } from './auth'; 3 | import { UpRadioChannelService } from './channel'; 4 | import { UpRadioKvStore } from './kvstore'; 5 | 6 | export type UpRadioPeerId = string; 7 | export type UpRadioApiSessionToken = string; 8 | 9 | export const API_KEY_HEADER_NAME = 'X-UpRadio-Api-Token'; 10 | 11 | export interface IUpRadioApiRequest extends Request { 12 | isAuthenticated: boolean; 13 | peerId: UpRadioPeerId; 14 | token: UpRadioApiSessionToken; 15 | } 16 | 17 | export class UpRadioApiError extends Error { 18 | status: number 19 | message: string; 20 | 21 | constructor(status: number, message: string) { 22 | super(message); 23 | this.message = message; 24 | this.status = status; 25 | } 26 | 27 | toResponse(): Response { 28 | return new Response(this.message, { status: this.status }); 29 | } 30 | } 31 | 32 | export class UpRadioApiResponse { 33 | static ok(): Response { 34 | return new Response('OK', { status: 200 }); 35 | } 36 | static json(data: any) { 37 | try { 38 | return new Response(JSON.stringify(data), { status: 200 }); 39 | } catch (_) { 40 | return UpRadioApiResponse.error(); 41 | } 42 | } 43 | static text(data: string) { 44 | try { 45 | return new Response(data, { status: 200 }); 46 | } catch (_) { 47 | return UpRadioApiResponse.error(); 48 | } 49 | } 50 | static badRequest() { 51 | return new Response('Bad Request', { status: 400 }); 52 | } 53 | static conflict() { 54 | return new Response('Conflict', { status: 409 }); 55 | } 56 | static notFound() { 57 | return new Response('Not found', { status: 404 }); 58 | } 59 | static error(err?: UpRadioApiError | Error) { 60 | if (err && err instanceof UpRadioApiError) { 61 | return err.toResponse(); 62 | } 63 | return new Response('Interval server error', { status: 500 }); 64 | } 65 | } 66 | 67 | export class UpRadioApiRouter { 68 | static async route(req: IUpRadioApiRequest): Promise { 69 | const path = new URL(req.url).pathname; 70 | 71 | let response: Response; 72 | 73 | try { 74 | switch (path) { 75 | case '/api/login': 76 | response = await UpRadioApiRouter.login(req); 77 | break; 78 | case '/api/channel/verify': 79 | await UpRadioAuthService.authenticate(req); 80 | response = await UpRadioApiRouter.channelVerify(req); 81 | break; 82 | case '/api/channel/resolve': 83 | await UpRadioAuthService.authenticate(req); 84 | response = await UpRadioApiRouter.channelResolve(req); 85 | break; 86 | case '/api/heartbeat': 87 | await UpRadioAuthService.authenticate(req); 88 | response = await UpRadioApiRouter.heartbeat(req); 89 | break; 90 | 91 | default: 92 | response = UpRadioApiResponse.notFound(); 93 | break; 94 | } 95 | } catch (err) { 96 | console.error(err); 97 | response = UpRadioApiResponse.error(err); 98 | } 99 | 100 | return response; 101 | } 102 | 103 | static async login(req: Request): Promise { 104 | let token: string | null = null; 105 | try { 106 | const payload = await req.json(); 107 | token = payload.token; 108 | } catch (err) { 109 | throw new UpRadioApiError(400, 'No token provided'); 110 | } 111 | 112 | if (!token) { 113 | throw new UpRadioApiError(400, 'No token provided'); 114 | } 115 | const newToken = await UpRadioAuthService.newToken(token); 116 | 117 | if (!newToken || typeof newToken !== 'string') { 118 | throw new Error('Problem generating token.'); 119 | } 120 | 121 | return UpRadioApiResponse.text(newToken); 122 | } 123 | 124 | static async heartbeat(req: IUpRadioApiRequest): Promise { 125 | const { channelName } = await req.json(); 126 | 127 | if (channelName) { 128 | if (!await UpRadioChannelService.verify(channelName, req.peerId)) { 129 | return UpRadioApiResponse.conflict(); 130 | } 131 | // Update the KV store to ensure ttl is extended 132 | await UpRadioKvStore.put(channelName, req.peerId); 133 | } 134 | 135 | // Update the KV store to ensure ttl is extended 136 | await UpRadioKvStore.put(req.token, req.peerId); 137 | 138 | return UpRadioApiResponse.ok(); 139 | } 140 | 141 | static async channelResolve(req: IUpRadioApiRequest) { 142 | const { channelName } = await req.json(); 143 | 144 | if (!channelName) { 145 | throw new UpRadioApiError(400, 'No channel name to resolve'); 146 | } 147 | 148 | const peerId = await UpRadioChannelService.resolve(channelName); 149 | 150 | if (!peerId) { 151 | throw new UpRadioApiError(404, 'Channel not found.'); 152 | } 153 | 154 | return UpRadioApiResponse.text(peerId); 155 | } 156 | 157 | static async channelVerify(req: IUpRadioApiRequest) { 158 | const { channelName } = await req.json(); 159 | 160 | if (!channelName) { 161 | throw new UpRadioApiError(400, 'No channel name to verify'); 162 | } 163 | 164 | if (!await UpRadioChannelService.verify(channelName, req.peerId)) { 165 | return UpRadioApiResponse.conflict(); 166 | } 167 | 168 | return UpRadioApiResponse.ok(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /workers-site/src/auth.ts: -------------------------------------------------------------------------------- 1 | import { UpRadioApiSessionToken, API_KEY_HEADER_NAME } from "./api"; 2 | import sha256 from 'crypto-js/sha256'; 3 | import Base64 from 'crypto-js/enc-base64'; 4 | import Utf8 from 'crypto-js/enc-utf8'; 5 | import { UpRadioApiError, UpRadioPeerId } from "./api"; 6 | import { IUpRadioApiRequest } from './api'; 7 | import { UpRadioKvStore } from './kvstore'; 8 | 9 | export class UpRadioAuthService { 10 | static async authenticate(req: IUpRadioApiRequest): Promise { 11 | const token = req.headers.get(API_KEY_HEADER_NAME) || undefined; 12 | 13 | if (!token) { 14 | req.isAuthenticated = false; 15 | } 16 | 17 | const peerId = await UpRadioAuthService.hasToken(token); 18 | 19 | if (!peerId) { 20 | req.isAuthenticated = false; 21 | } else { 22 | req.isAuthenticated = true; 23 | req.peerId = peerId; 24 | req.token = token; 25 | } 26 | 27 | if (!req.isAuthenticated) { 28 | throw new UpRadioApiError(401, 'Unauthorized'); 29 | } 30 | 31 | return req; 32 | } 33 | static async newToken(token: string): Promise { 34 | const wordArray = Base64.parse(token); 35 | const utf8Token = Utf8.stringify(wordArray); 36 | const [date, key, peerId] = utf8Token.split(':'); 37 | if (!date || !key || !peerId) throw new UpRadioApiError(400, 'Could not generate token from input.'); 38 | if (key !== PEER_KEY) throw new UpRadioApiError(400, 'Could not generate token from input.'); 39 | const newToken = Base64.stringify(sha256(date + peerId)); 40 | await UpRadioKvStore.put(newToken, peerId); 41 | return newToken; 42 | } 43 | static async hasToken(token: UpRadioApiSessionToken | undefined): Promise { 44 | const t: UpRadioPeerId | null = await UpRadioKvStore.get(token); 45 | 46 | return t; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /workers-site/src/channel.ts: -------------------------------------------------------------------------------- 1 | import { UpRadioPeerId } from "./api"; 2 | import { UpRadioKvStore } from './kvstore'; 3 | 4 | export type UpRadioChannelName = string; 5 | 6 | export class UpRadioChannelService { 7 | static async verify(channelName: UpRadioChannelName, peerId: UpRadioPeerId): Promise { 8 | // If channel doesn't exist, give it to peerId 9 | let channelOwner: UpRadioPeerId | null = await UpRadioKvStore.get(channelName); 10 | 11 | if (!channelOwner) { 12 | channelOwner = peerId; 13 | await UpRadioKvStore.put(channelName, channelOwner); 14 | return true; 15 | } 16 | 17 | // If channel exists, its owner must be the given peerId 18 | return channelOwner === peerId; 19 | } 20 | static async resolve(channelName: UpRadioChannelName): Promise { 21 | return UpRadioKvStore.get(channelName); 22 | } 23 | } -------------------------------------------------------------------------------- /workers-site/src/index.ts: -------------------------------------------------------------------------------- 1 | import CloudflareWorkerGlobalScope from 'types-cloudflare-worker'; 2 | import { UpRadioApiRouter, IUpRadioApiRequest } from './api'; 3 | declare var self: CloudflareWorkerGlobalScope; 4 | 5 | const { getAssetFromKV, mapRequestToAsset } = require('@cloudflare/kv-asset-handler') 6 | /** 7 | * The DEBUG flag will do two things that help during development: 8 | * 1. we will skip caching on the edge, which makes it easier to 9 | * debug. 10 | * 2. we will return an error message on exception in your Response rather 11 | * than the default 404.html page. 12 | */ 13 | const DEBUG = false 14 | 15 | export class Worker { 16 | static handle(event: FetchEvent) { 17 | const url = new URL(event.request.url); 18 | console.log(url.pathname); 19 | if (url.pathname.startsWith('/api')) { 20 | event.respondWith(UpRadioApiRouter.route(event.request)) 21 | } else { 22 | try { 23 | event.respondWith(handleEvent(event)) 24 | } catch (e) { 25 | if (DEBUG) { 26 | return event.respondWith( 27 | new Response(e.message || e.toString(), { 28 | status: 500, 29 | }), 30 | ) 31 | } 32 | event.respondWith(new Response('Internal Error', { status: 500 })) 33 | } 34 | } 35 | } 36 | } 37 | 38 | self.addEventListener('fetch', Worker.handle); 39 | 40 | async function handleEvent(event: FetchEvent) { 41 | let options: any = {}; 42 | options.mapRequestToAsset = (req: Request) => { 43 | // First let's apply the default handler, which we imported from 44 | // '@cloudflare/kv-asset-handler' at the top of the file. We do 45 | // this because the default handler already has logic to detect 46 | // paths that should map to HTML files, for which it appends 47 | // `/index.html` to the path. 48 | req = mapRequestToAsset(req) 49 | 50 | // Now we can detect if the default handler decided to map to 51 | // index.html in some specific directory. 52 | if (req.url.endsWith('/index.html')) { 53 | // Indeed. Let's change it to instead map to the root `/index.html`. 54 | // This avoids the need to do a redundant lookup that we know will 55 | // fail. 56 | return new Request(`${new URL(req.url).origin}/index.html`, req) 57 | } else { 58 | // The default handler decided this is not an HTML page. It's probably 59 | // an image, CSS, or JS file. Leave it as-is. 60 | return req 61 | } 62 | } 63 | /** 64 | * You can add custom logic to how we fetch your assets 65 | * by configuring the function `mapRequestToAsset` 66 | */ 67 | // options.mapRequestToAsset = handlePrefix(/^\/docs/) 68 | 69 | try { 70 | if (DEBUG) { 71 | // customize caching 72 | options.cacheControl = { 73 | bypassCache: true, 74 | } 75 | } 76 | return await getAssetFromKV(event, options) 77 | } catch (e) { 78 | // if an error is thrown try to serve the asset at 404.html 79 | if (!DEBUG) { 80 | try { 81 | let notFoundResponse = await getAssetFromKV(event, { 82 | mapRequestToAsset: (req: Request) => new Request(`${new URL(req.url).origin}/404.html`, req), 83 | }) 84 | 85 | return new Response(notFoundResponse.body, { ...notFoundResponse, status: 404 }) 86 | } catch (e) {} 87 | } 88 | 89 | return new Response(e.message || e.toString(), { status: 500 }) 90 | } 91 | } 92 | 93 | /** 94 | * Here's one example of how to modify a request to 95 | * remove a specific prefix, in this case `/docs` from 96 | * the url. This can be useful if you are deploying to a 97 | * route on a zone, or if you only want your static content 98 | * to exist at a specific path. 99 | */ 100 | // function handlePrefix(prefix: string) { 101 | // return (request: Request) => { 102 | // // compute the default (e.g. / -> index.html) 103 | // let defaultAssetKey = mapRequestToAsset(request) 104 | // let url = new URL(defaultAssetKey.url) 105 | 106 | // // strip the prefix from the path for lookup 107 | // url.pathname = url.pathname.replace(prefix, '/') 108 | 109 | // // inherit all other props from the default request 110 | // return new Request(url.toString(), defaultAssetKey) 111 | // } 112 | // } -------------------------------------------------------------------------------- /workers-site/src/kvstore.ts: -------------------------------------------------------------------------------- 1 | import { KVNamespace } from '@cloudflare/workers-types' 2 | 3 | declare global { 4 | const UPRADIO_KV: KVNamespace; 5 | const PEER_KEY: string; 6 | } 7 | 8 | export const DEFAULT_TTL_SECONDS = 60 * 10; // 10 minutes 9 | 10 | export class UpRadioKvStore { 11 | static async get(key?: string): Promise { 12 | if (!key) return null; 13 | return UPRADIO_KV.get(key) || null; 14 | } 15 | static async put(key?: string, value?: string | null, ttl: number = DEFAULT_TTL_SECONDS): Promise { 16 | if (!key || !value) return; 17 | return UPRADIO_KV.put(key, value, { expirationTtl: ttl }); 18 | } 19 | static async delete(key?: string): Promise { 20 | if (!key) return; 21 | return UPRADIO_KV.delete(key); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workers-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | /* https://developers.cloudflare.com/workers/reference/ */ 5 | /* Cloudflare Workers use the V8 JavaScript engine from Google Chrome. The 6 | * Workers runtime is updated at least once a week, to at least the version 7 | * that is currently used by Chrome’s stable release. This means you can 8 | * safely use latest JavaScript features, with no need for “transpilers”. 9 | */ 10 | "target": "ESNext", 11 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 12 | "module": "commonjs", 13 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 14 | "lib": ["esnext", "webworker"], 15 | /* Specify library files to be included in the compilation. */ 16 | // "allowJs": true, /* Allow javascript files to be compiled. */ 17 | // "checkJs": true, /* Report errors in .js files. */ 18 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 19 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 20 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 21 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 22 | // "outFile": "./", /* Concatenate and emit output to single file. */ 23 | "outDir": "./dist", 24 | /* Redirect output structure to the directory. */ 25 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 26 | // "composite": true, /* Enable project compilation */ 27 | // "removeComments": true, /* Do not emit comments to output. */ 28 | // "noEmit": true, /* Do not emit outputs. */ 29 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 30 | "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 31 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 32 | 33 | /* Strict Type-Checking Options */ 34 | "strict": true, 35 | /* Enable all strict type-checking options. */ 36 | "noImplicitAny": true, 37 | /* Raise error on expressions and declarations with an implied 'any' type. */ 38 | "strictNullChecks": true, 39 | /* Enable strict null checks. */ 40 | "strictFunctionTypes": true, 41 | /* Enable strict checking of function types. */ 42 | "strictBindCallApply": true, 43 | /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 44 | "strictPropertyInitialization": true, 45 | /* Enable strict checking of property initialization in classes. */ 46 | "noImplicitThis": true, 47 | /* Raise error on 'this' expressions with an implied 'any' type. */ 48 | "alwaysStrict": true, 49 | /* Parse in strict mode and emit "use strict" for each source file. */ 50 | 51 | /* Additional Checks */ 52 | "noUnusedLocals": true, 53 | /* Report errors on unused locals. */ 54 | "noUnusedParameters": true, 55 | /* Report errors on unused parameters. */ 56 | "noImplicitReturns": true, 57 | /* Report error when not all code paths in function return a value. */ 58 | "noFallthroughCasesInSwitch": true, 59 | /* Report errors for fallthrough cases in switch statement. */ 60 | 61 | /* Module Resolution Options */ 62 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 63 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 64 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 65 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 66 | // "typeRoots": ["node_modules/@types"], 67 | /* List of folders to include type definitions from. */ 68 | // "types": [], /* Type declaration files to be included in compilation. */ 69 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 70 | "esModuleInterop": true, 71 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 72 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 73 | 74 | /* Source Map Options */ 75 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 76 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 77 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 78 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 79 | 80 | /* Experimental Options */ 81 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 82 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 83 | "paths": { 84 | "@upradio-server/*": [ 85 | "./workers-site/src/*" 86 | ] 87 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 88 | }, 89 | "include": [ 90 | "./src/**/*", 91 | "./test/**/*" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /workers-site/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | context: __dirname, 5 | entry: { 6 | 'index': './src/index.ts', 7 | }, 8 | /*devtool: 'inline-source-map',*/ 9 | module: { 10 | rules: [{ 11 | test: /\.tsx?$/, 12 | use: 'ts-loader', 13 | exclude: /node_modules/ 14 | }] 15 | }, 16 | resolve: { 17 | extensions: ['.ts', '.js'], 18 | alias: { 19 | "@upradio-server": path.resolve(__dirname, 'src') 20 | } 21 | }, 22 | output: { 23 | filename: '[name].js', 24 | path: path.resolve(__dirname, 'dist') 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /wrangler.template.toml: -------------------------------------------------------------------------------- 1 | name = "upradio" 2 | type = "webpack" 3 | account_id = "... your account_id goes here ..." 4 | workers_dev = true 5 | route = "" 6 | zone_id = "" 7 | webpack_config = "./workers-site/webpack.config.js" 8 | 9 | kv-namespaces = [ 10 | { binding = "UPRADIO_KV", id = "... your namespace id goes here ..." } 11 | ] 12 | 13 | [site] 14 | bucket = "dist" 15 | entry-point = "workers-site" --------------------------------------------------------------------------------