├── .gitignore ├── .vscode └── launch.json ├── README.md ├── arconnect_notes ├── diagrams └── permacast.png ├── package.json ├── podcasts ├── new.js ├── podcast.js ├── podcast.json └── podcastNew.js ├── public ├── alt-favicon.ico ├── favicon.ico ├── index.html ├── locales │ ├── en │ │ └── translation.json │ ├── uk │ │ └── translation.json │ └── zh │ │ └── translation.json ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── App.test.js ├── component │ ├── arconnect_loader.jsx │ ├── index.jsx │ ├── navbar.jsx │ ├── podcast.jsx │ ├── podcast_html.jsx │ ├── podcast_rss.jsx │ ├── podcast_utils.jsx │ ├── upload_episode.jsx │ ├── upload_show.jsx │ └── wallet_loader.jsx ├── i18n.js ├── index.js ├── logo.svg ├── reportWebVitals.js ├── setupTests.js ├── utils │ ├── arweave.js │ ├── initStateGen.js │ ├── podcast.js │ ├── shorthands.js │ └── theme.js └── yellow-rec.svg ├── tailwind.config.js ├── v2-contracts ├── v2.js └── v2.json ├── v3 ├── oracle │ ├── oracle.js │ └── oracle.json ├── v3.js └── v3.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | # tailwindcss 22 | src/tailwind.css 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Permacast 2 | 3 | Permacast is a podcasting hosting and discovery platform powered by the Arweave Permaweb. Let you podcasts live forever with censorship resistance. 4 | 5 | ## Workflow 6 | 7 | Any content creator (podcasts) can use the permacast platform to perma-host his/her podcasts and its episodes. The audio files are permanently archived in Arweave's Permaweb once uploaded to Permacast frontend. 8 | 9 | Each content-creator needs an Arweave wallet topped-up with AR tokens to deploy a contract, and start adding episodes. 10 | 11 |
12 | 13 | ## Permacast Smart Contracts 14 | | Name | Path | Onchain Source Code | 15 | | ------------- |:-------------:| ------------- | 16 | | Permacast V2 Master Contract Src | [./v2-contracts](./v2-contracts) | [KrMNSCljeT0sox8bengHf0Z8dxyE0vCTLEAOtkdrfjM](https://viewblock.io/arweave/tx/KrMNSCljeT0sox8bengHf0Z8dxyE0vCTLEAOtkdrfjM) | 17 | | Permacast V3 Master Contract Src | [./v3](./v3) | [-SoIrUzyGEBklizLQo1w5AnS7uuOB87zUrg-kN1QWw4](https://viewblock.io/arweave/tx/-SoIrUzyGEBklizLQo1w5AnS7uuOB87zUrg-kN1QWw4) | 18 | | Factories Oracle Contract Address | [./v3/oracle](./v3/oracle) | [8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA](https://viewblock.io/arweave/address/8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA) | 19 | 20 | ## Front-Ends Access: 21 | - Production version -- Decentralized UI hosting: [permacast.net](https://permacast.net) 22 | - Development version -- Centralized UI hosting: [permacast.dev](https://permacast.dev) 23 | 24 | ## Tech-Stack 25 | - Frontend: React 26 | - Backend: SmartWeave contracts 27 | - Gateway: [Meson Network](https://meson.network/) 28 | - UI Hosting: [Spheron Network](https://spheron.network/) 29 | 30 | ## Permacast API 31 | [permacast-cache](https://github.com/Parallel-news/permacast-cache) is the repositiry of the API of Permacast FE. 32 | 33 | ## Documentation & Guides 34 | You can find guide and tutorials on how to use Permacast [here](https://github.com/Parallel-news/permacast-docs). 35 | 36 | ## License 37 | 38 | Permacast is licensed under the [MIT license](./LICENSE). 39 | 40 | -------------------------------------------------------------------------------- /arconnect_notes: -------------------------------------------------------------------------------- 1 | sessionStorage -> 2 | wallet_address 3 | arweaveWallet 4 | 5 | 6 | -------------------------------------------------------------------------------- /diagrams/permacast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/diagrams/permacast.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "permacast", 3 | "version": "0.3.2", 4 | "private": true, 5 | 6 | "dependencies": { 7 | "@headlessui/react": "^1.5.0", 8 | "@heroicons/react": "^1.0.6", 9 | "@tailwindcss/aspect-ratio": "^0.4.0", 10 | "@tailwindcss/typography": "^0.5.1", 11 | "@testing-library/jest-dom": "^5.11.4", 12 | "@testing-library/react": "^12.1.2", 13 | "@testing-library/user-event": "^13.5.0", 14 | "ardb": "^1.1.9", 15 | "arweave": "^1.10.22", 16 | "arweave-fees.js": "^0.0.2", 17 | "arweave-multihost": "^0.1.0", 18 | "autoprefixer": "^10.4.2", 19 | "concurrently": "^7.0.0", 20 | "daisyui": "^2.6.0", 21 | "i18next": "^21.6.13", 22 | "i18next-browser-languagedetector": "^6.1.3", 23 | "i18next-http-backend": "^1.3.2", 24 | "postcss": "^8.4.6", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2", 27 | "react-dropzone": "^11.3.4", 28 | "react-i18next": "^11.15.5", 29 | "react-icons": "^4.2.0", 30 | "react-router": "^5.2.0", 31 | "react-router-dom": "^5.2.0", 32 | "react-scripts": "4.0.3", 33 | "shikwasa": "^2.1.2", 34 | "smartweave": "^0.4.41", 35 | "sweetalert2": "^11.4.0", 36 | "tailwindcss": "^3.0.22", 37 | "theme-change": "^2.0.2", 38 | "web-vitals": "^2.1.4" 39 | }, 40 | "devDependencies": { 41 | "react-error-overlay": "6.0.9" 42 | }, 43 | "scripts": { 44 | "start": "concurrently \"npm run start:css\" \"react-scripts start\"", 45 | "start:css": "tailwindcss -o src/tailwind.css --watch", 46 | "build": "npm run build:css && react-scripts build", 47 | "build:css": "NODE_ENV=production tailwindcss -o src/tailwind.css -m", 48 | "test": "react-scripts test", 49 | "eject": "react-scripts eject" 50 | }, 51 | "eslintConfig": { 52 | "extends": ["react-app", "react-app/jest"] 53 | }, 54 | "browserslist": { 55 | "production": [">0.2%", "not dead", "not op_mini all"], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /podcasts/new.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SWC used as first level data registery for 3 | * Arweave hosted podcasts. 4 | * 5 | * The current contract represents a basic PoC 6 | * 7 | * contributor(s): charmful0x 8 | * 9 | * Lisence: MIT 10 | **/ 11 | 12 | 13 | 14 | export async function handle(state, action) { 15 | const input = action.input 16 | const caller = action.caller 17 | const podcasts = state.podcasts 18 | 19 | const contractID = SmartWeave.contract.id 20 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID) 21 | const base64Owner = contractTxObject["owner"] 22 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner) 23 | 24 | if (input.function === "createPodcast") { 25 | const name = input.name 26 | const desc = input.desc 27 | const cover = input.cover 28 | 29 | const pid = SmartWeave.transaction.id 30 | const tagsMap = new Map(); 31 | 32 | if (caller !== contractOwner ) { 33 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 34 | } 35 | 36 | if (typeof name !== "string" || name.length > 50) { 37 | throw new ContractError('uncorrect name limit') 38 | } 39 | 40 | if (typeof desc !== "string" || desc.length > 500) { 41 | throw new ContractError('description too long') 42 | } 43 | 44 | // validate the cover TXID. it should be an Arweave data 45 | // TX having image/x mime type 46 | 47 | // <------------------------ 48 | if (typeof cover !== "string" || cover.length !== 43) { 49 | throw new ContractError('uncorrect cover format') 50 | } 51 | 52 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover) 53 | const tags = coverTx.get("tags") 54 | 55 | for (let tag of tags) { 56 | const key = tag.get("name", {decode: true, string: true} ) 57 | const value = tag.get("value", {decode: true, string: true}) 58 | tagsMap.set(key, value) 59 | } 60 | 61 | if (! tagsMap.has("Content-Type")) { 62 | throw new ContractError('uncorrect data transaction') 63 | } 64 | 65 | if (! tagsMap.get("Content-Type").startsWith("image/") ) { 66 | throw new ContractError('invalid mime type') 67 | } 68 | 69 | // ------------------------> 70 | 71 | podcasts.push({ 72 | "pid": pid, 73 | "index": _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array 74 | "owner": caller, 75 | "podcastName": name, 76 | "description": desc, 77 | "cover": cover, 78 | "episodes":[], 79 | "logs": [pid] 80 | }) 81 | 82 | return { state } 83 | } 84 | 85 | if ( input.function === "addEpisode") { 86 | const index = input.index // podcasts index 87 | const name = input.name 88 | const audio = input.audio // the TXID of 'audio/' data 89 | const desc = input.desc 90 | 91 | const tagsMap = new Map() 92 | 93 | if (caller !== contractOwner ) { 94 | 95 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 96 | } 97 | 98 | if (! podcasts[index]) { 99 | throw new ContractError('podcast the given having index not found') 100 | } 101 | 102 | if (typeof name !== "string" || name.length > 50) { 103 | throw new ContractError('uncorrect name limit') 104 | } 105 | 106 | if (typeof desc !== "string" || desc.length > 250) { 107 | throw new ContractError('description too long') 108 | } 109 | 110 | if (typeof audio !== "string" || audio.length !== 43) { 111 | throw new ContractError('invalid audio TX type') 112 | } 113 | 114 | const audioTx = await SmartWeave.unsafeClient.transactions.get(audio) 115 | const tags = audioTx.get("tags") 116 | 117 | for (let tag of tags) { 118 | const key = tag.get("name", {decode: true, string: true} ) 119 | const value = tag.get("value", {decode: true, string: true}) 120 | tagsMap.set(key, value) 121 | } 122 | 123 | if (! tagsMap.has("Content-Type")) { 124 | throw new ContractError('uncorrect data transaction') 125 | } 126 | 127 | if (! tagsMap.get("Content-Type").startsWith("audio/") ) { 128 | throw new ContractError('invalid mime type') 129 | } 130 | 131 | podcasts[index]["episodes"].push({ 132 | "eid": SmartWeave.transaction.id, 133 | "childOf": index, 134 | "episodeName": name, 135 | "description": desc, 136 | "audioTx": audio, 137 | "uploadedAt": SmartWeave.block.height, 138 | "logs": [SmartWeave.transaction.id] 139 | }) 140 | 141 | return { state } 142 | 143 | } 144 | 145 | 146 | 147 | // PODCAST ACTIONS: 148 | 149 | if (input.function === "deletePodcast") { 150 | const index = input.index 151 | 152 | if ( caller !== contractOwner) { 153 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 154 | } 155 | 156 | if (! Number.isInteger(index) ) { 157 | throw new ContractError('invalid index') 158 | } 159 | 160 | if (! podcasts[index]) { 161 | throw new ContractError('podcast having the gievn index does not exist') 162 | } 163 | 164 | podcasts.splice(index, 1) 165 | 166 | return { state } 167 | } 168 | 169 | if (input.function === "editPodcastName") { 170 | const index = input.index 171 | const name = input.name 172 | 173 | const actionTx = SmartWeave.transaction.id 174 | 175 | if ( caller !== contractOwner) { 176 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 177 | } 178 | 179 | if (! Number.isInteger(index) ) { 180 | throw new ContractError('invalid index') 181 | } 182 | 183 | if (! podcasts[index]) { 184 | throw new ContractError('podcast having the given index does not exist') 185 | } 186 | 187 | if (typeof name !== "string") { 188 | throw new ContractError('invalid name type') 189 | } 190 | 191 | if ( name.length < 3 || name.length > 50 ) { 192 | throw new ContractError('the name does not meet the name limits') 193 | } 194 | 195 | if ( podcasts[index]["podcastName"] === name) { 196 | throw new ContractError('old name and new name cannot be equals') 197 | } 198 | 199 | podcasts[index]["podcastName"] = name 200 | podcasts[index]["logs"].push(actionTx) 201 | 202 | return { state } 203 | } 204 | 205 | if (input.function === "editPodcastDesc") { 206 | const index = input.index 207 | const desc = input.desc 208 | 209 | const actionTx = SmartWeave.transaction.id 210 | 211 | if ( caller !== contractOwner) { 212 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 213 | } 214 | 215 | if (! Number.isInteger(index) ) { 216 | throw new ContractError('invalid index') 217 | } 218 | 219 | if (! podcasts[index]) { 220 | throw new ContractError('podcast having the given ID does not exist') 221 | } 222 | 223 | if ( typeof desc !== "string" ) { 224 | throw new ContractError('invalid description type') 225 | } 226 | 227 | if ( desc.length > 250 ) { 228 | throw new ContractError('description length too high') 229 | } 230 | 231 | if ( podcasts[index]["description"] === desc ) { 232 | throw new ContractError('old description and new description cannot be equals') 233 | } 234 | 235 | podcasts[index]["description"] = desc 236 | podcasts[index]["logs"].push(actionTx) 237 | 238 | return { state } 239 | 240 | } 241 | 242 | if (input.function === "editPodcastCover") { 243 | const index = input.index 244 | const cover = input.cover 245 | const actionTx = SmartWeave.transaction.id 246 | const tagsMap = new Map(); 247 | 248 | if ( caller !== contractOwner) { 249 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 250 | } 251 | 252 | if (! Number.isInteger(index) ) { 253 | throw new ContractError('invalid index') 254 | } 255 | 256 | if (! podcasts[index]) { 257 | throw new ContractError('podcast having the given id does not exist') 258 | } 259 | 260 | if (typeof cover !== "string" || cover.length !== 43) { 261 | throw new ContractError('uncorrect cover format') 262 | } 263 | 264 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover) 265 | const tags = coverTx.get("tags") 266 | 267 | for (let tag of tags) { 268 | const key = tag.get("name", {decode: true, string: true} ) 269 | const value = tag.get("value", {decode: true, string: true}) 270 | tagsMap.set(key, value) 271 | } 272 | 273 | if (! tagsMap.has("Content-Type")) { 274 | throw new ContractError('uncorrect data transaction') 275 | } 276 | 277 | if (! tagsMap.get("Content-Type").startsWith("image/") ) { 278 | throw new ContractError('invalid mime type') 279 | } 280 | 281 | if ( podcasts[index]["cover"] === cover ) { 282 | throw new ContractError('old cover and new cover cannot be equals') 283 | } 284 | 285 | podcasts[index]["cover"] = cover 286 | podcasts[index]["logs"].push(actionTx) 287 | 288 | return { state } 289 | 290 | } 291 | 292 | 293 | 294 | 295 | // EPISODES ACTIONS: 296 | 297 | if (input.function === "editEpisodeName") { 298 | const name = input.name 299 | const index = input.index //podcast index 300 | const id = input.id // episode's index 301 | 302 | const actionTx = SmartWeave.transaction.id 303 | 304 | if (caller !== contractOwner) { 305 | throw new ContractError('invalid caller. Only the contractOwner} can perform this action') 306 | } 307 | 308 | if (! podcasts[index]) { 309 | throw new ContractError('podcast having the given ID not found') 310 | } 311 | 312 | if (! podcasts[index]["episodes"][id]) { 313 | throw new ContractError('episode having the given index not found') 314 | } 315 | 316 | if (typeof name !== "string") { 317 | throw new ContractError('invalid name format') 318 | } 319 | 320 | if (name.length < 2 || name.length > 50) { 321 | throw new ContractError('name does not meet the name limits') 322 | } 323 | 324 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) { 325 | throw new ContractError('new name and old name cannot be the same') 326 | } 327 | 328 | podcasts[index]["episodes"][id]["episodeName"] = name 329 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 330 | 331 | 332 | return { state } 333 | } 334 | 335 | if (input.function === "editEpisodeDesc") { 336 | const index = input.index 337 | const id = input.id 338 | const desc = input.desc 339 | 340 | const actionTx = SmartWeave.transaction.id 341 | 342 | if (caller !== contractOwner) { 343 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 344 | } 345 | 346 | if (! podcasts[index]) { 347 | throw new ContractError('podcast having the given ID not found') 348 | } 349 | 350 | if (! podcasts[index]["episodes"][id]) { 351 | throw new ContractError('episode having the given index id not found') 352 | } 353 | 354 | if (typeof desc !== "string") { 355 | throw new ContractError('invalid description format') 356 | } 357 | 358 | if ( desc.length < 25 || desc.length > 500 ) { 359 | throw new ContractError('the description text does not meet the desc limits') 360 | } 361 | 362 | if ( podcasts[index]["episodes"][id]["description"] === desc) { 363 | throw new ContractError('old description and new description canot be the same') 364 | } 365 | 366 | podcasts[index]["episodes"][id]["description"] = desc 367 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 368 | 369 | return { state } 370 | } 371 | 372 | if (input.function === "deleteEpisode") { 373 | const index = input.index 374 | const id = input.id 375 | 376 | if ( caller !== contractOwner) { 377 | throw new ContractError('invalid caller. Only the contractOwner can perform this action') 378 | } 379 | 380 | if (! podcasts[index]) { 381 | throw new ContractError('podcast having the given ID not found') 382 | } 383 | 384 | if (! podcasts[index]["episodes"][id]) { 385 | throw new ContractError('episode having the given index not found') 386 | } 387 | 388 | podcasts[index]["episodes"].splice(id, 1) 389 | 390 | return { state } 391 | } 392 | 393 | // HELPER FUNCTIONS: 394 | function _getPodcastIndex() { 395 | if (podcasts.length === 0) { 396 | return 0 397 | } 398 | 399 | return (podcasts.length - 1 ) 400 | } 401 | 402 | throw new ContractError('unknow function supplied') 403 | } 404 | -------------------------------------------------------------------------------- /podcasts/podcast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SWC used as first level data registery for 3 | * Arweave hosted podcasts. 4 | * 5 | * The current contract represents a basic PoC 6 | * 7 | * contributor(s): charmful0x 8 | * 9 | * Lisence: MIT 10 | **/ 11 | 12 | 13 | 14 | export async function handle(state, action) { 15 | const input = action.input 16 | const caller = action.caller 17 | const podcasts = state.podcasts 18 | 19 | const contractID = SmartWeave.contract.id 20 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID) 21 | const base64Owner = contractTxObject["owner"] 22 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner) 23 | 24 | if (input.function === "createPodcast") { 25 | const name = input.name 26 | const desc = input.desc 27 | const cover = input.cover 28 | const pid = SmartWeave.transaction.id 29 | 30 | const tagsMap = new Map(); 31 | 32 | if (caller !== contractOwner ) { 33 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 34 | } 35 | 36 | if (typeof name !== "string" || name.length > 50) { 37 | throw new ContractError(`uncorrect name limit`) 38 | } 39 | 40 | if (typeof desc !== "string" || desc.length > 500) { 41 | throw new ContractError(`description too long`) 42 | } 43 | 44 | // validate the cover TXID. it should be an Arweave data 45 | // TX having image/x mime type 46 | 47 | // <------------------------ 48 | if (typeof cover !== "string" || cover.length !== 43) { 49 | throw new ContractError(`uncorrect cover format`) 50 | } 51 | 52 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover) 53 | const tags = coverTx.get("tags") 54 | 55 | for (let tag of tags) { 56 | const key = tag.get("name", {decode: true, string: true} ) 57 | const value = tag.get("value", {decode: true, string: true}) 58 | tagsMap.set(key, value) 59 | } 60 | 61 | if (! tagsMap.has("Content-Type")) { 62 | throw new ContractError(`uncorrect data transaction`) 63 | } 64 | 65 | if (! tagsMap.get("Content-Type").startsWith("image/") ) { 66 | throw new ContractError(`invalid mime type`) 67 | } 68 | 69 | // ------------------------> 70 | 71 | podcasts.push( { 72 | "pid": pid, 73 | "index": _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array 74 | "podcastName": name, 75 | "description": desc, 76 | "cover": cover, 77 | "episodes":[], 78 | "logs": [pid] 79 | } ) 80 | 81 | return { state } 82 | } 83 | 84 | if ( input.function === "addEpisode") { 85 | const index = input.index // podcast index 86 | const name = input.name 87 | const audio = input.audio 88 | const desc = input.desc 89 | 90 | const tagsMap = new Map() 91 | 92 | if (caller !== contractOwner ) { 93 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 94 | } 95 | 96 | if (! Number.isInteger(index) ) { 97 | throw new ContractError(`index must be an integer`) 98 | } 99 | 100 | if (! podcasts[index]) { 101 | throw new ContractError(`podcast having index: ${index} not found`) 102 | } 103 | 104 | 105 | if (typeof name !== "string" || name.length > 50) { 106 | throw new ContractError(`uncorrect name limit`) 107 | } 108 | 109 | if (typeof desc !== "string" || desc.length > 250) { 110 | throw new ContractError(`description too long`) 111 | } 112 | 113 | if (typeof audio !== "string" || audio.length !== 43) { 114 | throw new ContractError(`invalid audio TX`) 115 | } 116 | 117 | const audioTx = await SmartWeave.unsafeClient.transactions.get(audio) 118 | const tags = audioTx.get("tags") 119 | 120 | for (let tag of tags) { 121 | const key = tag.get("name", {decode: true, string: true} ) 122 | const value = tag.get("value", {decode: true, string: true}) 123 | tagsMap.set(key, value) 124 | } 125 | 126 | if (! tagsMap.has("Content-Type")) { 127 | throw new ContractError(`uncorrect data transaction`) 128 | } 129 | 130 | if (! tagsMap.get("Content-Type").startsWith("audio/") ) { 131 | throw new ContractError(`invalid mime type`) 132 | } 133 | 134 | podcasts[index]["episodes"].push({ 135 | "eid": SmartWeave.transaction.id, // episode TXID 136 | "childOf": index, 137 | "episodeName": name, 138 | "description": desc, 139 | "audioTx": audio, 140 | "uploadedAt": SmartWeave.block.height, 141 | "logs": [SmartWeave.transaction.id] 142 | }) 143 | 144 | return { state } 145 | 146 | } 147 | 148 | 149 | // PODCAST ACTIONS: 150 | 151 | if (input.function === "deletePodcast") { 152 | const index = input.index //podcast index 153 | 154 | if ( caller !== contractOwner) { 155 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 156 | } 157 | 158 | if (! Number.isInteger(index) ) { 159 | throw new ContractError(`index must be an integer`) 160 | } 161 | 162 | if (! podcasts[index]) { 163 | throw new ContractError(`podcast having index: ${index} does not exist`) 164 | } 165 | 166 | podcasts.splice(index, 1) 167 | 168 | return { state } 169 | } 170 | 171 | if (input.function === "editPodcastName") { 172 | const index = input.index 173 | const name = input.name 174 | 175 | const actionTx = SmartWeave.transaction.id 176 | 177 | if ( caller !== contractOwner) { 178 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 179 | } 180 | 181 | if (! Number.isInteger(index) ) { 182 | throw new ContractError(`index must be an integer`) 183 | } 184 | 185 | if (! podcasts[index]) { 186 | throw new ContractError(`podcast having index: ${index} does not exist`) 187 | } 188 | 189 | if (typeof name !== "string") { 190 | throw new ContractError(`invalid name type`) 191 | } 192 | 193 | if ( name.length < 3 || name.length > 50 ) { 194 | throw new ContractError(`the name does not meet the name limits`) 195 | } 196 | 197 | if ( podcasts[index]["podcastName"] === name) { 198 | throw new ContractError(`old name and new name cannot be equals`) 199 | } 200 | 201 | podcasts[index]["podcastName"] = name 202 | podcasts[index]["logs"].push(actionTx) 203 | 204 | return { state } 205 | } 206 | 207 | if (input.function === "editPodcastDesc") { 208 | const index = input.index 209 | const desc = input.desc 210 | 211 | const actionTx = SmartWeave.transaction.id 212 | 213 | if ( caller !== contractOwner) { 214 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 215 | } 216 | 217 | if (! Number.isInteger(index) ) { 218 | throw new ContractError(`index must be an integer`) 219 | } 220 | 221 | if (! podcasts[index]) { 222 | throw new ContractError(`podcast having index: ${index} does not exist`) 223 | } 224 | 225 | if ( typeof desc !== "string" ) { 226 | throw new ContractError(`invalid description type`) 227 | } 228 | 229 | if ( desc.length > 250 ) { 230 | throw new ContractError(`description length too high`) 231 | } 232 | 233 | if ( podcasts[index]["description"] === desc ) { 234 | throw new ContractError(`old description and new description cannot be equals`) 235 | } 236 | 237 | podcasts[index]["description"] = desc 238 | podcasts[index]["logs"].push(actionTx) 239 | 240 | return { state } 241 | 242 | } 243 | 244 | if (input.function === "editPodcastCover") { 245 | const index = input.index 246 | const cover = input.cover 247 | 248 | const tagsMap = new Map(); 249 | const actionTx = SmartWeave.transaction.id 250 | 251 | if ( caller !== contractOwner) { 252 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 253 | } 254 | 255 | if (! Number.isInteger(index) ) { 256 | throw new ContractError(`index must be an integer`) 257 | } 258 | 259 | if (! podcasts[index]) { 260 | throw new ContractError(`podcast having index: ${index} does not exist`) 261 | } 262 | 263 | if (typeof cover !== "string" || cover.length !== 43) { 264 | throw new ContractError(`uncorrect cover format`) 265 | } 266 | 267 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover) 268 | const tags = coverTx.get("tags") 269 | 270 | for (let tag of tags) { 271 | const key = tag.get("name", {decode: true, string: true} ) 272 | const value = tag.get("value", {decode: true, string: true}) 273 | tagsMap.set(key, value) 274 | } 275 | 276 | if (! tagsMap.has("Content-Type")) { 277 | throw new ContractError(`uncorrect data transaction`) 278 | } 279 | 280 | if (! tagsMap.get("Content-Type").startsWith("image/") ) { 281 | throw new ContractError(`invalid mime type`) 282 | } 283 | 284 | if ( podcasts[index]["cover"] === cover ) { 285 | throw new ContractError(`old cover and new cover cannot be equals`) 286 | } 287 | 288 | podcasts[index]["cover"] = cover 289 | podcasts[index]["logs"].push(actionTx) 290 | 291 | return { state } 292 | 293 | } 294 | 295 | // EPISODES ACTIONS: 296 | 297 | if (input.function === "editEpisodeName") { 298 | const name = input.name 299 | const index = input.index // podcast index 300 | const id = input.id // episode index 301 | 302 | const actionTx = SmartWeave.transaction.id 303 | 304 | if (caller !== contractOwner) { 305 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 306 | } 307 | 308 | if (! podcasts[index]) { 309 | throw new ContractError(`podcast having index: ${index} not found`) 310 | } 311 | 312 | if (! podcasts[index]["episodes"][id]) { 313 | throw new ContractError(`episode having index: ${id} not found`) 314 | } 315 | 316 | if (typeof name !== "string") { 317 | throw new ContractError(`invalid name format`) 318 | } 319 | 320 | if (name.length < 2 || name.length > 50) { 321 | throw new ContractError(`${name} does not meet the name limits`) 322 | } 323 | 324 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) { 325 | throw new ContractError(`new name and old name cannot be the same`) 326 | } 327 | 328 | podcasts[index]["episodes"][id]["episodeName"] = name 329 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 330 | 331 | return { state } 332 | } 333 | 334 | if (input.function === "editEpisodeDesc") { 335 | const index = input.index 336 | const id = input.id 337 | const desc = input.desc 338 | 339 | const actionTx = SmartWeave.transaction.id 340 | 341 | if (caller !== contractOwner) { 342 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 343 | } 344 | 345 | if (! podcasts[index]) { 346 | throw new ContractError(`podcast having index: ${index} not found`) 347 | } 348 | 349 | if (! podcasts[index]["episodes"][id]) { 350 | throw new ContractError(`episode having index: ${id} not found`) 351 | } 352 | 353 | if (typeof desc !== "string") { 354 | throw new ContractError(`invalid description format`) 355 | } 356 | 357 | if ( desc.length < 25 || desc.length > 500 ) { 358 | throw new ContractError(`the description text does not meet the desc limits`) 359 | } 360 | 361 | if ( podcasts[index]["episodes"][id]["description"] === desc) { 362 | throw new ContractError(`old description and new description canot be the same`) 363 | } 364 | 365 | podcasts[index]["episodes"][id]["description"] = desc 366 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 367 | 368 | return { state } 369 | } 370 | 371 | if (input.function === "deleteEpisode") { 372 | const index = input.index 373 | const id = input.id 374 | 375 | if ( caller !== contractOwner) { 376 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`) 377 | } 378 | 379 | if (! podcasts[index]) { 380 | throw new ContractError(`podcast having ID: ${index} not found`) 381 | } 382 | 383 | if (! podcasts[index][id]) { 384 | throw new ContractError(`episode having index: ${id} not found`) 385 | } 386 | 387 | podcasts[index]["episodes"].splice(id, 1) 388 | 389 | return { state } 390 | } 391 | 392 | // HELPER FUNCTIONS: 393 | function _getPodcastIndex() { 394 | if (podcasts.length === 0) { 395 | return 0 396 | } 397 | 398 | return (podcasts.length - 1 ) 399 | } 400 | 401 | 402 | throw new ContractError(`unknow function supplied: '${input.function}'`) 403 | 404 | } 405 | 406 | -------------------------------------------------------------------------------- /podcasts/podcast.json: -------------------------------------------------------------------------------- 1 | { 2 | "podcasts": [] 3 | } 4 | -------------------------------------------------------------------------------- /podcasts/podcastNew.js: -------------------------------------------------------------------------------- 1 | /** 2 | * SWC used as first level data registery for 3 | * Arweave hosted podcasts. 4 | * 5 | * The current contract represents a basic PoC 6 | * 7 | * contributor(s): charmful0x 8 | * 9 | * Lisence: MIT 10 | **/ 11 | 12 | 13 | 14 | export async function handle(state, action) { 15 | const input = action.input 16 | const caller = action.caller 17 | const podcasts = state.podcasts 18 | 19 | // ERRORS List 20 | const ERROR_INVALID_CALLER = `the caller is not allowed to execute this function`; 21 | const ERROR_INVALID_PRIMITIVE_TYPE = `the given data is not a corrected primitive type per function`; 22 | const ERROR_INVALID_STRING_LENGTH = `the string is out of the allowed length ranges`; 23 | const ERROR_NOT_A_DATA_TX = `the transaction is not an Arweave TX DATA`; 24 | const ERROR_MIME_TYPE = `the given mime type is not supported`; 25 | const ERROR_UNSUPPORTED_LANG = `the given language code is not supported`; 26 | const ERROR_REQUIRED_PARAMETER = `the function still require a parameter`; 27 | const ERROR_INVALID_NUMBER_TYPE = `only inetegers are allowed`; 28 | const ERROR_NEGATIVE_INTEGER = `negative integer was supplied when only positive Intare allowed`; 29 | const ERROR_EPISODE_INDEX_NOT_FOUND = `there is no episode with the given index`; 30 | const ERROR_PODCAST_INDEX_NOT_FOUND = `there is no podcast with the given index`; 31 | const ERROR_OLD_VALUE_EQUAL_TO_NEW = `old valueand new value are equal`; 32 | 33 | 34 | if (input.function === "createPodcast") { 35 | const name = input.name 36 | const author = input.author 37 | const desc = input.desc 38 | const lang = input.lang 39 | const isExplicit = input.isExplicit 40 | const categories = input.categories 41 | const email = input.email 42 | const cover = input.cover 43 | 44 | const pid = SmartWeave.transaction.id 45 | 46 | 47 | await _getContractOwner(true, caller) 48 | 49 | // show-level string validation 50 | 51 | _validateStringTypeLen(name, 3, 400); 52 | _validateStringTypeLen(author, 2, 50) 53 | _validateStringTypeLen(desc, 10, 4000); 54 | _validateStringTypeLen(email, 0, 320); 55 | _validateStringTypeLen(categories, 3, 150); 56 | _validateStringTypeLen(cover, 43, 43); 57 | _validateStringTypeLen(lang, 2, 2); 58 | 59 | 60 | await _validateDataTransaction(cover, "image/"); 61 | 62 | if (! ["yes", "no"].includes(isExplicit)) { 63 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE) 64 | } 65 | 66 | 67 | podcasts.push({ 68 | pid: pid, 69 | index: _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array 70 | childOd: SmartWeave.contract.id, 71 | owner: caller, 72 | podcastName: name, 73 | author: author, 74 | email: email, 75 | description: desc, 76 | language: lang, 77 | explicit: isExplicit, 78 | categories: (categories.split(",")).map(category => category.trim()), 79 | cover: cover, 80 | episodes:[], 81 | logs: [pid] 82 | }) 83 | 84 | return { state } 85 | } 86 | 87 | if ( input.function === "addEpisode") { 88 | const index = input.index // podcasts index 89 | const name = input.name 90 | const audio = input.audio // the TXID of 'audio/' data 91 | const desc = input.desc 92 | 93 | await _getContractOwner(true, caller); 94 | 95 | // show-level string validation 96 | 97 | _validateStringTypeLen(name, 3, 4000); 98 | _validateStringTypeLen(audio, 43, 43); 99 | _validateStringTypeLen(desc, 0, 4000); 100 | _validateInteger(index, true) 101 | 102 | const TxMetadata = await _validateDataTransaction(audio, "audio/") 103 | 104 | 105 | if (! podcasts[index]) { 106 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 107 | } 108 | 109 | 110 | podcasts[index]["episodes"].push({ 111 | eid: SmartWeave.transaction.id, 112 | childOf: index, 113 | episodeName: name, 114 | description: desc, 115 | audioTx: audio, 116 | audioTxByteSize: Number.parseInt( TxMetadata.size ), 117 | type: TxMetadata.type, 118 | uploadedAt: SmartWeave.block.timestamp, 119 | logs: [SmartWeave.transaction.id] 120 | }) 121 | 122 | return { state } 123 | 124 | } 125 | 126 | 127 | 128 | // PODCAST ACTIONS: 129 | 130 | if (input.function === "deletePodcast") { 131 | const index = input.index 132 | 133 | await _getContractOwner(true, caller); 134 | _validateInteger(index, true); 135 | 136 | if (! podcasts[index]) { 137 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 138 | } 139 | 140 | podcasts.splice(index, 1) 141 | 142 | return { state } 143 | } 144 | 145 | if (input.function === "editPodcastName") { 146 | const index = input.index 147 | const name = input.name 148 | 149 | const actionTx = SmartWeave.transaction.id 150 | 151 | await _getContractOwner(true, caller); 152 | _validateStringTypeLen(name, 3, 50); 153 | _validateInteger(index, true); 154 | 155 | if (! podcasts[index]) { 156 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 157 | } 158 | 159 | if ( podcasts[index]["podcastName"] === name) { 160 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW) 161 | } 162 | 163 | podcasts[index]["podcastName"] = name 164 | podcasts[index]["logs"].push(actionTx) 165 | 166 | return { state } 167 | } 168 | 169 | if (input.function === "editPodcastDesc") { 170 | const index = input.index 171 | const desc = input.desc 172 | 173 | const actionTx = SmartWeave.transaction.id 174 | 175 | await _getContractOwner(true, caller); 176 | _validateInteger(true, index); 177 | _validateStringTypeLen(desc, 10, 750); 178 | 179 | if (! podcasts[index]) { 180 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 181 | } 182 | 183 | if ( podcasts[index]["description"] === desc ) { 184 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW) 185 | } 186 | 187 | podcasts[index]["description"] = desc 188 | podcasts[index]["logs"].push(actionTx) 189 | 190 | return { state } 191 | 192 | } 193 | 194 | if (input.function === "editPodcastCover") { 195 | const index = input.index 196 | const cover = input.cover 197 | const actionTx = SmartWeave.transaction.id 198 | const tagsMap = new Map(); 199 | 200 | await _getContractOwner(true, caller); 201 | _validateStringTypeLen(cover, 43, 43); 202 | _validateInteger(true, index); 203 | 204 | if (! podcasts[index]) { 205 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 206 | } 207 | 208 | if ( podcasts[index]["cover"] === cover ) { 209 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW) 210 | } 211 | 212 | await _validateDataTransaction(cover) 213 | 214 | podcasts[index]["cover"] = cover 215 | podcasts[index]["logs"].push(actionTx) 216 | 217 | return { state } 218 | 219 | } 220 | 221 | 222 | 223 | 224 | // EPISODES ACTIONS: 225 | 226 | if (input.function === "editEpisodeName") { 227 | const name = input.name 228 | const index = input.index //podcast index 229 | const id = input.id // episode's index 230 | 231 | const actionTx = SmartWeave.transaction.id 232 | 233 | await _getContractOwner(true, caller); 234 | 235 | _validateStringTypeLen(name, 2, 50); 236 | _validateInteger(index, true); 237 | _validateInteger(id, true); 238 | _validateEpisodeExistence(index, id) 239 | 240 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) { 241 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW) 242 | } 243 | 244 | podcasts[index]["episodes"][id]["episodeName"] = name 245 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 246 | 247 | 248 | return { state } 249 | } 250 | 251 | if (input.function === "editEpisodeDesc") { 252 | const index = input.index 253 | const id = input.id 254 | const desc = input.desc 255 | 256 | const actionTx = SmartWeave.transaction.id 257 | 258 | await _getContractOwner(true, caller); 259 | 260 | _validateStringTypeLen(desc, 25, 500); 261 | _validateInteger(index, true); 262 | _validateInteger(id, true); 263 | _validateEpisodeExistence(index, id); 264 | 265 | 266 | if ( podcasts[index]["episodes"][id]["description"] === desc) { 267 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW) 268 | } 269 | 270 | podcasts[index]["episodes"][id]["description"] = desc 271 | podcasts[index]["episodes"][id]["logs"].push(actionTx) 272 | 273 | return { state } 274 | } 275 | 276 | if (input.function === "deleteEpisode") { 277 | const index = input.index 278 | const id = input.id 279 | 280 | await _getContractOwner(true, caller); 281 | 282 | _validateInteger(index, true); 283 | _validateInteger(id, true); 284 | _validateEpisodeExistence(index, id) 285 | 286 | podcasts[index]["episodes"].splice(id, 1) 287 | 288 | return { state } 289 | } 290 | 291 | // HELPER FUNCTIONS: 292 | function _getPodcastIndex() { 293 | if (podcasts.length === 0) { 294 | return 0 295 | } 296 | 297 | return (podcasts.length - 1 ) 298 | }; 299 | 300 | function _validateStringTypeLen(str, minLen, maxLen) { 301 | 302 | if (typeof str !== "string") { 303 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE) 304 | } 305 | 306 | if (str.length < minLen || str.length > maxLen) { 307 | throw new ContractError(ERROR_INVALID_STRING_LENGTH) 308 | } 309 | }; 310 | 311 | function _validateInteger(number, allowNull) { 312 | 313 | if ( typeof allowNull === "undefined" ) { 314 | throw new ContractError(ERROR_REQUIRED_PARAMETER) 315 | } 316 | 317 | if (! Number.isInteger(number) ) { 318 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE) 319 | } 320 | 321 | if (allowNull) { 322 | if (number < 0) { 323 | throw new ContractError(ERROR_NEGATIVE_INTEGER) 324 | } 325 | } else if (number <= 0) { 326 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE) 327 | } 328 | }; 329 | 330 | async function _getContractOwner(validate, caller) { 331 | 332 | const contractID = SmartWeave.contract.id 333 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID) 334 | const base64Owner = contractTxObject["owner"] 335 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner) 336 | 337 | if (validate && (contractOwner !== caller)) { 338 | throw new ContractError(ERROR_INVALID_CALLER) 339 | } 340 | 341 | return contractOwner 342 | } 343 | 344 | async function _validateDataTransaction(tx, mimeType) { 345 | 346 | const tagsMap = new Map(); 347 | const transaction = await SmartWeave.unsafeClient.transactions.get(tx) 348 | const tags = transaction.get("tags") 349 | 350 | for (let tag of tags) { 351 | const key = tag.get("name", {decode: true, string: true} ) 352 | const value = tag.get("value", {decode: true, string: true}) 353 | tagsMap.set(key, value) 354 | } 355 | 356 | if (! tagsMap.has("Content-Type")) { 357 | throw new ContractError(ERROR_NOT_A_DATA_TX) 358 | } 359 | 360 | if (! tagsMap.get("Content-Type").startsWith(mimeType) ) { 361 | throw new ContractError(ERROR_MIME_TYPE) 362 | } 363 | 364 | return { 365 | size: transaction.data_size, 366 | type: tagsMap.get("Content-Type") 367 | } 368 | 369 | }; 370 | 371 | 372 | function _validateEpisodeExistence(index, id) { 373 | 374 | if (! podcasts[index]) { 375 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND) 376 | } 377 | 378 | if (! podcasts[index]["episodes"][id]) { 379 | throw new ContractError(ERROR_EPISODE_INDEX_NOT_FOUND) 380 | } 381 | } 382 | 383 | 384 | throw new ContractError(`unknow function supplied: ${input.function}`) 385 | } 386 | -------------------------------------------------------------------------------- /public/alt-favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | permacast 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /public/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "navbar": { 4 | "help": "Get help", 5 | "new": "What's new", 6 | "swal": { 7 | "title": "New in permacast V3 ✨", 8 | "html": "
  • iTunes/Spotify compatible RSS imports via the upcoming permacast CLI
  • Gas cost reduction
  • Auto-protection from duplicated uploads
  • Read full changelog
  • " 9 | } 10 | }, 11 | "connector": { 12 | "login": "ArConnect login", 13 | "logout": "Logout", 14 | "swal": { 15 | "title": "Install ArConnect to continue", 16 | "text": "Permablog uses ArConnect to make it easier to authenticate and send transactions for questions and answers", 17 | "footer": "Download ArConnect here" 18 | } 19 | }, 20 | "uploadshow": { 21 | "addpoadcast": "Add a podcast", 22 | "title": "Add a new show", 23 | "label": "You'll add episodes to the show next.", 24 | "name": "Show name", 25 | "description": "Show description", 26 | "image": "Cover image", 27 | "author": "Author", 28 | "email": "Email", 29 | "language": "Podcast language", 30 | "category": "Category", 31 | "explicit": "Contains explicit content", 32 | "upload": "Upload", 33 | "cancel": "Cancel", 34 | "feeText": "Uploading the show will cost: ", 35 | "swal": { 36 | "showadded": { 37 | "title": "Show added", 38 | "text": "Show added permanently to Arweave. Check in a few minutes after the transaction has mined." 39 | }, 40 | "uploadfailed": { 41 | "title": "Unable to add show", 42 | "text": "Check your wallet balance and network connection" 43 | }, 44 | "reset": { 45 | "text": "Podcast cover image is not squared (1:1 aspect ratio)!" 46 | }, 47 | "uploading": { 48 | "title": "Uploading, please wait a few seconds..." 49 | } 50 | }, 51 | "uploading": "Processing..." 52 | }, 53 | "podcasthtml": { 54 | "swal": { 55 | "title": "Coming soon", 56 | "text": "Tip your favorite podcasts with $NEWS to show support" 57 | } 58 | }, 59 | "uploadepisode": { 60 | "swal": { 61 | "upload": { 62 | "title": "Upload underway!", 63 | "text": "We'll let you know when it's done. Go grab a ☕ or 🍺" 64 | }, 65 | "uploadcomplete": { 66 | "title": "Upload complete", 67 | "text": "Episode uploaded permanently to Arweave. Check in a few minutes after the transaction has mined." 68 | }, 69 | "uploadfailed": { 70 | "title": "Upload failed", 71 | "text": "Check your AR balance and network connection" 72 | } 73 | }, 74 | "title": "Add new episode to", 75 | "name": "Episode name", 76 | "description": "Episode description", 77 | "file": "Audio file", 78 | "verto": "List as an Atomic NFT on Verto?", 79 | "toupload": "to upload", 80 | "upload": "Upload", 81 | "uploading": "Uploading, please wait...", 82 | "uploaded": "Uploaded", 83 | "feeText": "Uploading an episode will incur an additional fee of " 84 | }, 85 | "generalerrors": { 86 | "cantfindaddress": { 87 | "title": "Unable to find address", 88 | "text": "Make sure your address exists and re-connect the wallet" 89 | }, 90 | "cantfetchprices": { 91 | "title": "Unable to fetch storage prices", 92 | "text": "Check your internet connection and try again later" 93 | }, 94 | "lowbalance": { 95 | "title": "Low balance", 96 | "text": "You don't have enough AR to cover the transaction fee. This transaction will cost: " 97 | } 98 | }, 99 | "podcast": { 100 | "newepisode": "add new episode" 101 | }, 102 | "loading": "Loading podcasts...", 103 | "nopodcasts": "No podcasts here yet. Upload one!", 104 | "sortpodcastsby": "Sort podcasts by", 105 | "sorting": { 106 | "podcastsactivity": "Latest Active podcast", 107 | "episodescount": "Episodes Count" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /public/locales/uk/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Мова", 3 | "navbar": { 4 | "help": "Отримати допомогу", 5 | "new": "Що нового", 6 | "swal": { 7 | "title": "Нове у permacast V3 ✨", 8 | "html": "
  • iTunes/Spotify сумісні RSS імпортуються через майбутній permacast CLI
  • Зниження вартості комісії за газ
  • Автоматичний захист від завантаження дублікатів
  • Прочитай повний список змін
  • " 9 | } 10 | }, 11 | "connector": { 12 | "login": "ArConnect логін", 13 | "logout": "Вийти", 14 | "swal": { 15 | "title": "Інсталюй ArConnect, щоб продовжити", 16 | "text": "Permablog використовує ArConnect, щоб полегшити автентифікацію та відправку запитів та відповідей для транзакцій", 17 | "footer": "Завантаж ArConnect тут" 18 | } 19 | }, 20 | "uploadshow": { 21 | "addpoadcast": "Додай подкаст", 22 | "title": "Додай нове шоу", 23 | "label": "Ви додасте епізоди до шоу далі.", 24 | "name": "Назва шоу", 25 | "description": "Опис шоу", 26 | "image": "Обкладинка", 27 | "author": "Автор", 28 | "email": "Електронна пошта", 29 | "language": "Мова подкасту", 30 | "category": "Категорія", 31 | "explicit": "Містить відвертий контент", 32 | "upload": "Завантажити", 33 | "cancel": "Відмінити", 34 | "feeText": "Завантаження шоу буде коштуватиt: ", 35 | "swal": { 36 | "showadded": { 37 | "title": "Шоу додано", 38 | "text": "Шоу остаточно додано до Arweave. Перевір через кілька хвилин після видобутку транзакції." 39 | }, 40 | "uploadfailed": { 41 | "title": "Не вдається додати шоу", 42 | "text": "Перевір баланс гаманця та підключення до мережі" 43 | }, 44 | "reset": { 45 | "text": "Обкладинка подкасту не квадратна (співвідношення сторін 1:1)!" 46 | }, 47 | "uploading": { 48 | "title": "Завантаження триває, почекай кілька секунд..." 49 | } 50 | }, 51 | "uploading": "Обробка..." 52 | }, 53 | "podcasthtml": { 54 | "swal": { 55 | "title": "Незабаром", 56 | "text": "Підтримай монеткою свої улюблені подкасти з $NEWS" 57 | } 58 | }, 59 | "uploadepisode": { 60 | "swal": { 61 | "upload": { 62 | "title": "Завантаження розпочалося!", 63 | "text": "Ми повідомимо, коли воно завершиться. Попий поки ☕ чи 🍺" 64 | }, 65 | "uploadcomplete": { 66 | "title": "Завантаження завершено", 67 | "text": "Епізод остаточно завантажено до Arweave. Перевір через кілька хвилин після видобутку транзакції." 68 | }, 69 | "uploadfailed": { 70 | "title": "Помилка завантаження", 71 | "text": "Перевір баланс AR та підключення до мережі" 72 | } 73 | }, 74 | "title": "Додай новий епізод до", 75 | "name": "Назва епізоду", 76 | "description": "Опис епізоду", 77 | "file": "Аудіо файл", 78 | "verto": "Монетизувати як Atomic NFT на Verto?", 79 | "toupload": "до завантаження", 80 | "upload": "Завантажити", 81 | "uploading": "Завантаження триває, будь ласка, почекай...", 82 | "uploaded": "Завантажено", 83 | "feeText": "За завантаження епізоду буде стягнуто додаткову оплату у розмірі" 84 | }, 85 | "generalerrors": { 86 | "cantfindaddress": { 87 | "title": "Не можливо знайти адресу", 88 | "text": "Переконайся, що адреса існує та повторно підключись до гаманця" 89 | }, 90 | "cantfetchprices": { 91 | "title": "Не вдалося отримати ціни на сховище", 92 | "text": "Перевір інтернет-з’єднання та спробуй пізніше" 93 | }, 94 | "lowbalance": { 95 | "title": "Недостатній баланс", 96 | "text": "У тебе недостатньо AR щоб покрити витрати на транзакцію. Ця транзакція коштуватиме:" 97 | } 98 | }, 99 | "podcast": { 100 | "newepisode": "додай новий епізод" 101 | }, 102 | "loading": "Завантажую подкасти...", 103 | "nopodcasts": "Тут ще немає подкастів. Завантаж свій!", 104 | "sortpodcastsby": "Сортувати подкасти за", 105 | "sorting": { 106 | "podcastsactivity": "Останній Активний подкаст", 107 | "episodescount": "Кількість Епізодів" 108 | } 109 | } -------------------------------------------------------------------------------- /public/locales/zh/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "语言", 3 | "navbar": { 4 | "help": "获取帮助", 5 | "new": "新功能", 6 | "swal": { 7 | "title": "Permacast V3 新功能 ✨", 8 | "html": "
  • 通过即将推出的 Permacast CLI 导入 iTunes/Spotify 兼容的 RSS
  • 降低 Gas 成本
  • 防止重复上传的自动保护措施
  • 阅读完整的更新日志
  • " 9 | } 10 | }, 11 | "connector": { 12 | "login": "ArConnect 登录", 13 | "logout": "登出", 14 | "swal": { 15 | "title": "安装 ArConnect 以继续", 16 | "text": "Permacast 使用 ArConnect 让认证和发送交易更加容易", 17 | "footer": "下载 ArConnect" 18 | } 19 | }, 20 | "uploadshow": { 21 | "addpoadcast": "添加播客", 22 | "title": "添加新的节目", 23 | "label": "之后您可在节目中添加剧集", 24 | "name": "节目名称", 25 | "description": "节目描述", 26 | "image": "封面图片", 27 | "author": "作者", 28 | "email": "邮箱", 29 | "language": "播客语言", 30 | "category": "类别", 31 | "explicit": "少儿不宜", 32 | "upload": "上传", 33 | "cancel": "取消", 34 | "feeText": "上传节目的费用:", 35 | "swal": { 36 | "showadded": { 37 | "title": "节目已添加", 38 | "text": "节目已永久地上传到 Arweave,待交易完成几分钟后再来查看" 39 | }, 40 | "uploadfailed": { 41 | "title": "无法添加节目", 42 | "text": "检查您的 AR 余额和网络连接" 43 | }, 44 | "reset": { 45 | "text": "播客的封面图片必须为正方形(1:1的长宽比)!" 46 | }, 47 | "uploading": { 48 | "title": "上传中,请稍候..." 49 | } 50 | }, 51 | "uploading": "加工..." 52 | }, 53 | "podcasthtml": { 54 | "swal": { 55 | "title": "即将到来", 56 | "text": "支持并为您最喜爱的播客打赏 $NEWS" 57 | } 58 | }, 59 | "uploadepisode": { 60 | "swal": { 61 | "upload": { 62 | "title": "上传中", 63 | "text": "我们会在上传完成后通知您,来一杯 ☕ 或 🍺" 64 | }, 65 | "uploadcomplete": { 66 | "title": "上传完成", 67 | "text": "剧集已永久地上传到 Arweave,待交易完成几分钟后再来查看" 68 | }, 69 | "uploadfailed": { 70 | "title": "上传失败", 71 | "text": "检查您的 AR 余额和网络连接" 72 | } 73 | }, 74 | "title": "添加新的剧集到", 75 | "name": "剧集名称", 76 | "description": "剧集描述", 77 | "file": "音频文件", 78 | "verto": "是否在 Verto 上展示原子 NFT ?", 79 | "toupload": "来进行上传", 80 | "upload": "上传", 81 | "uploading": "上传中,请耐心等待...", 82 | "uploaded": "已完成", 83 | "feeText": "上传剧集将产生额外费用" 84 | }, 85 | "generalerrors": { 86 | "cantfindaddress": { 87 | "title": "找不到地址", 88 | "text": "尝试重新连接您的钱包并更改其访问配置" 89 | }, 90 | "cantfetchprices": { 91 | "title": "无法获取存储价格", 92 | "text": "检查您的互联网连接,稍后再试" 93 | }, 94 | "lowbalance": { 95 | "title": "余额不足", 96 | "text": "您没有足够的 AR 来支付交易费用。 该交易将花费:" 97 | } 98 | }, 99 | "podcast": { 100 | "newepisode": "添加新剧集" 101 | }, 102 | "loading": "正在加载播客...", 103 | "nopodcasts": "这里还没有播客,请上传一个!", 104 | "sortpodcastsby": "播客排序", 105 | "sorting": { 106 | "podcastsactivity": "最新收听最多的播客", 107 | "episodescount": "剧集数" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { HashRouter as Router, Route } from "react-router-dom"; 2 | import NavBar from "./component/navbar.jsx"; 3 | import Podcast from "./component/podcast.jsx"; 4 | import Index from "./component/index.jsx"; 5 | import PodcastRss from "./component/podcast_rss.jsx"; 6 | 7 | export default function App() { 8 | return ( 9 |
    10 | 11 | 12 | } 16 | /> 17 | } /> 18 | } 22 | /> 23 | 24 |
    25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/component/arconnect_loader.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import UploadShow from './upload_show.jsx' 3 | import Swal from 'sweetalert2' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | const requiredPermissions = ['ACCESS_ADDRESS', 'ACCESS_ALL_ADDRESSES', 'SIGNATURE', 'SIGN_TRANSACTION'] 7 | 8 | export default function Header() { 9 | const [walletConnected, setWalletConnected] = useState(false) 10 | const [address, setAddress] = useState(undefined) 11 | const [ansData, setANSData] = useState(undefined) 12 | const { t } = useTranslation() 13 | 14 | useEffect(() => { 15 | // add ArConnect event listeners 16 | window.addEventListener('arweaveWalletLoaded', walletLoadedEvent) 17 | window.addEventListener('walletSwitch', walletSwitchEvent) 18 | return () => { 19 | // remove ArConnect event listeners 20 | window.removeEventListener('arweaveWalletLoaded', walletLoadedEvent) 21 | window.removeEventListener('walletSwitch', walletSwitchEvent) 22 | } 23 | }) 24 | 25 | // wallet address change event 26 | // when the user switches wallets 27 | const walletSwitchEvent = async (e) => { 28 | setAddress(e.detail.address) 29 | // setAddress("ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U") 30 | // setANSData(await getANSLabel(e.detail.address)) 31 | } 32 | 33 | // ArConnect script injected event 34 | const walletLoadedEvent = async () => { 35 | try { 36 | // connected, set address 37 | // (the user can still be connected, but 38 | // for this actions the "ACCESS_ADDRESS" 39 | // permission is required. if the user doesn't 40 | // have that, we still need to ask them to connect) 41 | const addr = await getAddr() 42 | setAddress(addr) 43 | // setAddress("ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U") 44 | // setANSData(await getANSLabel(addr)) 45 | setWalletConnected(true) 46 | } catch { 47 | // not connected 48 | setAddress(undefined) 49 | setWalletConnected(false) 50 | } 51 | } 52 | 53 | const installArConnectAlert = () => { 54 | Swal.fire({ 55 | icon: "warning", 56 | title: t("connector.swal.title"), 57 | text: t("connector.swal.text"), 58 | footer: `${t("connector.swal.footer")}`, 59 | customClass: "font-mono", 60 | }) 61 | } 62 | 63 | const getAddr = () => window.arweaveWallet.getActiveAddress() 64 | 65 | const shortenAddress = (addr) => { 66 | if (addr) { 67 | return addr.substring(0, 4) + '...' + addr.substring(addr.length - 4) 68 | } 69 | return addr 70 | } 71 | 72 | // const getANSLabel = async (addr) => { 73 | 74 | // return ans?.currentLabel 75 | // } 76 | 77 | useEffect(() => { 78 | const fetchData = async () => { 79 | try { 80 | const response = await fetch(`https://ans-testnet.herokuapp.com/profile/${address}`) 81 | const ans = await response.json() 82 | const {address_color, currentLabel, avatar = ""} = ans; 83 | console.log({address_color, currentLabel, avatar}) 84 | setANSData({address_color, currentLabel, avatar}) 85 | } catch (error) { 86 | console.error(error) 87 | } 88 | }; 89 | 90 | fetchData(); 91 | }, [address]); 92 | 93 | const arconnectConnect = async () => { 94 | if (window.arweaveWallet) { 95 | try { 96 | await window.arweaveWallet.connect(requiredPermissions) 97 | setAddress(await getAddr()) 98 | setWalletConnected(true) 99 | 100 | } catch { } 101 | } else { 102 | installArConnectAlert() 103 | } 104 | } 105 | 106 | const arconnectDisconnect = async () => { 107 | await window.arweaveWallet.disconnect() 108 | setWalletConnected(false) 109 | setAddress(undefined) 110 | } 111 | 112 | return ( 113 | <> 114 | {(walletConnected && ( 115 | <> 116 | 117 |
    121 | 122 | {ansData?.currentLabel ? `${ansData?.currentLabel}.ar` : shortenAddress(address)} 123 | 124 | {(ansData?.avatar === "") ? 125 |
    : 126 | // } 127 |
    128 | Profile 129 |
    } 130 | 131 |
    132 | 133 | )) || ( 134 |
    138 | 🦔 {t("connector.login")} 139 |
    140 | )} 141 | 142 | ) 143 | 144 | } -------------------------------------------------------------------------------- /src/component/index.jsx: -------------------------------------------------------------------------------- 1 | import { React, useState, useEffect } from 'react' 2 | import PodcastHtml from './podcast_html.jsx' 3 | import { MESON_ENDPOINT } from '../utils/arweave.js' 4 | import { useTranslation } from 'react-i18next' 5 | import { fetchPodcasts, sortPodcasts } from '../utils/podcast.js' 6 | import { Dropdown } from '../component/podcast_utils.jsx' 7 | 8 | export default function Index() { 9 | const [loading, setLoading] = useState(false) 10 | const [podcastsHtml, setPodcastsHtml] = useState([]) 11 | const { t } = useTranslation() 12 | const [sortedPodcasts, setSortedPodcasts] = useState() 13 | const [selection, setSelection] = useState(0) 14 | const filters = [ 15 | {type: "episodescount", desc: t("sorting.episodescount")}, 16 | {type: "podcastsactivity", desc: t("sorting.podcastsactivity")} 17 | ] 18 | const filterTypes = filters.map(f => f.type) 19 | 20 | const renderPodcasts = (podcasts) => { 21 | let html = [] 22 | for (const p of podcasts) { 23 | if (p && p.pid !== 'aMixVLXScjjNUUcXBzHQsUPmMIqE3gxDxNAXdeCLAmQ') { 24 | html.push( 25 | 34 | ) 35 | } 36 | } 37 | return html 38 | } 39 | 40 | useEffect(() => { 41 | const fetchData = async () => { 42 | setLoading(true) 43 | const sorted = await sortPodcasts(filterTypes) 44 | const podcastsHtml = renderPodcasts(sorted[filterTypes[selection]]) 45 | setPodcastsHtml(podcastsHtml) 46 | setSortedPodcasts(sorted) 47 | setLoading(false) 48 | } 49 | fetchData() 50 | }, []) 51 | 52 | const changeSorting = (n) => { 53 | const filteredPodcasts = sortedPodcasts[filterTypes[n]] 54 | const newPodcasts = renderPodcasts(filteredPodcasts) 55 | setPodcastsHtml(newPodcasts) 56 | setSelection(n) 57 | } 58 | 59 | return ( 60 |
    61 |
    62 | {loading ? t("loading") : podcastsHtml.length === 0 ? t("nopodcasts") : null} 63 |
    64 |
    65 |
    66 | {loading ? "": } 67 |
    68 |
    69 |
    70 | {podcastsHtml} 71 |
    72 |
    73 | ) 74 | 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/component/navbar.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import YellowRec from '../yellow-rec.svg' 3 | import Swal from 'sweetalert2' 4 | import ArConnectLoader from './arconnect_loader' 5 | import { isDarkMode } from '../utils/theme' 6 | import { themeChange } from "theme-change"; 7 | import { useTranslation } from 'react-i18next' 8 | import { Disclosure } from '@headlessui/react' 9 | import { TranslateIcon } from '@heroicons/react/outline' 10 | import { MenuIcon, XIcon } from "@heroicons/react/outline"; 11 | 12 | 13 | const language = [ 14 | { 15 | "code": "zh", 16 | "name": "简体中文" 17 | }, 18 | { 19 | "code": "en", 20 | "name": "English" 21 | }, 22 | { 23 | "code": "uk", 24 | "name": "український" 25 | }, 26 | ] 27 | 28 | export default function NavBar() { 29 | const [darkMode, setDarkMode] = useState(isDarkMode()) 30 | 31 | useEffect(() => { 32 | themeChange(false); 33 | // 👆 false parameter is required for react project 34 | }, []); 35 | 36 | const { t, i18n } = useTranslation(); 37 | 38 | const changeLanguage = (lng) => { 39 | i18n.changeLanguage(lng); 40 | }; 41 | 42 | const loadWhatsNew = () => { 43 | Swal.fire( 44 | { 45 | title: t("navbar.swal.title"), 46 | html: t("navbar.swal.html"), 47 | customClass: { 48 | title: "font-mono", 49 | htmlContainer: 'list text-left text-md font-mono' 50 | } 51 | } 52 | ) 53 | } 54 | 55 | return ( 56 |
    57 | 58 | {({ open }) => 59 | <> 60 | 61 |
    62 | 63 | permacast 64 | permacast 65 | 66 | 67 |
    68 | 📨 {t("navbar.help")} 69 | loadWhatsNew()}>✨ {t("navbar.new")} 70 |
    71 | 78 |
    79 | 82 |
      83 | {language.map(l => ( 84 |
    • 85 | changeLanguage(l.code)}>{l.name} 86 |
    • 87 | ))} 88 |
    89 |
    90 |
    91 | 92 | Open main menu 93 | {open ? ( 94 | 99 |
    100 |
    101 | 102 | 103 |
    104 |
    105 | 108 |
      109 | {language.map(l => ( 110 |
    • 111 | changeLanguage(l.code)}>{l.name} 112 |
    • 113 | ))} 114 |
    115 |
    116 | 122 | 📨 {t("navbar.help")} 123 | 124 | 128 | loadWhatsNew()}> 129 | ✨ {t("navbar.new")} 130 | 131 | 132 | 136 | 137 | 138 |
    139 |
    140 | } 141 |
    142 | 143 |
    144 | 145 |
    146 |
    147 |
    148 | ) 149 | } -------------------------------------------------------------------------------- /src/component/podcast.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import PodcastHtml from './podcast_html.jsx' 3 | import UploadEpisode from './upload_episode.jsx' 4 | import 'shikwasa/dist/shikwasa.min.css' 5 | import Swal from 'sweetalert2' 6 | import Shikwasa from 'shikwasa' 7 | import { MESON_ENDPOINT } from '../utils/arweave.js' 8 | import { isDarkMode } from '../utils/theme.js' 9 | import { fetchPodcasts } from '../utils/podcast.js'; 10 | import { useTranslation } from 'react-i18next'; 11 | 12 | export default function Podcast(props) { 13 | const [loading, setLoading] = useState(true) 14 | const [showEpisodeForm, setShowEpisodeForm] = useState(false) 15 | const [addr, setAddr] = useState('') 16 | const [thePodcast, setThePodcast] = useState(null) 17 | const [podcastHtml, setPodcastHtml] = useState(null) 18 | const [podcastEpisodes, setPodcastEpisodes] = useState([]) 19 | const { t } = useTranslation() 20 | 21 | const getPodcastEpisodes = async () => { 22 | const pid = props.match.params.podcastId; 23 | 24 | const response = await fetch(`https://whispering-retreat-94540.herokuapp.com/feeds/episodes/${pid}`, { 25 | method: 'GET', 26 | headers: { 'Content-Type': 'application/json', } 27 | }); 28 | 29 | const episodes = (await response.json())["episodes"]; 30 | return episodes; 31 | } 32 | 33 | const getPodcast = (p) => { 34 | let podcasts = p.filter( 35 | obj => !(obj && Object.keys(obj).length === 0) 36 | ) 37 | let id = props.match.params.podcastId; 38 | let podcast = _findPodcastById(podcasts, id) 39 | return podcast 40 | } 41 | 42 | const _findPodcastById = (podcastsList, id) => { 43 | let pList = podcastsList.filter( 44 | obj => !(obj && Object.keys(obj).length === 0) 45 | ) 46 | 47 | const match = pList.find(podcast => podcast.pid === id) 48 | return match 49 | } 50 | 51 | // let p = podcasts.find(podcastId => Object.values(podcasts).pid === podcastId) 52 | // console.log(p) 53 | /* 54 | let p = podcasts.podcasts 55 | for (var i=0, iLen=p.length; i { 63 | // let p = podcast.podcasts 64 | // const keys = Object.keys(p) 65 | // const values = Object.values(p) 66 | // const resultArr = [] 67 | 68 | // for (let i = 0; i < keys.length; i++) { 69 | // const currentValues = values[i] 70 | // const currentKey = keys[i] 71 | // currentValues["pid"] = currentKey 72 | // resultArr.push(currentValues) 73 | 74 | // } 75 | // return resultArr 76 | // } 77 | 78 | const loadPodcastHtml = (p) => { 79 | return 91 | } 92 | 93 | const tryAddressConnecting = async () => { 94 | let addr; 95 | try { 96 | addr = await window.arweaveWallet.getActiveAddress(); 97 | return addr; 98 | } catch (error) { 99 | console.log("🦔Displaying feed for non-ArConnect installed users🦔"); 100 | // address retrived from the top list of https://viewblock.io/arweave/addresses 101 | addr = "dRFuVE-s6-TgmykU4Zqn246AR2PIsf3HhBhZ0t5-WXE"; 102 | return addr; 103 | } 104 | }; 105 | 106 | const loadEpisodes = async (podcast, episodes) => { 107 | console.log(podcast) 108 | const episodeList = [] 109 | const addr = await tryAddressConnecting(); 110 | for (let i in episodes) { 111 | let e = episodes[i] 112 | console.log("episode", e) 113 | if (e.eid !== 'FqPtfefS8QNGWdPcUcrEZ0SXk_IYiOA52-Fu6hXcesw') { 114 | episodeList.push( 115 |
    119 |
    120 |
    121 | 127 | 132 | 133 | 134 | 135 | 136 |
    137 |
    {e.episodeName}
    138 |
    139 |
    140 | {truncatedDesc(e.description, 52)} 141 |
    142 |
    143 | ) 144 | 145 | } 146 | } 147 | return episodeList 148 | } 149 | 150 | const checkEpisodeForm = async (podObj) => { 151 | let addr = await window.arweaveWallet.getActiveAddress(); 152 | if (addr === podObj.owner || podObj.superAdmins.includes(addr)) { 153 | setShowEpisodeForm(true) 154 | window.scrollTo(0, 0) 155 | } else { 156 | alert('Not the owner of this podcast') 157 | } 158 | } 159 | /* 160 | loadPodcasts = async (id) => { 161 | const swcId = id 162 | let res = await readContract(arweave, swcId) 163 | return res 164 | } 165 | */ 166 | const truncatedDesc = (desc, maxLength) => { 167 | if (desc.length < maxLength) { 168 | return <>{desc} 169 | } else { 170 | return <>{desc.substring(0, maxLength)}... showDesc(desc)}>[read more] 171 | } 172 | } 173 | 174 | const showDesc = (desc) => { 175 | Swal.fire({ 176 | text: desc, 177 | button: 'close', 178 | customClass: "font-mono", 179 | }) 180 | } 181 | 182 | const showPlayer = (podcast, e) => { 183 | const player = new Shikwasa({ 184 | container: () => document.querySelector('.podcast-player'), 185 | themeColor: 'gray', 186 | theme: `${isDarkMode() ? 'dark' : 'light'}`, 187 | autoplay: true, 188 | audio: { 189 | title: e.episodeName, 190 | artist: podcast.podcastName, 191 | cover: `${MESON_ENDPOINT}/${podcast.cover}`, 192 | src: `${MESON_ENDPOINT}/${e.contentTx}`, 193 | }, 194 | download: true 195 | }) 196 | player.play() 197 | window.scrollTo(0, document.body.scrollHeight) 198 | } 199 | 200 | useEffect(() => { 201 | async function fetchData() { 202 | setLoading(true) 203 | 204 | const p = getPodcast(await fetchPodcasts()) 205 | console.log(p) 206 | const ep = await getPodcastEpisodes() 207 | setThePodcast(p) 208 | setPodcastHtml(loadPodcastHtml(p)) 209 | setPodcastEpisodes(await loadEpisodes(p, ep)) 210 | setAddr(await tryAddressConnecting()) 211 | 212 | setLoading(false) 213 | } 214 | fetchData() 215 | }, []) 216 | 217 | return ( 218 |
    219 | {showEpisodeForm ? : null} 220 | {loading &&
    {t("loading")}
    } 221 |
    222 | {podcastHtml} 223 |
    224 |
    {podcastEpisodes}
    225 | {!loading && (thePodcast.owner === addr || thePodcast.superAdmins.includes(addr)) && } 226 | < div className="podcast-player sticky bottom-0 w-screen" /> 227 |
    228 | 229 | ) 230 | } 231 | -------------------------------------------------------------------------------- /src/component/podcast_html.jsx: -------------------------------------------------------------------------------- 1 | import { React, } from 'react'; 2 | import { FaRss, FaRegGem } from 'react-icons/fa'; 3 | // import { arweave, NEWS_CONTRACT } from '../utils/arweave.js' 4 | import Swal from 'sweetalert2'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | export default function PodcastHtml({ name, link, description, image, rss, smallImage = false, truncated = false }) { 8 | const { t } = useTranslation() 9 | const loadRss = () => { 10 | console.log(rss) 11 | window.open(`https://whispering-retreat-94540.herokuapp.com/feeds/${rss}`, '_blank') 12 | } 13 | const tipButton = () => { 14 | return 15 | } 16 | 17 | // const checkNewsBalance = async (addr, tipAmount) => { 18 | // const contract = contract(NEWS_CONTRACT) 19 | // const state = await contract.readState(); 20 | // if (state.balances.hasOwnProperty(addr) && state.balances.addr >= tipAmount) { 21 | // return true 22 | // } else { 23 | // return false 24 | // } 25 | // } 26 | 27 | // const transferNews = async (recipient, tipAmount) => { 28 | // const input = { "function": "transfer", "target": recipient, "qty": parseInt(tipAmount) }; 29 | // const contract = contract(NEWS_CONTRACT); 30 | // const tx = await contract.writeInteraction(arweave, "use_wallet", NEWS_CONTRACT, input); 31 | // console.log(tx); 32 | // } 33 | 34 | const tipPrompt = async () => { 35 | Swal.fire({ 36 | title: t("podcasthtml.swal.title"), 37 | text: t("podcasthtml.swal.text"), 38 | customClass: "font-mono", 39 | }) 40 | return false 41 | 42 | // const addr = await window.arweaveWallet.getActiveAddress(); 43 | 44 | // const podcastId = id; 45 | // const name = name; 46 | // const recipient = props.owner; 47 | // const { value: tipAmount } = await Swal.fire({ 48 | // title: `Tip ${name} 🙏`, 49 | // input: 'text', 50 | // inputPlaceholder: 'Amount to tip ($NEWS)', 51 | // confirmButtonText: 'Tip' 52 | // }); 53 | 54 | // if (tipAmount && checkNewsBalance(addr, tipAmount)) { 55 | 56 | // let n = parseInt(tipAmount); 57 | // if (Number.isInteger(n) && n > 0) { 58 | 59 | // if (transferNews(recipient, tipAmount)) { 60 | 61 | // Swal.fire({ 62 | // title: 'You just supported a great podcast 😻', 63 | // text: `${name} just got ${tipAmount} $NEWS.` 64 | // }) 65 | 66 | // } else { 67 | // Swal.fire({ 68 | // title: 'Enter a whole number of $NEWS to tip.' 69 | // }) 70 | // } 71 | // } 72 | // } 73 | } 74 | 75 | // const episodeCount = (count) => { 76 | // if (count == 1) { 77 | // return `${count} episode` 78 | // } else { 79 | // return `${count} episodes` 80 | // } 81 | // } 82 | 83 | return ( 84 |
    85 | 92 |
    93 |
    94 | {name} {rss ? {tipButton()} : null} 95 |
    96 |

    97 | {truncated && description.length > 52 ? description.substring(0, 52) + '...' : description} 98 |

    99 |
    100 |
    101 | ) 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/component/podcast_rss.jsx: -------------------------------------------------------------------------------- 1 | export default function PodcastRss(props) { 2 | return ( 3 | <>{props.match.params.podcastId} 4 | ) 5 | } -------------------------------------------------------------------------------- /src/component/podcast_utils.jsx: -------------------------------------------------------------------------------- 1 | import { React, useState } from 'react' 2 | import { SortAscendingIcon } from '@heroicons/react/solid' 3 | import { Transition } from '@headlessui/react' 4 | 5 | export function Dropdown({filters, selection, changeSorting}) { 6 | const [open, setOpen] = useState(false) 7 | 8 | return ( 9 |
    10 | 31 | 40 |
    41 |
      42 | {filters.map((filter, index) => ( 43 |
    • { 44 | changeSorting(index) 45 | setOpen(!open); 46 | }} className={` 47 | rounded-lg 48 | bg-base-100 49 | py-2 50 | px-4 51 | w-full 52 | inline-flex 53 | cursor-pointer 54 | ${selection === index ? 'bg-base-300' : 'hover:bg-base-200'} 55 | `}> 56 | {filter.desc} 57 |
    • 58 | ))} 59 |
    60 |
    61 |
    62 |
    63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /src/component/upload_episode.jsx: -------------------------------------------------------------------------------- 1 | import ArDB from 'ardb' 2 | import Swal from 'sweetalert2' 3 | import { useState } from 'react' 4 | import { useTranslation } from 'react-i18next' 5 | import { CONTRACT_SRC, NFT_SRC, FEE_MULTIPLIER, arweave, EPISODE_FEE_PERCENTAGE } from '../utils/arweave.js' 6 | import { processFile, calculateStorageFee, userHasEnoughAR } from '../utils/shorthands.js'; 7 | 8 | const ardb = new ArDB(arweave) 9 | 10 | export default function UploadEpisode({ podcast }) { 11 | console.log(podcast) 12 | const { t } = useTranslation() 13 | const [showUploadFee, setShowUploadFee] = useState(null) 14 | const [episodeUploading, setEpisodeUploading] = useState(false) 15 | const [uploadProgress, setUploadProgress] = useState(false) 16 | const [uploadPercentComplete, setUploadPercentComplete] = useState(0) 17 | 18 | const uploadToArweave = async (data, fileType, epObj, event, serviceFee) => { 19 | const wallet = await window.arweaveWallet.getActiveAddress(); 20 | console.log(wallet); 21 | if (!wallet) { 22 | return null; 23 | } else { 24 | const tx = await arweave.createTransaction({ data: data }); 25 | const initState = `{"issuer": "${wallet}","owner": "${wallet}","name": "${epObj.name}","ticker": "PANFT","description": "Permacast Episode from ${epObj.name}","thumbnail": "${podcast.cover}","balances": {"${wallet}": 1}}`; 26 | tx.addTag("Content-Type", fileType); 27 | tx.addTag("App-Name", "SmartWeaveContract"); 28 | tx.addTag("App-Version", "0.3.0"); 29 | tx.addTag("Contract-Src", NFT_SRC); 30 | tx.addTag("Init-State", initState); 31 | tx.addTag("Permacast-Version", "amber") 32 | tx.addTag("Thumbnail", podcast.cover); 33 | 34 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString(); 35 | 36 | await arweave.transactions.sign(tx); 37 | console.log("signed tx", tx); 38 | const uploader = await arweave.transactions.getUploader(tx); 39 | 40 | while (!uploader.isComplete) { 41 | await uploader.uploadChunk(); 42 | 43 | setUploadProgress(true) 44 | setUploadPercentComplete(uploader.pctComplete) 45 | } 46 | if (uploader.txPosted) { 47 | const newTx = await arweave.createTransaction({target:"eBYuvy8mlxUsm8JZNTpV6fisNaJt0cEbg-znvPeQ4A0", quantity: arweave.ar.arToWinston('' + serviceFee)}) 48 | console.log(newTx) 49 | await arweave.transactions.sign(newTx) 50 | console.log(newTx) 51 | await arweave.transactions.post(newTx) 52 | console.log(newTx.response) 53 | epObj.content = tx.id; 54 | 55 | console.log('txPosted:') 56 | console.log(epObj) 57 | uploadShow(epObj); 58 | event.target.reset(); 59 | Swal.fire({ 60 | title: t("uploadepisode.swal.uploadcomplete.title"), 61 | text: t("uploadepisode.swal.uploadcomplete.text"), 62 | icon: "success", 63 | customClass: "font-mono", 64 | }); 65 | setShowUploadFee(null); 66 | } else { 67 | Swal.fire( 68 | { 69 | title: t("uploadepisode.swal.uploadfailed.title"), 70 | text: t("uploadepisode.swal.uploadfailed.text"), 71 | icon: "error", 72 | customClass: "font-mono", 73 | } 74 | ); 75 | } 76 | } 77 | }; 78 | 79 | const handleEpisodeUpload = async (event) => { 80 | setEpisodeUploading(true) 81 | Swal.fire({ 82 | title: t("uploadepisode.swal.upload.title"), 83 | text: t("uploadepisode.swal.upload.text"), 84 | customClass: "font-mono", 85 | }) 86 | let epObj = {} 87 | event.preventDefault(); 88 | 89 | epObj.name = event.target.episodeName.value 90 | epObj.desc = event.target.episodeShowNotes.value 91 | epObj.index = podcast.index 92 | epObj.verto = false 93 | let episodeFile = event.target.episodeMedia.files[0] 94 | let fileType = episodeFile.type 95 | console.log(fileType) 96 | processFile(episodeFile).then((file) => { 97 | let epObjSize = JSON.stringify(epObj).length 98 | let bytes = file.byteLength + epObjSize + fileType.length 99 | calculateStorageFee(bytes).then((cost) => { 100 | const serviceFee = cost / EPISODE_FEE_PERCENTAGE; 101 | userHasEnoughAR(t, bytes, serviceFee).then((result) => { 102 | if (result === "all good") { 103 | console.log('Fee cost: ' + (serviceFee)) 104 | uploadToArweave(file, fileType, epObj, event, serviceFee) 105 | } else console.log('upload failed'); 106 | }) 107 | }) 108 | }) 109 | setEpisodeUploading(false) 110 | } 111 | 112 | 113 | const getSwcId = async () => { 114 | await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"]) 115 | let addr = await window.arweaveWallet.getActiveAddress() //await getAddrRetry() 116 | if (!addr) { 117 | await window.arweaveWallet.connect(["ACCESS_ADDRESS"]); 118 | addr = await window.arweaveWallet.getActiveAddress() 119 | } 120 | const tx = await ardb.search('transactions') 121 | .from(addr) 122 | .tag('App-Name', 'SmartWeaveContract') 123 | .tag('Permacast-Version', 'amber') 124 | .tag('Contract-Src', CONTRACT_SRC) 125 | .find() 126 | 127 | 128 | if (!tx || tx.length === 0) { 129 | Swal.fire( 130 | { 131 | title: 'Insuffucient balance or Arweave gateways are unstable. Please try again later', 132 | customClass: "font-mono", 133 | } 134 | ); 135 | } else { 136 | console.log("tx", tx) 137 | return tx[0].id 138 | } 139 | } 140 | 141 | const uploadShow = async (show) => { 142 | const theContractId = await getSwcId() 143 | console.log("theContractId", theContractId) 144 | console.log("show", show) 145 | let input = { 146 | 'function': 'addEpisode', 147 | 'pid': podcast.pid, 148 | 'name': show.name, 149 | 'desc': true, 150 | 'content': show.content 151 | } 152 | 153 | console.log(input) 154 | const contract = podcast?.newChildOf ? podcast.newChildOf : podcast.childOf; 155 | console.log("CONTRACT CHILDOF") 156 | console.log(contract) 157 | let tags = { "Contract": contract, "App-Name": "SmartWeaveAction", "App-Version": "0.3.0", "Content-Type": "text/plain", "Input": JSON.stringify(input), "Permacast-Version": "amber" } 158 | // let contract = smartweave.contract(theContractId).connect("use_wallet"); 159 | // let txId = await contract.writeInteraction(input, tags); 160 | const interaction = await arweave.createTransaction({data: show.desc}); 161 | 162 | for (let key in tags) { 163 | interaction.addTag(key, tags[key]); 164 | } 165 | 166 | interaction.reward = (+interaction.reward * FEE_MULTIPLIER).toString(); 167 | 168 | await arweave.transactions.sign(interaction); 169 | await arweave.transactions.post(interaction); 170 | console.log('addEpisode txid:'); 171 | console.log(interaction.id) 172 | } 173 | 174 | 175 | const calculateUploadFee = (file) => { 176 | console.log('fee reached') 177 | const fee = 0.0124 * ((file.size / 1024 / 1024) * 3).toFixed(4) 178 | setShowUploadFee(fee) 179 | } 180 | 181 | return ( 182 |
    183 |
    {t("uploadepisode.title")} {podcast?.podcastName}
    184 |
    185 |
    186 |
    187 | {t("uploadepisode.name")} 188 | 189 |
    190 |
    191 | {t("uploadepisode.description")} 192 | 193 |
    194 |
    195 | {t("uploadepisode.file")} 196 | calculateUploadFee(e.target.files[0])} name="episodeMedia" /> 197 |
    198 |
    199 | 200 |
    201 | {showUploadFee ? ( 202 |
    203 |

    ~${showUploadFee} {t("uploadepisode.toupload")}

    204 |
    205 | {t("uploadepisode.feeText")} 206 | 207 | ${(showUploadFee / EPISODE_FEE_PERCENTAGE).toFixed(4)} 208 | 209 |
    210 |
    211 | ) : null} 212 |

    213 | {!episodeUploading ? 214 | 220 | : 221 | 228 | } 229 | {uploadProgress &&
    {t("uploadepisode.uploaded")} {uploadPercentComplete}%
    } 230 |
    231 |
    232 |
    233 | ) 234 | } 235 | -------------------------------------------------------------------------------- /src/component/upload_show.jsx: -------------------------------------------------------------------------------- 1 | import { React, useState, useRef } from 'react'; 2 | import ArDB from 'ardb'; 3 | import { CONTRACT_SRC, FEE_MULTIPLIER, arweave, languages_en, languages_zh, categories_en, categories_zh, SHOW_FEE_AR } from '../utils/arweave.js' 4 | import { genetateFactoryState } from '../utils/initStateGen.js'; 5 | import { processFile, fetchWalletAddress, userHasEnoughAR, calculateStorageFee } from '../utils/shorthands.js'; 6 | 7 | import Swal from 'sweetalert2'; 8 | import { useTranslation } from 'react-i18next'; 9 | 10 | const ardb = new ArDB(arweave) 11 | 12 | export default function UploadShow() { 13 | 14 | let finalShowObj = {} 15 | const [show, setShow] = useState(false); 16 | const [isUploading, setIsUploading] = useState(false); 17 | const [cost, setCost] = useState(0); 18 | const podcastCoverRef = useRef() 19 | const { t, i18n } = useTranslation() 20 | const languages = i18n.language === 'zh' ? languages_zh : languages_en 21 | const categories = i18n.language === 'zh' ? categories_zh : categories_en 22 | 23 | const deployContract = async (address) => { 24 | 25 | const initialState = await genetateFactoryState(address); 26 | console.log(initialState) 27 | const tx = await arweave.createTransaction({ data: initialState }) 28 | 29 | 30 | tx.addTag("App-Name", "SmartWeaveContract") 31 | tx.addTag("App-Version", "0.3.0") 32 | tx.addTag("Contract-Src", CONTRACT_SRC) 33 | tx.addTag("Permacast-Version", "amber"); 34 | tx.addTag("Content-Type", "application/json") 35 | tx.addTag("Timestamp", Date.now()) 36 | 37 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString(); 38 | 39 | await arweave.transactions.sign(tx) 40 | await arweave.transactions.post(tx) 41 | console.log(tx) 42 | return tx.id 43 | } 44 | 45 | 46 | const uploadShow = async (show) => { 47 | Swal.fire({ 48 | title: t("uploadshow.swal.uploading.title"), 49 | timer: 2000, 50 | customClass: "font-mono", 51 | }) 52 | let contractId 53 | let addr = await fetchWalletAddress() 54 | 55 | console.log("ADDRESSS") 56 | console.log(addr) 57 | const tx = await ardb.search('transactions') 58 | .from(addr) 59 | .tag('App-Name', 'SmartWeaveContract') 60 | .tag('Permacast-Version', 'amber') 61 | .tag('Contract-Src', CONTRACT_SRC) 62 | .find(); 63 | 64 | console.log(tx) 65 | if (tx.length !== 0) { 66 | contractId = tx[0].id 67 | } 68 | if (!contractId) { 69 | console.log('not contractId - deploying new contract') 70 | contractId = await deployContract(addr) 71 | } 72 | let input = { 73 | 'function': 'createPodcast', 74 | 'name': show.name, 75 | 'contentType': 'a', 76 | 'cover': show.cover, 77 | 'lang': show.lang, 78 | 'isExplicit': show.isExplicit, 79 | 'author': show.author, 80 | 'categories': show.category, 81 | 'email': show.email 82 | } 83 | 84 | console.log(input) 85 | console.log("CONTRACT ID:") 86 | console.log(contractId); 87 | 88 | let tags = { "Contract": contractId, "App-Name": "SmartWeaveAction", "App-Version": "0.3.0", "Content-Type": "text/plain", "Input": JSON.stringify(input)}; 89 | 90 | const interaction = await arweave.createTransaction({data: show.desc}); 91 | 92 | for (const key in tags) { 93 | interaction.addTag(key, tags[key]); 94 | } 95 | 96 | interaction.reward = (+interaction.reward * FEE_MULTIPLIER).toString(); 97 | await arweave.transactions.sign(interaction); 98 | await arweave.transactions.post(interaction); 99 | if (interaction.id) { 100 | Swal.fire({ 101 | title: t("uploadshow.swal.showadded.title"), 102 | text: t("uploadshow.swal.showadded.text"), 103 | icon: 'success', 104 | customClass: "font-mono", 105 | }) 106 | console.log("INTERACTION.ID") 107 | console.log(interaction.id) 108 | } else { 109 | alert('An error occured.') 110 | } 111 | } 112 | 113 | const uploadToArweave = async (data, fileType, showObj) => { 114 | console.log('made it here, data is') 115 | console.log(data) 116 | arweave.createTransaction({ data: data }).then((tx) => { 117 | tx.addTag("Content-Type", fileType); 118 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString(); 119 | console.log('created') 120 | arweave.transactions.sign(tx).then(() => { 121 | console.log('signed') 122 | arweave.transactions.post(tx).then((response) => { 123 | console.log(response) 124 | if (response.statusText === "OK") { 125 | arweave.createTransaction({target:"eBYuvy8mlxUsm8JZNTpV6fisNaJt0cEbg-znvPeQ4A0", quantity: arweave.ar.arToWinston('' + SHOW_FEE_AR)}).then((tx) => { 126 | arweave.transactions.sign(tx).then(() => { 127 | console.log(tx) 128 | arweave.transactions.post(tx).then((response) => { 129 | console.log(response) 130 | setIsUploading(false) 131 | }) 132 | }) 133 | }) 134 | showObj.cover = tx.id 135 | finalShowObj = showObj; 136 | console.log(finalShowObj) 137 | uploadShow(finalShowObj) 138 | setShow(false) 139 | 140 | } else { 141 | Swal.fire({ 142 | title: t("uploadshow.swal.uploadfailed.title"), 143 | text: t("uploadshow.swal.uploadfailed.text"), 144 | icon: 'danger', 145 | customClass: "font-mono", 146 | }) 147 | } 148 | }); 149 | }); 150 | }); 151 | } 152 | 153 | const resetPodcastCover = () => { 154 | podcastCoverRef.current.value = "" 155 | Swal.fire({ 156 | text: t("uploadshow.swal.reset.text"), 157 | icon: 'warning', 158 | confirmButtonText: 'Continue', 159 | customClass: "font-mono", 160 | }) 161 | } 162 | 163 | const isPodcastCoverSquared = (event) => { 164 | if (event.target.files.length !== 0) { 165 | const podcastCoverImage = new Image() 166 | podcastCoverImage.src = window.URL.createObjectURL(event.target.files[0]) 167 | podcastCoverImage.onload = () => { 168 | calculateStorageFee(event.target.files[0].size).then((fee) => { 169 | setCost(fee) 170 | }) 171 | if (podcastCoverImage.width !== podcastCoverImage.height) { 172 | resetPodcastCover() 173 | } 174 | } 175 | } 176 | } 177 | 178 | const handleShowUpload = async (event) => { 179 | 180 | event.preventDefault() 181 | // extract attrs from form 182 | const showObj = {} 183 | const podcastName = event.target.podcastName.value 184 | const podcastDescription = event.target.podcastDescription.value 185 | const podcastCover = event.target.podcastCover.files[0] 186 | const podcastAuthor = event.target.podcastAuthor.value 187 | const podcastEmail = event.target.podcastEmail.value 188 | const podcastCategory = event.target.podcastCategory.value 189 | const podcastExplicit = event.target.podcastExplicit.checked ? "yes" : "no" 190 | const podcastLanguage = event.target.podcastLanguage.value 191 | const coverFileType = podcastCover.type 192 | // add attrs to input for SWC 193 | showObj.name = podcastName 194 | showObj.desc = podcastDescription 195 | showObj.author = podcastAuthor 196 | showObj.email = podcastEmail 197 | showObj.category = podcastCategory 198 | showObj.isExplicit = podcastExplicit 199 | showObj.lang = podcastLanguage 200 | // upload cover, send all to Arweave 201 | let cover = await processFile(podcastCover) 202 | let showObjSize = JSON.stringify(showObj).length 203 | let bytes = cover.byteLength + showObjSize + coverFileType.length 204 | 205 | setIsUploading(true) 206 | 207 | if (await userHasEnoughAR(t, bytes, SHOW_FEE_AR) === "all good") { 208 | await uploadToArweave(cover, coverFileType, showObj) 209 | } else { 210 | console.log('upload failed') 211 | setIsUploading(false) 212 | }; 213 | } 214 | 215 | const languageOptions = () => { 216 | const langsArray = Object.entries(languages); 217 | let optionsArr = [] 218 | for (let lang of langsArray) { 219 | optionsArr.push( 220 | 221 | ) 222 | } 223 | return optionsArr 224 | } 225 | 226 | const categoryOptions = () => { 227 | let optionsArr = [] 228 | for (let i in categories) { 229 | optionsArr.push( 230 | 231 | ) 232 | } 233 | return optionsArr 234 | } 235 | 236 | return ( 237 | <> 238 | 239 | 240 |
    241 |
    242 |
    243 |

    {t("uploadshow.title")}

    244 |

    {t("uploadshow.label")}

    245 |
    246 |
    247 |
    248 |
    249 | {t("uploadshow.name")} 250 | 251 |
    252 |
    253 | {t("uploadshow.description")} 254 |