From 7a630507fbc1c8587e43a91006b2bccaa6e90d7e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Aug 2025 12:45:28 +0600 Subject: [PATCH 01/19] website: add downloads pages --- website/package.json | 9 +- website/pnpm-lock.yaml | 340 ++++++++++++++++++ website/src/layouts/RootLayout.astro | 47 ++- .../src/modules/downloads/download-item.astro | 33 ++ .../modules/downloads/older/release-body.tsx | 39 ++ .../src/modules/downloads/older/releases.tsx | 178 +++++++++ website/src/modules/root/supporters.tsx | 80 +++++ website/src/pages/downloads/index.astro | 50 ++- .../src/pages/downloads/nightly/index.astro | 47 +++ website/src/pages/downloads/older/index.astro | 12 + .../src/pages/downloads/packages/index.astro | 5 + website/src/pages/index.astro | 75 +++- website/src/styles/global.css | 1 + website/tsconfig.json | 2 + 14 files changed, 913 insertions(+), 5 deletions(-) create mode 100644 website/src/modules/downloads/download-item.astro create mode 100644 website/src/modules/downloads/older/release-body.tsx create mode 100644 website/src/modules/downloads/older/releases.tsx create mode 100644 website/src/modules/root/supporters.tsx create mode 100644 website/src/pages/downloads/nightly/index.astro create mode 100644 website/src/pages/downloads/older/index.astro create mode 100644 website/src/pages/downloads/packages/index.astro diff --git a/website/package.json b/website/package.json index 3a72b01f..8a04545c 100644 --- a/website/package.json +++ b/website/package.json @@ -10,18 +10,25 @@ }, "dependencies": { "@astrojs/react": "^4.3.0", + "@octokit/rest": "^22.0.0", "@skeletonlabs/skeleton-react": "^1.2.4", "@tailwindcss/vite": "^4.1.11", "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "astro": "^5.12.8", + "date-fns": "^4.1.0", + "markdown-it": "^14.1.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", + "sanitize-html": "^2.17.0", "tailwindcss": "^4.1.11", "usehooks-ts": "^3.1.1" }, "devDependencies": { - "@skeletonlabs/skeleton": "^3.1.7" + "@skeletonlabs/skeleton": "^3.1.7", + "@tailwindcss/typography": "^0.5.16", + "@types/markdown-it": "^14.1.2", + "@types/sanitize-html": "^2.16.0" } } \ No newline at end of file diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 20766d76..64025873 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@astrojs/react': specifier: ^4.3.0 version: 4.3.0(@types/node@24.1.0)(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(jiti@2.5.1)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@octokit/rest': + specifier: ^22.0.0 + version: 22.0.0 '@skeletonlabs/skeleton-react': specifier: ^1.2.4 version: 1.2.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -26,6 +29,12 @@ importers: astro: specifier: ^5.12.8 version: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 react: specifier: ^19.1.1 version: 19.1.1 @@ -35,6 +44,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.1.1) + sanitize-html: + specifier: ^2.17.0 + version: 2.17.0 tailwindcss: specifier: ^4.1.11 version: 4.1.11 @@ -45,6 +57,15 @@ importers: '@skeletonlabs/skeleton': specifier: ^3.1.7 version: 3.1.7(tailwindcss@4.1.11) + '@tailwindcss/typography': + specifier: ^0.5.16 + version: 0.5.16(tailwindcss@4.1.11) + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@types/sanitize-html': + specifier: ^2.16.0 + version: 2.16.0 packages: @@ -445,6 +466,58 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@octokit/auth-token@6.0.0': + resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} + engines: {node: '>= 20'} + + '@octokit/core@7.0.3': + resolution: {integrity: sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ==} + engines: {node: '>= 20'} + + '@octokit/endpoint@11.0.0': + resolution: {integrity: sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==} + engines: {node: '>= 20'} + + '@octokit/graphql@9.0.1': + resolution: {integrity: sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==} + engines: {node: '>= 20'} + + '@octokit/openapi-types@25.1.0': + resolution: {integrity: sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==} + + '@octokit/plugin-paginate-rest@13.1.1': + resolution: {integrity: sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@6.0.0': + resolution: {integrity: sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@16.0.0': + resolution: {integrity: sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g==} + engines: {node: '>= 20'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@7.0.0': + resolution: {integrity: sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==} + engines: {node: '>= 20'} + + '@octokit/request@10.0.3': + resolution: {integrity: sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA==} + engines: {node: '>= 20'} + + '@octokit/rest@22.0.0': + resolution: {integrity: sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==} + engines: {node: '>= 20'} + + '@octokit/types@14.1.0': + resolution: {integrity: sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -680,6 +753,11 @@ packages: resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.16': + resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.11': resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} peerDependencies: @@ -709,9 +787,18 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -729,6 +816,9 @@ packages: '@types/react@19.1.9': resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} + '@types/sanitize-html@2.16.0': + resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -874,6 +964,9 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + before-after-hook@4.0.0: + resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==} + blob-to-buffer@1.2.9: resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} @@ -984,6 +1077,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -996,6 +1092,10 @@ packages: decode-named-character-reference@1.2.0: resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} @@ -1030,6 +1130,19 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -1047,6 +1160,10 @@ packages: resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} engines: {node: '>=10.13.0'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -1063,6 +1180,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -1079,6 +1200,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@3.0.0: + resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1158,6 +1282,9 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -1188,6 +1315,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-wsl@3.1.0: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} @@ -1285,9 +1416,21 @@ packages: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lodash.castarray@4.4.0: + resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1303,6 +1446,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -1348,6 +1495,9 @@ packages: mdn-data@2.12.2: resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -1519,6 +1669,9 @@ packages: parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -1533,6 +1686,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -1554,6 +1711,10 @@ packages: proxy-compare@3.0.1: resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -1636,6 +1797,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + sanitize-html@2.17.0: + resolution: {integrity: sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA==} + scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -1743,6 +1907,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + ufo@1.6.1: resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} @@ -1794,6 +1961,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.3: + resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + unstorage@1.16.1: resolution: {integrity: sha512-gdpZ3guLDhz+zWIlYP1UwQ259tG5T5vYRzDaHMkQ1bBY1SQPutvZnrRjTFaWUUpseErJIgAZS51h6NOcZVZiqQ==} peerDependencies: @@ -1865,6 +2035,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -2358,6 +2531,68 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 + '@octokit/auth-token@6.0.0': {} + + '@octokit/core@7.0.3': + dependencies: + '@octokit/auth-token': 6.0.0 + '@octokit/graphql': 9.0.1 + '@octokit/request': 10.0.3 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + before-after-hook: 4.0.0 + universal-user-agent: 7.0.3 + + '@octokit/endpoint@11.0.0': + dependencies: + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/graphql@9.0.1': + dependencies: + '@octokit/request': 10.0.3 + '@octokit/types': 14.1.0 + universal-user-agent: 7.0.3 + + '@octokit/openapi-types@25.1.0': {} + + '@octokit/plugin-paginate-rest@13.1.1(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/types': 14.1.0 + + '@octokit/plugin-request-log@6.0.0(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + + '@octokit/plugin-rest-endpoint-methods@16.0.0(@octokit/core@7.0.3)': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/types': 14.1.0 + + '@octokit/request-error@7.0.0': + dependencies: + '@octokit/types': 14.1.0 + + '@octokit/request@10.0.3': + dependencies: + '@octokit/endpoint': 11.0.0 + '@octokit/request-error': 7.0.0 + '@octokit/types': 14.1.0 + fast-content-type-parse: 3.0.0 + universal-user-agent: 7.0.3 + + '@octokit/rest@22.0.0': + dependencies: + '@octokit/core': 7.0.3 + '@octokit/plugin-paginate-rest': 13.1.1(@octokit/core@7.0.3) + '@octokit/plugin-request-log': 6.0.0(@octokit/core@7.0.3) + '@octokit/plugin-rest-endpoint-methods': 16.0.0(@octokit/core@7.0.3) + + '@octokit/types@14.1.0': + dependencies: + '@octokit/openapi-types': 25.1.0 + '@oslojs/encoding@1.1.0': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -2553,6 +2788,14 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.11)': + dependencies: + lodash.castarray: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.merge: 4.6.2 + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.11 + '@tailwindcss/vite@4.1.11(vite@6.3.5(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1))': dependencies: '@tailwindcss/node': 4.1.11 @@ -2595,10 +2838,19 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} + '@types/ms@2.1.0': {} '@types/nlcst@2.0.3': @@ -2617,6 +2869,10 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/sanitize-html@2.16.0': + dependencies: + htmlparser2: 8.0.2 + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.3.0': {} @@ -2928,6 +3184,8 @@ snapshots: base64-js@1.5.1: {} + before-after-hook@4.0.0: {} + blob-to-buffer@1.2.9: {} boxen@8.0.1: @@ -3029,6 +3287,8 @@ snapshots: csstype@3.1.3: {} + date-fns@4.1.0: {} + debug@4.4.1: dependencies: ms: 2.1.3 @@ -3037,6 +3297,8 @@ snapshots: dependencies: character-entities: 2.0.2 + deepmerge@4.3.1: {} + defu@6.1.4: {} dequal@2.0.3: {} @@ -3061,6 +3323,24 @@ snapshots: dlv@1.1.3: {} + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dset@3.1.4: {} electron-to-chromium@1.5.194: {} @@ -3074,6 +3354,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.2 + entities@4.5.0: {} + entities@6.0.1: {} es-module-lexer@1.7.0: {} @@ -3109,6 +3391,8 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} estree-walker@2.0.2: {} @@ -3121,6 +3405,8 @@ snapshots: extend@3.0.2: {} + fast-content-type-parse@3.0.0: {} + fast-deep-equal@3.1.3: {} fdir@6.4.6(picomatch@4.0.3): @@ -3260,6 +3546,13 @@ snapshots: html-void-elements@3.0.0: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-cache-semantics@4.2.0: {} import-meta-resolve@4.1.0: {} @@ -3279,6 +3572,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-wsl@3.1.0: dependencies: is-inside-container: 1.0.0 @@ -3344,8 +3639,18 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.1 lightningcss-win32-x64-msvc: 1.30.1 + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lodash.castarray@4.4.0: {} + lodash.debounce@4.0.8: {} + lodash.isplainobject@4.0.6: {} + + lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} lru-cache@10.4.3: {} @@ -3364,6 +3669,15 @@ snapshots: '@babel/types': 7.28.2 source-map-js: 1.2.1 + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} mdast-util-definitions@6.0.0: @@ -3488,6 +3802,8 @@ snapshots: mdn-data@2.12.2: {} + mdurl@2.0.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -3751,6 +4067,8 @@ snapshots: unist-util-visit-children: 3.0.0 vfile: 6.0.3 + parse-srcset@1.0.2: {} + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -3761,6 +4079,11 @@ snapshots: picomatch@4.0.3: {} + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + postcss@8.5.6: dependencies: nanoid: 3.3.11 @@ -3780,6 +4103,8 @@ snapshots: proxy-compare@3.0.1: {} + punycode.js@2.3.1: {} + radix3@1.1.2: {} react-dom@19.1.1(react@19.1.1): @@ -3926,6 +4251,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.46.2 fsevents: 2.3.3 + sanitize-html@2.17.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + scheduler@0.26.0: {} semver@6.3.1: {} @@ -4046,6 +4380,8 @@ snapshots: typescript@5.9.2: {} + uc.micro@2.1.0: {} + ufo@1.6.1: {} ultrahtml@1.6.0: {} @@ -4122,6 +4458,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.3: {} + unstorage@1.16.1: dependencies: anymatch: 3.1.3 @@ -4144,6 +4482,8 @@ snapshots: lodash.debounce: 4.0.8 react: 19.1.1 + util-deprecate@1.0.2: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/website/src/layouts/RootLayout.astro b/website/src/layouts/RootLayout.astro index e8af9eca..a0d2dfc3 100644 --- a/website/src/layouts/RootLayout.astro +++ b/website/src/layouts/RootLayout.astro @@ -1,4 +1,5 @@ --- +import { FaGithub } from "react-icons/fa6"; import "../styles/global.css"; import TopBar from "~/components/navigation/TopBar.astro"; --- @@ -11,10 +12,52 @@ import TopBar from "~/components/navigation/TopBar.astro"; Spotube + + + + + + - - +
+ + +
+ diff --git a/website/src/modules/downloads/download-item.astro b/website/src/modules/downloads/download-item.astro new file mode 100644 index 00000000..a85539a7 --- /dev/null +++ b/website/src/modules/downloads/download-item.astro @@ -0,0 +1,33 @@ +--- +import type { IconType } from "react-icons"; + +interface Props { + links: Record; +} + +const { links } = Astro.props; +--- + +
+ { + Object.entries(links).map((link) => { + return ( + +
+ {link[1][1].map((icon) => { + const Icon = icon; + return ; + })} +

+ {link[1][2]} +

+
+

{link[0]}

+
+ ); + }) + } +
diff --git a/website/src/modules/downloads/older/release-body.tsx b/website/src/modules/downloads/older/release-body.tsx new file mode 100644 index 00000000..1f80b81e --- /dev/null +++ b/website/src/modules/downloads/older/release-body.tsx @@ -0,0 +1,39 @@ +import type { RestEndpointMethodTypes } from "@octokit/rest"; +import { LuBook, LuChevronDown, LuChevronUp } from "react-icons/lu"; +import markdownIt from "markdown-it"; +import sanitizeHtml from "sanitize-html"; + +interface Props { + release: RestEndpointMethodTypes["repos"]["getReleaseByTag"]["response"]["data"]; +} + +export default function ReleaseBody({ release }: Props) { + const summary = "Release Notes & Changelogs"; + const body = release.body ?? "No release notes available."; + + const md = markdownIt({ + html: true, + linkify: true, + typographer: true, + }); + + const sanitizedBody = sanitizeHtml(md.render(body)); + return (
+ + + {summary} + + + + + + + + + +
+
) +} \ No newline at end of file diff --git a/website/src/modules/downloads/older/releases.tsx b/website/src/modules/downloads/older/releases.tsx new file mode 100644 index 00000000..2aa58b60 --- /dev/null +++ b/website/src/modules/downloads/older/releases.tsx @@ -0,0 +1,178 @@ +import { formatDistanceToNow, formatRelative } from "date-fns"; +import ReleaseBody from "~/modules/downloads/older/release-body"; +import RootLayout from "~/layouts/RootLayout.astro"; +import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest"; +import { + FaAndroid, + FaApple, + FaGit, + FaGooglePlay, + FaLinux, + FaWindows, +} from "react-icons/fa6"; +import type { IconType } from "react-icons"; +import { useEffect, useState } from "react"; + +function getIcon(assetUrl: string) { + assetUrl = assetUrl.toLowerCase(); + if (assetUrl.includes("linux")) return FaLinux; + if (assetUrl.includes("windows")) return FaWindows; + if (assetUrl.includes("mac")) return FaApple; + if (assetUrl.includes("android")) return FaAndroid; + if (assetUrl.includes("playstore")) return FaGooglePlay; + if (assetUrl.includes("ios")) return FaApple; + + return FaGit; +} + +function formatName(assetName: string) { + // format the assetName to be + // {OS} ({package extension}) + + const lowerCasedAssetName = assetName.toLowerCase(); + const extension = assetName.split(".").at(-1); + + if (lowerCasedAssetName.includes("linux")) { + if (lowerCasedAssetName.includes("aarch64")) { + return [`Linux`, extension, `ARM64`] + } + return [`Linux`, extension, `x64`] + }; + if (lowerCasedAssetName.includes("windows")) return [`Windows`, extension]; + if (lowerCasedAssetName.includes("mac")) return [`macOS`, extension]; + if ( + lowerCasedAssetName.includes("android") || + lowerCasedAssetName.includes("playstore") + ) + return [`Android`, extension]; + if (lowerCasedAssetName.includes("ios")) return [`iOS`, extension]; + + return [assetName.replace(`.${extension}`, ""), extension]; +} + +type OctokitAsset = + RestEndpointMethodTypes["repos"]["listReleases"]["response"]["data"][0]["assets"][0]; + +function groupByOS(downloads: OctokitAsset[]) { + return downloads.reduce( + (acc, val) => { + const lowName = val.name.toLowerCase(); + + if (lowName.includes("android") || lowName.includes("playstore")) + acc["android"] = [...(acc.android ?? []), val]; + if (lowName.includes("linux")) + acc["linux"] = [...(acc["linux"] ?? []), val]; + if (lowName.includes("windows")) + acc["windows"] = [...(acc["windows"] ?? []), val]; + if (lowName.includes("ios")) acc["ios"] = [...(acc["ios"] ?? []), val]; + if (lowName.includes("mac")) acc["mac"] = [...(acc["mac"] ?? []), val]; + + return acc; + }, + {} as Record< + "android" | "ios" | "mac" | "linux" | "windows", + OctokitAsset[] + > + ); +} + +const icons: Record = { + android: [FaAndroid, "#3DDC84"], + mac: [FaApple, ""], + ios: [FaApple, ""], + linux: [FaLinux, "#000000"], + windows: [FaWindows, "#0078D7"], +}; + +export default function ReleasesSection() { + const github = new Octokit(); + + const [releases, setReleases] = useState([]); + + useEffect(() => { + github.repos.listReleases({ + owner: "KRTirtho", + repo: "spotube", + + }).then((res) => { + setReleases(res.data); + }) + + }, []) + + return <> + { + releases.map((release) => { + return ( +
+

+ {release.tag_name} + + ( + {formatDistanceToNow(release.published_at ?? new Date(), { + addSuffix: true, + })} + ) + +

+ +
+ {Object.entries(groupByOS(release.assets)).map( + ([osName, assets]) => { + const Icon = icons[osName][0]; + + return ( +
+
+ + {osName} +
+
+ {assets.map((asset) => { + const Icon = getIcon(asset.browser_download_url); + const formattedName = formatName(asset.name); + + return ( + + + + ); + })} +
+
+ ); + } + )} +
+ +
+
+ ); + }) + } + +} \ No newline at end of file diff --git a/website/src/modules/root/supporters.tsx b/website/src/modules/root/supporters.tsx new file mode 100644 index 00000000..56e8479b --- /dev/null +++ b/website/src/modules/root/supporters.tsx @@ -0,0 +1,80 @@ +import { useEffect, useState } from "react"; +import { Avatar } from "@skeletonlabs/skeleton-react"; + +interface Member { + MemberId: number; + createdAt: string; + type: string; + role: string; + isActive: boolean; + totalAmountDonated: number; + currency?: string; + lastTransactionAt: string; + lastTransactionAmount: number; + profile: string; + name: string; + company?: string; + description?: string; + image?: string; + email?: string; + twitter?: string; + github?: string; + website?: string; + tier?: string; +} + +const formatter = new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + compactDisplay: 'short', + maximumFractionDigits: 0 +}); + + +export function Supporters() { + const [members, setMembers] = useState([]); + + useEffect(() => { + // Fetch members data from an API or other source + async function fetchMembers() { + const res = await fetch('https://opencollective.com/spotube/members/all.json'); + const members = (await res.json()) as Member[]; + setMembers( + members + .filter((m) => m.totalAmountDonated > 0) + .sort((a, b) => b.totalAmountDonated - a.totalAmountDonated) + ); + }; + + fetchMembers(); + }, []); + + + return ; +} \ No newline at end of file diff --git a/website/src/pages/downloads/index.astro b/website/src/pages/downloads/index.astro index 26937f1e..8678105f 100644 --- a/website/src/pages/downloads/index.astro +++ b/website/src/pages/downloads/index.astro @@ -1,5 +1,53 @@ --- +import type { IconType } from "react-icons"; +import { LuDownload, LuHistory, LuPackage, LuSparkles } from "react-icons/lu"; +import { extendedDownloadLinks } from "~/collections/app"; import RootLayout from "~/layouts/RootLayout.astro"; +import DownloadItems from "~/modules/downloads/download-item.astro"; + +const otherDownloads: [string, string, IconType][] = [ + ["/downloads/packages", "CLI Packages Managers", LuPackage], + ["/downloads/older", "Older Versions", LuHistory], + ["/downloads/nightly", "Nightly Builds", LuSparkles], +]; --- - + +
+

+ Download + +

+

+
Spotube is available for every platform
+
+ + +
+ +
+ +
+ +

Other Downloads

+

+
+ { + otherDownloads.map((download) => { + const Icon = download[2]; + + return ( + +
+ +
{download[1]}
+
+
+ ); + }) + } +
+
+ +
+
diff --git a/website/src/pages/downloads/nightly/index.astro b/website/src/pages/downloads/nightly/index.astro new file mode 100644 index 00000000..0e79c8c6 --- /dev/null +++ b/website/src/pages/downloads/nightly/index.astro @@ -0,0 +1,47 @@ +--- +import { LuBug, LuSparkles, LuTriangleAlert } from "react-icons/lu"; +import { extendedNightlyDownloadLinks } from "~/collections/app"; +import RootLayout from "~/layouts/RootLayout.astro"; +import DownloadItems from "~/modules/downloads/download-item.astro"; +--- + + +
+

+ Nightly Downloads + +

+

+ +
+ +

Following are the new v5 Nightly versions:

+ + +
+ +
+
+
diff --git a/website/src/pages/downloads/older/index.astro b/website/src/pages/downloads/older/index.astro new file mode 100644 index 00000000..cdcc62a6 --- /dev/null +++ b/website/src/pages/downloads/older/index.astro @@ -0,0 +1,12 @@ +--- +import RootLayout from "~/layouts/RootLayout.astro"; +import ReleasesSection from "~/modules/downloads/older/releases"; +--- + + +
+
+ +
+
+
diff --git a/website/src/pages/downloads/packages/index.astro b/website/src/pages/downloads/packages/index.astro new file mode 100644 index 00000000..26937f1e --- /dev/null +++ b/website/src/pages/downloads/packages/index.astro @@ -0,0 +1,5 @@ +--- +import RootLayout from "~/layouts/RootLayout.astro"; +--- + + diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 50fea5ea..454efbcd 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -1,5 +1,78 @@ --- +import { FaAndroid, FaApple, FaLinux, FaWindows } from "react-icons/fa6"; import RootLayout from "../layouts/RootLayout.astro"; +import { LuHeart } from "react-icons/lu"; +import { Supporters } from "~/modules/root/supporters"; --- - + +
+
+
+

Spotube

+
+

+ A cross-platform Extensible open-source Music Streaming platform +
+ + + + +
+

+

+ And it's not + built with Electron (web technologies) +

+
+
+ + HackerNews + + +
+
+
+ +
+ +
+ +
+

+ Supporters + +

+

+ We are grateful for the support of individuals and organizations who + have made Spotube possible. +

+
+ + Open Collective + +
+ +
+
+ +
+
diff --git a/website/src/styles/global.css b/website/src/styles/global.css index 564d485f..29df24d9 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@plugin "@tailwindcss/typography"; @source '../../node_modules/@skeletonlabs/skeleton-react/dist'; diff --git a/website/tsconfig.json b/website/tsconfig.json index d43217a5..5d4aa51d 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -16,5 +16,7 @@ ], }, "baseUrl": "./src", + "module": "node16", + "moduleResolution": "node16" } } \ No newline at end of file From ee7d0cfeb5b92bbaeae479eb28fca5bc59d5c91f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Aug 2025 13:04:22 +0600 Subject: [PATCH 02/19] website: markdown mdx support and wrap up other pages --- website/astro.config.mjs | 4 +- website/package.json | 1 + website/pnpm-lock.yaml | 510 ++++++++++++++++++ website/src/layouts/MarkdownLayout.astro | 3 + website/src/pages/about/index.astro | 25 +- .../src/pages/downloads/packages/index.astro | 5 - .../src/pages/downloads/packages/index.mdx | 68 +++ 7 files changed, 609 insertions(+), 7 deletions(-) create mode 100644 website/src/layouts/MarkdownLayout.astro delete mode 100644 website/src/pages/downloads/packages/index.astro create mode 100644 website/src/pages/downloads/packages/index.mdx diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 9512ab26..dd00c5fd 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,11 +5,13 @@ import tailwindcss from '@tailwindcss/vite'; import react from '@astrojs/react'; +import mdx from '@astrojs/mdx'; + // https://astro.build/config export default defineConfig({ vite: { plugins: [tailwindcss()] }, - integrations: [react()] + integrations: [react(), mdx()] }); \ No newline at end of file diff --git a/website/package.json b/website/package.json index 8a04545c..1afec40e 100644 --- a/website/package.json +++ b/website/package.json @@ -9,6 +9,7 @@ "astro": "astro" }, "dependencies": { + "@astrojs/mdx": "^4.3.3", "@astrojs/react": "^4.3.0", "@octokit/rest": "^22.0.0", "@skeletonlabs/skeleton-react": "^1.2.4", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 64025873..39774bf8 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@astrojs/mdx': + specifier: ^4.3.3 + version: 4.3.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)) '@astrojs/react': specifier: ^4.3.0 version: 4.3.0(@types/node@24.1.0)(@types/react-dom@19.1.7(@types/react@19.1.9))(@types/react@19.1.9)(jiti@2.5.1)(lightningcss@1.30.1)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -82,6 +85,12 @@ packages: '@astrojs/markdown-remark@6.3.5': resolution: {integrity: sha512-MiR92CkE2BcyWf3b86cBBw/1dKiOH0qhLgXH2OXA6cScrrmmks1Rr4Tl0p/lFpvmgQQrP54Pd1uidJfmxGrpWQ==} + '@astrojs/mdx@4.3.3': + resolution: {integrity: sha512-+9+xGP2TBXxcm84cpiq4S9JbuHOHM1fcvREfqW7VHxlUyfUQPByoJ9YYliqHkLS6BMzG+O/+o7n8nguVhuEv4w==} + engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} + peerDependencies: + astro: ^5.0.0 + '@astrojs/prism@3.3.0': resolution: {integrity: sha512-q8VwfU/fDZNoDOf+r7jUnMC2//H2l0TuQ6FkGJL8vD8nw/q5KiL3DS1KKBI3QhI9UQhpJ5dc7AtqfbXWuOgLCQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} @@ -466,6 +475,9 @@ packages: '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + '@octokit/auth-token@6.0.0': resolution: {integrity: sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==} engines: {node: '>= 20'} @@ -778,6 +790,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -799,6 +814,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -819,6 +837,9 @@ packages: '@types/sanitize-html@2.16.0': resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -912,6 +933,11 @@ packages: '@zag-js/utils@1.21.1': resolution: {integrity: sha512-YXkmKoQilMaCQolPnfyyF1xIDOkvSGUQ3RfNlGCxcfnXZeuLWhV/9kalXql5OhtDMChaSeI5IgoV6zH2KEeN0A==} + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} @@ -946,6 +972,10 @@ packages: array-iterate@2.0.1: resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + astro@5.12.8: resolution: {integrity: sha512-KkJ7FR+c2SyZYlpakm48XBiuQcRsrVtdjG5LN5an0givI/tLik+ePJ4/g3qrAVhYMjJOxBA2YgFQxANPiWB+Mw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} @@ -1005,6 +1035,9 @@ packages: character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1029,6 +1062,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1171,6 +1207,12 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + esbuild@0.25.8: resolution: {integrity: sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==} engines: {node: '>=18'} @@ -1188,6 +1230,24 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} @@ -1261,9 +1321,15 @@ packages: hast-util-raw@9.1.0: resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} @@ -1291,12 +1357,24 @@ packages: import-meta-resolve@4.1.0: resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1306,6 +1384,9 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -1446,6 +1527,10 @@ packages: magicast@0.3.5: resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -1480,6 +1565,18 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -1522,12 +1619,30 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} micromark-factory-label@2.0.1: resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + micromark-factory-space@2.0.1: resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} @@ -1558,6 +1673,9 @@ packages: micromark-util-encode@2.0.1: resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + micromark-util-html-tag-name@2.0.1: resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} @@ -1666,6 +1784,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-latin@7.0.0: resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} @@ -1740,6 +1861,20 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -1755,6 +1890,9 @@ packages: rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-stringify@10.0.1: resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} @@ -1764,6 +1902,9 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -1833,6 +1974,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -1855,6 +2000,12 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} + style-to-js@1.1.17: + resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + + style-to-object@1.0.9: + resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + tailwindcss@4.1.11: resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} @@ -1943,6 +2094,9 @@ packages: unist-util-modify-children@4.0.0: resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} @@ -2196,6 +2350,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@astrojs/mdx@4.3.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2))': + dependencies: + '@astrojs/markdown-remark': 6.3.5 + '@mdx-js/mdx': 3.1.0(acorn@8.15.0) + acorn: 8.15.0 + astro: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) + es-module-lexer: 1.7.0 + estree-util-visit: 2.0.0 + hast-util-to-html: 9.0.5 + kleur: 4.1.5 + rehype-raw: 7.0.0 + remark-gfm: 4.0.1 + remark-smartypants: 3.0.2 + source-map: 0.7.6 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + '@astrojs/prism@3.3.0': dependencies: prismjs: 1.30.0 @@ -2531,6 +2704,36 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.4 + '@mdx-js/mdx@3.1.0(acorn@8.15.0)': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.15.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + '@octokit/auth-token@6.0.0': {} '@octokit/core@7.0.3': @@ -2828,6 +3031,10 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + '@types/estree@1.0.8': {} '@types/fontkit@2.0.8': @@ -2851,6 +3058,8 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} + '@types/ms@2.1.0': {} '@types/nlcst@2.0.3': @@ -2873,6 +3082,8 @@ snapshots: dependencies: htmlparser2: 8.0.2 + '@types/unist@2.0.11': {} + '@types/unist@3.0.3': {} '@ungap/structured-clone@1.3.0': {} @@ -3052,6 +3263,10 @@ snapshots: '@zag-js/utils@1.21.1': {} + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} ansi-align@3.0.1: @@ -3075,6 +3290,8 @@ snapshots: array-iterate@2.0.1: {} + astring@1.9.0: {} + astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2): dependencies: '@astrojs/compiler': 2.12.2 @@ -3224,6 +3441,8 @@ snapshots: character-entities@2.0.2: {} + character-reference-invalid@2.0.1: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -3238,6 +3457,8 @@ snapshots: clsx@2.1.1: {} + collapse-white-space@2.1.0: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3360,6 +3581,20 @@ snapshots: es-module-lexer@1.7.0: {} + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.15.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + esbuild@0.25.8: optionalDependencies: '@esbuild/aix-ppc64': 0.25.8 @@ -3395,6 +3630,35 @@ snapshots: escape-string-regexp@5.0.0: {} + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + estree-walker@2.0.2: {} estree-walker@3.0.3: @@ -3499,6 +3763,27 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -3513,6 +3798,26 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.17 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + hast-util-to-parse5@8.0.0: dependencies: '@types/hast': 3.0.4 @@ -3557,15 +3862,28 @@ snapshots: import-meta-resolve@4.1.0: {} + inline-style-parser@0.2.4: {} + iron-webcrypto@1.2.1: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + is-arrayish@0.3.2: optional: true + is-decimal@2.0.1: {} + is-docker@3.0.0: {} is-fullwidth-code-point@3.0.0: {} + is-hexadecimal@2.0.1: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -3669,6 +3987,8 @@ snapshots: '@babel/types': 7.28.2 source-map-js: 1.2.1 + markdown-extensions@2.0.0: {} + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -3767,6 +4087,55 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -3881,6 +4250,57 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -3894,6 +4314,18 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + micromark-factory-space@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -3946,6 +4378,16 @@ snapshots: micromark-util-encode@2.0.1: {} + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + micromark-util-html-tag-name@2.0.1: {} micromark-util-normalize-identifier@2.0.1: @@ -4058,6 +4500,16 @@ snapshots: pako@0.2.9: {} + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.2.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + parse-latin@7.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -4122,6 +4574,35 @@ snapshots: readdirp@4.1.2: {} + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -4144,6 +4625,14 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + rehype-stringify@10.0.1: dependencies: '@types/hast': 3.0.4 @@ -4168,6 +4657,13 @@ snapshots: transitivePeerDependencies: - supports-color + remark-mdx@3.1.0: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -4315,6 +4811,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} string-width@4.2.3: @@ -4342,6 +4840,14 @@ snapshots: dependencies: ansi-regex: 6.1.0 + style-to-js@1.1.17: + dependencies: + style-to-object: 1.0.9 + + style-to-object@1.0.9: + dependencies: + inline-style-parser: 0.2.4 + tailwindcss@4.1.11: {} tapable@2.2.2: {} @@ -4430,6 +4936,10 @@ snapshots: '@types/unist': 3.0.3 array-iterate: 2.0.1 + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-position@5.0.0: dependencies: '@types/unist': 3.0.3 diff --git a/website/src/layouts/MarkdownLayout.astro b/website/src/layouts/MarkdownLayout.astro new file mode 100644 index 00000000..5841ba47 --- /dev/null +++ b/website/src/layouts/MarkdownLayout.astro @@ -0,0 +1,3 @@ +
+ +
diff --git a/website/src/pages/about/index.astro b/website/src/pages/about/index.astro index 26937f1e..8b9d2360 100644 --- a/website/src/pages/about/index.astro +++ b/website/src/pages/about/index.astro @@ -2,4 +2,27 @@ import RootLayout from "~/layouts/RootLayout.astro"; --- - + +
+

About

+ +

+ +

Author & Developer

+
+ + Author of Spotube +
+
Kingkor Roy Tirtho
+

Flutter developer

+
+
+
diff --git a/website/src/pages/downloads/packages/index.astro b/website/src/pages/downloads/packages/index.astro deleted file mode 100644 index 26937f1e..00000000 --- a/website/src/pages/downloads/packages/index.astro +++ /dev/null @@ -1,5 +0,0 @@ ---- -import RootLayout from "~/layouts/RootLayout.astro"; ---- - - diff --git a/website/src/pages/downloads/packages/index.mdx b/website/src/pages/downloads/packages/index.mdx new file mode 100644 index 00000000..13f2587c --- /dev/null +++ b/website/src/pages/downloads/packages/index.mdx @@ -0,0 +1,68 @@ +import { FaLinux, FaWindows, FaApple } from 'react-icons/fa6'; +import RootLayout from 'layouts/RootLayout.astro'; +import MarkdownLayout from 'layouts/MarkdownLayout.astro'; + + + +
+

Package Managers

+ Spotube is available in various Package Managers supported by Platform + ## Linux + ### Flatpak📦 + Make sure [Flatpak](https://flatpak.org) is installed in your Linux device & Run the following command in the terminal: + ```bash + $ flatpak install com.github.KRTirtho.Spotube + ``` + ### Arch User Repository (AUR)♾️ + If you're an Arch Linux user, you can also install Spotube from AUR. + Make sure you have `yay`/`pamac`/`paru` installed in your system. And Run the Following command in the Terminal: + ```bash + $ yay -Sy spotube-bin + ``` + ```bash + $ pamac install spotube-bin + ``` + ```bash + $ paru -Sy spotube-bin + ``` + {/* */} + ## MacOS + ### Homebrew🍻 + Spotube can be installed through Homebrew. We host our own cask definition thus you'll need to add our tap first: + ```bash + $ brew tap krtirtho/apps + $ brew install --cask spotube + ``` + {/* */} + ## Windows + ### Chocolatey🍫 + Spotube is available in [community.chocolatey.org](https://community.chocolatey.org) repo. If you have chocolatey install in your system just run following command in an Elevated Command Prompt or PowerShell: + ```powershell + $ choco install spotube + ``` + ### WinGet💫 + Spotube is also available in the Official Windows PackageManager WinGet. Make sure you have WinGet installed in your Windows machine and run following in a Terminal: + ```powershell + $ winget install --id KRTirtho.Spotube + ``` + ### Scoop🥄 + Spotube is also available in [Scoop](https://scoop.sh) bucket. Make sure you have Scoop installed in your Windows machine and run following in a Terminal: + ```powershell + $ scoop bucket add extras + $ scoop install spotube + ``` +
+
+
\ No newline at end of file From f228937e3e5e8ab2778bfa575b92ae6b0ec66d12 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Aug 2025 13:06:44 +0600 Subject: [PATCH 03/19] website: hide older releases before 4.0.2 --- website/src/modules/downloads/older/releases.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/src/modules/downloads/older/releases.tsx b/website/src/modules/downloads/older/releases.tsx index 2aa58b60..27eb1814 100644 --- a/website/src/modules/downloads/older/releases.tsx +++ b/website/src/modules/downloads/older/releases.tsx @@ -95,7 +95,12 @@ export default function ReleasesSection() { repo: "spotube", }).then((res) => { - setReleases(res.data); + setReleases( + res.data.filter((release) => { + // Ignore all releases that were published before March 18 2025 + return new Date(release.published_at ?? new Date()) >= new Date("2025-03-18T00:00:00Z"); + }) + ); }) }, []) From a734ded6f8b643222f894781ad525b48d40ce17a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Aug 2025 21:25:24 +0600 Subject: [PATCH 04/19] website: add documentation structure and navigation components --- website/src/collections/app.ts | 3 +- .../components/navigation/DocSideBar.astro | 118 ++++++++++++++++++ .../src/components/navigation/TopBar.astro | 2 +- .../components/navigation/sidebar-button.tsx | 2 +- website/src/content.config.ts | 18 +++ .../content/docs/get-started/introduction.mdx | 8 ++ website/src/layouts/DocLayout.astro | 85 +++++++++++++ website/src/modules/docs/Breadcrumbs.astro | 32 +++++ .../src/modules/docs/TableOfContents.astro | 47 +++++++ website/src/pages/docs/[...slug]/index.astro | 33 +++++ 10 files changed, 345 insertions(+), 3 deletions(-) create mode 100644 website/src/components/navigation/DocSideBar.astro create mode 100644 website/src/content.config.ts create mode 100644 website/src/content/docs/get-started/introduction.mdx create mode 100644 website/src/layouts/DocLayout.astro create mode 100644 website/src/modules/docs/Breadcrumbs.astro create mode 100644 website/src/modules/docs/TableOfContents.astro create mode 100644 website/src/pages/docs/[...slug]/index.astro diff --git a/website/src/collections/app.ts b/website/src/collections/app.ts index 9a1d9596..e04b02a0 100644 --- a/website/src/collections/app.ts +++ b/website/src/collections/app.ts @@ -9,11 +9,12 @@ import { FaWindows, FaRedhat, } from "react-icons/fa6"; -import { LuHouse, LuNewspaper, LuDownload } from "react-icons/lu"; +import { LuHouse, LuNewspaper, LuDownload, LuBook } from "react-icons/lu"; export const routes: Record = { "/": ["Home", LuHouse], "/blog": ["Blog", LuNewspaper], + "/docs/get-started/introduction": ["Docs", LuBook], "/downloads": ["Downloads", LuDownload], "/about": ["About", null], }; diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro new file mode 100644 index 00000000..0c4d8b65 --- /dev/null +++ b/website/src/components/navigation/DocSideBar.astro @@ -0,0 +1,118 @@ +--- +import type { HTMLAttributes } from "astro/types"; +import type { CollectionEntry } from "astro:content"; +import { getCollection } from "astro:content"; + +interface NavigationItem extends HTMLAttributes<"a"> { + title: string; + tag?: string; +} + +interface NavigationGroup { + title: string; + items: NavigationItem[]; +} + +interface Props { + topGroups?: NavigationGroup[]; + classList?: string; + bottomGroups?: NavigationGroup[]; +} +const { topGroups, bottomGroups, classList } = Astro.props; + +const sortByOrder = (a: CollectionEntry<"docs">, b: CollectionEntry<"docs">) => + a.data.order - b.data.order; + +async function queryCollection(startsWith: string) { + return ( + await getCollection("docs", (entry) => { + if (!entry.id.startsWith(startsWith)) return false; + if (entry.id.split("/").length > 2) return false; + if (entry.id.endsWith("meta")) return false; + return true; + }) + ).toSorted(sortByOrder); +} + +async function queryMetaCollection(startsWith: string) { + return ( + await getCollection("docs", (entry) => { + if (!entry.id.startsWith(startsWith)) return false; + if (!entry.id.endsWith("meta")) return false; + return true; + }) + ).toSorted(sortByOrder); +} + +const toNavItems = (entries: CollectionEntry<"docs">[]) => + entries.map((page) => ({ + title: page.data.title, + href: `/docs/${page.id}`, + })); + +// Define navigation sections +const sections: [ + string, + string, + (prefix: string) => Promise[]>, +][] = [ + ["Get Started", "get-started/", queryCollection], + ["Guides", "guides/", queryCollection], + ["Design System", "design/", queryCollection], + ["Tailwind Components", "tailwind/", queryCollection], + ["Functional Components", "components/", queryMetaCollection], + ["Headless Components", "headless/", queryCollection], + ["Integrations", "integrations/", queryMetaCollection], + ["Resources", "resources/", queryCollection], +]; + +// Build navigation dynamically +const navigation: NavigationGroup[] = [ + ...(topGroups ?? []), + ...(await Promise.all( + sections.map(async ([title, prefix, queryFn]) => ({ + title, + items: toNavItems(await queryFn(prefix)), + })) + )), + ...(bottomGroups ?? []), +]; +--- + + diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 7dfa570d..71d1d8ac 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -6,7 +6,7 @@ import SidebarButton from "./sidebar-button"; const pathname = Astro.url.pathname; --- -
+
diff --git a/website/src/components/navigation/sidebar-button.tsx b/website/src/components/navigation/sidebar-button.tsx index 0e31004e..96139917 100644 --- a/website/src/components/navigation/sidebar-button.tsx +++ b/website/src/components/navigation/sidebar-button.tsx @@ -1,7 +1,7 @@ import { useRef, useState } from "react"; import { LuMenu } from "react-icons/lu"; import { useOnClickOutside } from "usehooks-ts"; -import { routes } from "~/collections/app"; +import { routes } from "~/collections/app.ts"; export default function SidebarButton() { const ref = useRef(null) diff --git a/website/src/content.config.ts b/website/src/content.config.ts new file mode 100644 index 00000000..c659504e --- /dev/null +++ b/website/src/content.config.ts @@ -0,0 +1,18 @@ +import { defineCollection, z } from 'astro:content'; +import { glob } from 'astro/loaders'; + +const docs = defineCollection({ + schema: z.object({ + title: z.string().optional().default('(Title)'), + description: z.string().optional().default('(Description)'), + pubDate: z.date().optional(), + tags: z.array(z.string()).optional(), + order: z.number().optional().default(0) + }), + loader: glob({ + base: './src/content/docs', + pattern: ['**/*.mdx', '!**/_*.mdx'] + }), +}); + +export const collections = { docs }; \ No newline at end of file diff --git a/website/src/content/docs/get-started/introduction.mdx b/website/src/content/docs/get-started/introduction.mdx new file mode 100644 index 00000000..593a29d4 --- /dev/null +++ b/website/src/content/docs/get-started/introduction.mdx @@ -0,0 +1,8 @@ +--- +layout: 'layouts/DocLayout.astro' +title: Introduction +description: Intro to Spotube Docs +order: 0 +--- + +## Spotube Docs \ No newline at end of file diff --git a/website/src/layouts/DocLayout.astro b/website/src/layouts/DocLayout.astro new file mode 100644 index 00000000..e71ab8dd --- /dev/null +++ b/website/src/layouts/DocLayout.astro @@ -0,0 +1,85 @@ +--- +import DocSideBar from "~/components/navigation/DocSideBar.astro"; +import Breadcrumbs from "~/modules/docs/Breadcrumbs.astro"; +import TableOfContents from "~/modules/docs/TableOfContents.astro"; + +interface PageHeadings { + depth: number; + slug: string; + text: string; +} + +// interface Chip { +// label: string; +// href: string; +// icon?: string; +// preset?: string; +// } + +interface Props { + frontmatter: { + // Required --- + title: string; + description: string; + }; + headings: PageHeadings[]; +} + +const { frontmatter, headings } = Astro.props; + +// GitHub Settings +// const branch = "website"; +// URLs +// const urls = { +// githubDocsUrl: `https://github.com/KRTirtho/spotube/tree/${branch}/website/src/content`, +// githubSpotubeUrl: `https://github.com/KRTirtho/spotube`, +// }; +--- + +
+ + + +
+ +
+ + +

{frontmatter.title ?? "(title)"}

+

+ {frontmatter.description ?? "(description)"} +

+
+ +
+ +
+ + +
+ + +
diff --git a/website/src/modules/docs/Breadcrumbs.astro b/website/src/modules/docs/Breadcrumbs.astro new file mode 100644 index 00000000..28ff06ab --- /dev/null +++ b/website/src/modules/docs/Breadcrumbs.astro @@ -0,0 +1,32 @@ +--- +const breadcrumbs = Astro.url.pathname + .split("/") + .filter((crumb) => Boolean(crumb) && crumb !== "docs"); +--- + +
    + { + breadcrumbs.map((crumb, i) => ( + <> +
  1. + {i > 0 && + i !== breadcrumbs.length - 1 && + breadcrumbs[0] !== "components" ? ( + + {crumb.replace("-", " ")} + + ) : ( + crumb.replace("-", " ") + )} +
  2. + {i !== breadcrumbs.length - 1 &&
  3. } + + )) + } +
diff --git a/website/src/modules/docs/TableOfContents.astro b/website/src/modules/docs/TableOfContents.astro new file mode 100644 index 00000000..c4eb0081 --- /dev/null +++ b/website/src/modules/docs/TableOfContents.astro @@ -0,0 +1,47 @@ +--- +interface PageHeadings { + depth: number; + slug: string; + text: string; +} + +interface Props { + headings: PageHeadings[]; +} + +const { headings } = Astro.props; + +function setDepthClass(depth: number) { + if (depth === 3) return "ml-4"; + if (depth === 4) return "ml-6"; + if (depth === 5) return "ml-8"; + if (depth === 6) return "ml-10"; + return; +} +--- + +{ + headings.length > 0 && ( + + ) +} diff --git a/website/src/pages/docs/[...slug]/index.astro b/website/src/pages/docs/[...slug]/index.astro new file mode 100644 index 00000000..be019bdb --- /dev/null +++ b/website/src/pages/docs/[...slug]/index.astro @@ -0,0 +1,33 @@ +--- +import RootLayout from "layouts/RootLayout.astro"; +import type { GetStaticPaths } from "astro"; +import { render } from "astro:content"; +import { getCollection, getEntry } from "astro:content"; + +export const getStaticPaths = (async () => { + const pages = await getCollection("docs"); + return pages.map((page) => ({ + params: { + slug: page.id, + }, + props: { + page: page, + }, + })); +}) satisfies GetStaticPaths; + +const { page } = Astro.props; +const { Content, remarkPluginFrontmatter } = await render(page); + +let meta: Awaited>; +if (page.id.startsWith("components/") || page.id.startsWith("integrations/")) { + meta = await getEntry("docs", page.id.replace(/\/[^/]*$/, "/meta")); + if (meta !== undefined) { + Object.assign(remarkPluginFrontmatter, meta.data); + } +} +--- + + + + From 7bb69c02deade2da32b739ff7185a91605c61b61 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 8 Aug 2025 23:54:00 +0600 Subject: [PATCH 05/19] website: write into and sidebar and developing plugins pages --- website/astro.config.mjs | 19 +++--- .../installing-plugins/navigate.webp | Bin 0 -> 125604 bytes website/src/collections/app.ts | 2 +- .../components/navigation/DocSideBar.astro | 14 ++-- .../src/components/navigation/TopBar.astro | 5 +- .../docs/developing-plugins/introduction.mdx | 60 ++++++++++++++++++ .../docs/get-started/installing-plugins.mdx | 28 ++++++++ .../content/docs/get-started/introduction.mdx | 16 ++++- website/src/layouts/DocLayout.astro | 15 ++--- 9 files changed, 129 insertions(+), 30 deletions(-) create mode 100644 website/public/docs/getting-started/installing-plugins/navigate.webp create mode 100644 website/src/content/docs/developing-plugins/introduction.mdx create mode 100644 website/src/content/docs/get-started/installing-plugins.mdx diff --git a/website/astro.config.mjs b/website/astro.config.mjs index dd00c5fd..5f6e3eda 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,17 +1,20 @@ // @ts-check -import { defineConfig } from 'astro/config'; +import { defineConfig } from "astro/config"; -import tailwindcss from '@tailwindcss/vite'; +import tailwindcss from "@tailwindcss/vite"; -import react from '@astrojs/react'; +import react from "@astrojs/react"; -import mdx from '@astrojs/mdx'; +import mdx from "@astrojs/mdx"; // https://astro.build/config export default defineConfig({ vite: { - plugins: [tailwindcss()] + plugins: [tailwindcss()], }, - - integrations: [react(), mdx()] -}); \ No newline at end of file + integrations: [react(), mdx()], + redirects: { + "/docs": "/docs/get-started/introduction", + "/docs/get-started": "/docs/get-started/introduction", + }, +}); diff --git a/website/public/docs/getting-started/installing-plugins/navigate.webp b/website/public/docs/getting-started/installing-plugins/navigate.webp new file mode 100644 index 0000000000000000000000000000000000000000..3f432179913446dd7a018b1b55ef1d29e5fdf5f6 GIT binary patch literal 125604 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
@@ -33,12 +34,14 @@ const pathname = Astro.url.pathname; { Object.entries(routes).map((route) => { const Icon = route[1][1]; + const isActive = + route[0] === "/" ? pathname === "/" : pathname.startsWith(route[0]); return (
-
+
@@ -70,16 +73,6 @@ const { frontmatter, headings } = Astro.props;

=zxF#002Uf=9#igM+U|B(9R>Ii#W-4 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 literal 0 HcmV?d00001 diff --git a/website/src/collections/app.ts b/website/src/collections/app.ts index e04b02a0..3ae86c8a 100644 --- a/website/src/collections/app.ts +++ b/website/src/collections/app.ts @@ -14,7 +14,7 @@ import { LuHouse, LuNewspaper, LuDownload, LuBook } from "react-icons/lu"; export const routes: Record = { "/": ["Home", LuHouse], "/blog": ["Blog", LuNewspaper], - "/docs/get-started/introduction": ["Docs", LuBook], + "/docs": ["Docs", LuBook], "/downloads": ["Downloads", LuDownload], "/about": ["About", null], }; diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index 0c4d8b65..b8570fb0 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -57,13 +57,13 @@ const sections: [ (prefix: string) => Promise[]>, ][] = [ ["Get Started", "get-started/", queryCollection], - ["Guides", "guides/", queryCollection], - ["Design System", "design/", queryCollection], - ["Tailwind Components", "tailwind/", queryCollection], - ["Functional Components", "components/", queryMetaCollection], - ["Headless Components", "headless/", queryCollection], - ["Integrations", "integrations/", queryMetaCollection], - ["Resources", "resources/", queryCollection], + ["Developing Plugins", "developing-plugins/", queryCollection], + // ["Design System", "design/", queryCollection], + // ["Tailwind Components", "tailwind/", queryCollection], + // ["Functional Components", "components/", queryMetaCollection], + // ["Headless Components", "headless/", queryCollection], + // ["Integrations", "integrations/", queryMetaCollection], + // ["Resources", "resources/", queryCollection], ]; // Build navigation dynamically diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 71d1d8ac..3da5feab 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -4,6 +4,7 @@ import { FaGithub } from "react-icons/fa6"; import SidebarButton from "./sidebar-button"; const pathname = Astro.url.pathname; +console.log("pathname:", pathname); ---

From b3f6824a94f84c3c1cb1fcd5ab54548e1aa7661b Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 11 Aug 2025 21:03:28 +0600 Subject: [PATCH 06/19] docs: add plugin create guide --- .../create-your-first-plugin.mdx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 website/src/content/docs/developing-plugins/create-your-first-plugin.mdx diff --git a/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx b/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx new file mode 100644 index 00000000..8ea9d4d2 --- /dev/null +++ b/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx @@ -0,0 +1,24 @@ +--- +layout: "layouts/DocLayout.astro" +title: Create your first plugin +description: "" +order: 1 +--- + +If you are comfortable with Dart, Flutter and Hetu Script, you can start developing your first plugin. +This guide will help you initialize a plugin project and write your first plugin. + +## Initializing a plugin project + +[spotube-plugin-template][spotube-plugin-template] is a template repository for Spotube plugins. It's a starting point +with everything you need to get started with plugin development. You should use it to create your own plugin. + +Simply clone or click "Use this template" button on the repository page to create a new repository. + +```bash +$ git clone https://github.com/KRTirtho/spotube-plugin-template.git +$ cd spotube-plugin-template +``` + +{/* Links */} +[spotube-plugin-template]: https://github.com/KRTirtho/spotube-plugin-template From 8bdbe7dfba4b459a705d1645804c5feddf021eb2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 09:25:13 +0600 Subject: [PATCH 07/19] docs: add detailed documentation for plugin endpoints and methods --- .../developing-plugins/album-endpoint.mdx | 48 ++++++++++ .../developing-plugins/artist-endpoint.mdx | 56 +++++++++++ .../docs/developing-plugins/auth-endpoint.mdx | 72 ++++++++++++++ .../create-your-first-plugin.mdx | 61 +++++++++++- .../implmenting-plugin-methods.mdx | 95 +++++++++++++++++++ .../developing-plugins/playlist-endpoint.mdx | 81 ++++++++++++++++ .../developing-plugins/track-endpoint.mdx | 43 +++++++++ .../docs/developing-plugins/user-endpoint.mdx | 83 ++++++++++++++++ 8 files changed, 538 insertions(+), 1 deletion(-) create mode 100644 website/src/content/docs/developing-plugins/album-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/artist-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/auth-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/implmenting-plugin-methods.mdx create mode 100644 website/src/content/docs/developing-plugins/playlist-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/track-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/user-endpoint.mdx diff --git a/website/src/content/docs/developing-plugins/album-endpoint.mdx b/website/src/content/docs/developing-plugins/album-endpoint.mdx new file mode 100644 index 00000000..bf8317a5 --- /dev/null +++ b/website/src/content/docs/developing-plugins/album-endpoint.mdx @@ -0,0 +1,48 @@ +--- +layout: "layouts/DocLayout.astro" +title: The AlbumEndpoint +description: "" +order: 6 +--- + +The AlbumEndpoint is used to fetch album information and do album-related actions. In the `src/segments/album.ht` file you can find all the +required method definitions. + +```hetu_script +class AlbumEndpoint { + construct (this.client) + + fun getAlbum(id: string) { + // TODO: Implement method + } + + fun tracks(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun releases({offset: int, limit: int}) { + // TODO: Implement method + } + + fun save(albumIds: List) { // List + // TODO: Implement method + } + + fun unsave(albumIds: List) { // List + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `getAlbum()` | Fetches album information by ID. | [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `tracks()` | Fetches tracks of the specified album. Accepts an ID and optional pagination parameters. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `releases()` | Fetches new album releases user followed artists or globally | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `save()` | Saves the specified albums. Accepts a list of album IDs. | `void` | +| `unsave()` | Removes the specified albums from saved albums. Accepts a list of album IDs. | `void` | + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object diff --git a/website/src/content/docs/developing-plugins/artist-endpoint.mdx b/website/src/content/docs/developing-plugins/artist-endpoint.mdx new file mode 100644 index 00000000..fef8b44a --- /dev/null +++ b/website/src/content/docs/developing-plugins/artist-endpoint.mdx @@ -0,0 +1,56 @@ +--- +layout: "layouts/DocLayout.astro" +title: The ArtistEndpoint +description: "" +order: 7 +--- + +The ArtistEndpoint is used to fetch artist information and do artist-related actions. In the `src/segments/artist.ht` file you can find all the +required method definitions. + +```hetu_script +class ArtistEndpoint { + var client: HttpClient + + construct (this.client) + + fun getArtist(id: string) { + // TODO: Implement method + } + + fun related(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun topTracks(id: string, {limit: int, offset: int}) { + // TODO: Implement method + } + + fun albums(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun save(artistIds: List) { + // TODO: Implement method + } + + fun unsave(artistIds: List) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `getArtist()` | Fetches artist information by ID. | [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `related()` | Fetches related artists based on the specified artist ID. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `topTracks()` | Fetches top tracks of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `albums()` | Fetches albums of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `save()` | Saves the specified artists. Accepts a list of artist IDs. | `void` | +| `unsave()` | Removes the specified artists from saved artists. Accepts a list of artist IDs. | `void` | + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[SpotubeFullArtistObject]: /models/spotube-full-artist-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object diff --git a/website/src/content/docs/developing-plugins/auth-endpoint.mdx b/website/src/content/docs/developing-plugins/auth-endpoint.mdx new file mode 100644 index 00000000..9a7e6ebb --- /dev/null +++ b/website/src/content/docs/developing-plugins/auth-endpoint.mdx @@ -0,0 +1,72 @@ +--- +layout: "layouts/DocLayout.astro" +title: The AuthEndpoint +description: "" +order: 3 +--- + +> If your plugin doesn't need authentication support, you can skip this section. + +In the `src/segments/auth.ht` file you can find all the required method definition. These are the necessary +methods Spotube calls in it's lifecycle. + +```hetu_script +class AuthEndpoint { + var client: HttpClient + final controller: StreamController + + get authStateStream -> Stream => controller.stream + + construct (this.client){ + controller = StreamController.broadcast() + } + + fun isAuthenticated() -> bool { + // TODO: Implement method + return false + } + + fun authenticate() -> Future { + // TODO: Implement method + } + + fun logout() -> Future { + // TODO: Implement method + } +} +``` + +For this specific endpoint, you may need `WebView` or `Forms` to get user inputs. The [`hetu_spotube_plugin`][hetu_spotube_plugin] provides +such APIs. + +> Learn more about it in the [Spotube Plugin API][spotube_plugin_api] section + +### The `.authStateStream` property + +The `AuthEndpoint.authStateStream` property is also necessary to notify Spotube about the authentication status. [`hetu_std`][hetu_std] is a built-in +module and it exports `StreamController` which basically 1:1 copy of the Dart's [StreamController][dart_stream_controller]. +If the status of authentication changes you need to add a new event using the `controller.add` +Following events are respected by Spotube: + +| Name | Description | +| ----------- | ------------------------------------------------------------ | +| `login` | When user successfully completes login | +| `logout` | When user logs out of the service | +| `recovered` | When user's cached/saved credentials are recovered from disk | +| `refreshed` | When user's session is refreshed | + +Example of adding a new authentication event: + +```hetu_script +controller.add({ type: "login" }.toJson()) +``` + +By the way, the event type is a `Map` in the Dart side, so make sure to always convert hetu_script's [structs into Maps][hetu_struct_into_map] + +{/* Urls */} +[hetu_script_import_export_docs]: https://hetu-script.github.io/docs/en-US/grammar/import/ +[hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin +[spotube_plugin_api]: / +[hetu_std]: https://github.com/hetu-community/hetu_std +[dart_stream_controller]: https://api.flutter.dev/flutter/dart-async/StreamController-class.html +[hetu_struct_into_map]: https://hetu-script.github.io/docs/en-US/api_reference/hetu/#struct diff --git a/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx b/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx index 8ea9d4d2..08a22e8f 100644 --- a/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx +++ b/website/src/content/docs/developing-plugins/create-your-first-plugin.mdx @@ -13,12 +13,71 @@ This guide will help you initialize a plugin project and write your first plugin [spotube-plugin-template][spotube-plugin-template] is a template repository for Spotube plugins. It's a starting point with everything you need to get started with plugin development. You should use it to create your own plugin. -Simply clone or click "Use this template" button on the repository page to create a new repository. +Simply clone or click "Use this template" button on the GitHub repository page to create a new repository. ```bash $ git clone https://github.com/KRTirtho/spotube-plugin-template.git $ cd spotube-plugin-template ``` +## Understanding plugins.json + +After cloning the repository, you will find a file named `plugins.json` in the root directory. +This file is crucial for Spotube to recognize your plugin. It looks like this: + +```json +{ + "type": "metadata", + "version": "1.0.0", + "name": "Alphanumeric plugin name with hyphens or underscore", + "author": "Your Name", + "description": "A brief description of the plugin's functionality.", + "entryPoint": "plugin class name", + "apis": ["webview", "localstorage", "timezone"], + "abilities": ["authentication", "scrobbling"], + "repository": "https://github.com/KRTirtho/spotube-plugin-template", + "pluginApiVersion": "1.0.0" +} +``` + +| Property | Description | +| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `type` | The type of the plugin, which is always `metadata` for Spotube plugins. | +| `version` | The version of the plugin, following [semantic versioning][semantic-version] (e.g., `1.0.0`). | +| `name` | The name of the plugin | +| `author` | The name of the plugin author. | +| `description` | A brief description of the plugin's functionality. | +| `entryPoint` | The name of the class that serves as the entry point for the plugin. | +| `apis` | An array of APIs that the plugin uses. This is used to determine which APIs are available to the plugin. Following APIs are available "webview", "localstorage", "timezone" | +| `abilities` | An array of abilities that the plugin has. This is used to determine which abilities the plugin has. Following abilities can be listed: "authentication", "scrobbling" | +| `repository` | The URL of the plugin's repository. This is used to display the plugin's repository in the plugin manager. | +| `pluginApiVersion` | The version of the plugin API that the plugin uses. This is used to determine if the plugin is compatible with the current version of Spotube. | + +Change the values in the `plugins.json` file to match your plugin's information. + +## Running the `example` app + +There's an `example` folder that contains a simple Flutter app that utilizes all the methods +Spotube would call on your plugin. You can run this app to test your plugin's functionality. + +But first you need too compile the plugin to bytecode. You can simply do this using: + +```shell +$ make +``` + +Make sure you've `make` command installed on your system and also must have the [hetu_script_dev_tools][hetu_script_dev_tools] package globally installed. +After compiling the plugin, you can run the example app like any other Flutter app. + +```shell +$ cd example +$ flutter run +``` + +> Most of the buttons, will not work as they not yet implemented. You've to implement the methods in your plugin source code. +> We will cover how to implement the methods in the next section. + {/* Links */} [spotube-plugin-template]: https://github.com/KRTirtho/spotube-plugin-template +[semantic-version]: https://semver.org/ +[hetu_script_dev_tools]: https://pub.dev/packages/hetu_script_dev_tools diff --git a/website/src/content/docs/developing-plugins/implmenting-plugin-methods.mdx b/website/src/content/docs/developing-plugins/implmenting-plugin-methods.mdx new file mode 100644 index 00000000..012922dd --- /dev/null +++ b/website/src/content/docs/developing-plugins/implmenting-plugin-methods.mdx @@ -0,0 +1,95 @@ +--- +layout: "layouts/DocLayout.astro" +title: Implementing plugin methods +description: Tutorial on how to implement methods in your Spotube plugin. +order: 2 +--- + +In the previous section, you learned how to create a plugin project and run the example app. +In this section, you will learn how to implement methods in your Spotube plugin. + +## The `entryPoint` class + +The `entryPoint` (from the plugin.json) class is the main class of your plugin. You can find it in `src/plugin.ht`. It's the class +that Spotube will instantiate when it loads your plugin. You can pretty much keep this class same as the template, unless you +there's something specific you want to change. + +```hetu_script +// The name of the class should match the `entryPoint` in plugin.json +class TemplateMetadataProviderPlugin { + // These are required properties that Spotube will use to call the methods. + // ==== Start of required properties ==== + var auth: AuthEndpoint + var album: AlbumEndpoint + var artist: ArtistEndpoint + var browse: BrowseEndpoint + var playlist: PlaylistEndpoint + var search: SearchEndpoint + var track: TrackEndpoint + var user: UserEndpoint + var core: CorePlugin + // ==== End of required properties ==== + + var api: HttpClient + + construct (){ + api = HttpClient( + HttpBaseOptions( + baseUrl: "https://example.com" + ) + ) + + auth = AuthEndpoint(api) + + album = AlbumEndpoint(api) + artist = ArtistEndpoint(api) + browse = BrowseEndpoint(api) + playlist = PlaylistEndpoint(api) + search = SearchEndpoint(api) + track = TrackEndpoint(api) + user = UserEndpoint(api) + core = CorePlugin(api) + + auth.authStateStream.listen((event) { + // get authentication events + }) + } +} +``` + +If you read how the import/export works for [hetu_script][hetu_script_import_export_docs], you should realize it's pretty similar to ECMA Script modules or ES6+ Modules +from the JavaScript world. + +```hetu_script +import { AuthEndpoint } from './segments/auth.ht' +import { AlbumEndpoint } from "./segments/album.ht" +import { ArtistEndpoint } from "./segments/artist.ht" +import { BrowseEndpoint } from "./segments/browse.ht" +import { PlaylistEndpoint } from './segments/playlist.ht' +import { SearchEndpoint } from './segments/search.ht' +import { TrackEndpoint } from './segments/track.ht' +import { UserEndpoint } from './segments/user.ht' +import { CorePlugin } from './segments/core.ht' +``` + +## Implementing subclasses + +Now that we've seen `entryPoint` class, we can look into the properties of that classes which are the actual +classes that contains methods that Spotube calls. All of them are in `src/segments` folder + +> **IMPORTANT!:** hetu\*script claims it supports async/await. But unfortunately it still doesn't work yet. +> So for now, we have to bear with .then() +> +> Also, if you've read the hetu_script docs, you should know hetu_script doesn't support Error Handling. +> This is a design decision of the language and the errors should only be handled in the Dart code. +> So there's no try/catch/finally or .catch() method + +In the next section, we will cover how to implement the methods in these classes. + +{/* Urls */} +[hetu_script_import_export_docs]: https://hetu-script.github.io/docs/en-US/grammar/import/ +[hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin +[spotube_plugin_api]: / +[hetu_std]: https://github.com/hetu-community/hetu_std +[dart_stream_controller]: https://api.flutter.dev/flutter/dart-async/StreamController-class.html +[hetu_struct_into_map]: https://hetu-script.github.io/docs/en-US/api_reference/hetu/#struct diff --git a/website/src/content/docs/developing-plugins/playlist-endpoint.mdx b/website/src/content/docs/developing-plugins/playlist-endpoint.mdx new file mode 100644 index 00000000..70b0a92a --- /dev/null +++ b/website/src/content/docs/developing-plugins/playlist-endpoint.mdx @@ -0,0 +1,81 @@ +--- +layout: "layouts/DocLayout.astro" +title: The PlaylistEndpoint +description: "" +order: 8 +--- + +The PlaylistEndpoint is used to fetch playlist information and do track-related actions. In the `src/segments/playlist.ht` file you can find all the +required method definitions. + +```hetu_script +class PlaylistEndpoint { + var client: HttpClient + + construct (this.client) + + fun getPlaylist(id: string) { + // TODO: Implement method + } + + fun tracks(id: string, { offset: int, limit: int }) { + // TODO: Implement method + } + + fun create(userId: string, { + name: string, + description: string, + public: bool, + collaborative: bool + }) { + // TODO: Implement method + } + + fun update(playlistId: string, { + name: string, + description: string, + public: bool, + collaborative: bool + }) { + // TODO: Implement method + } + + fun deletePlaylist(playlistId: string) { + // TODO: Implement method + } + + fun addTracks(playlistId: string, { trackIds: List, position: int }) { + // TODO: Implement method + } + + + fun removeTracks(playlistId: string, { trackIds: List }) { + // TODO: Implement method + } + + fun save(playlistId: string) { + // TODO: Implement method + } + + fun unsave(playlistId: string) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ---------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `getPlaylist` | Fetches a playlist by its ID. | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `tracks` | Fetches tracks in a playlist. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `create` | Creates a new playlist and returns | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `update` | Updates an existing playlist. | `void` | +| `deletePlaylist` | Deletes a playlist. | `void` | +| `addTracks` | Adds tracks to a playlist. | `void` | +| `removeTracks` | Removes tracks from a playlist. | `void` | +| `save` | Saves a playlist to the user's library. | `void` | +| `unsave` | Removes a playlist from the user's library. | `void` | + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object diff --git a/website/src/content/docs/developing-plugins/track-endpoint.mdx b/website/src/content/docs/developing-plugins/track-endpoint.mdx new file mode 100644 index 00000000..41b4c8ac --- /dev/null +++ b/website/src/content/docs/developing-plugins/track-endpoint.mdx @@ -0,0 +1,43 @@ +--- +layout: "layouts/DocLayout.astro" +title: The TrackEndpoint +description: "" +order: 5 +--- + +The TrackEndpoint is used to fetch track information and do track-related actions. In the `src/segments/track.ht` file you can find all the +required method definitions. + +```hetu_script +class TrackEndpoint { + var client: HttpClient + + construct (this.client) + + fun getTrack(id: string) { + // TODO: Implement method + } + + fun save(trackIds: List) { // List + // TODO: Implement method + } + + fun unsave(trackIds: List) { // List + // TODO: Implement method + } + + fun radio(id: string) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------- | +| `getTrack()` | Fetches track information by ID. | [SpotubeFullTrackObject][SpotubeFullTrackObject] | +| `save()` | Saves the specified tracks. Accepts a list of track IDs. | void | +| `unsave()` | Removes the specified tracks from saved tracks. Accepts a list of track IDs. | void | +| `radio()` | Fetches related tracks based on specified tracks. Try to return a List of 50 tracks. | [List\][SpotubeFullTrackObject] | + +{/* Urls */} +[SpotubeFullTrackObject]: /models/spotube-full-track-object diff --git a/website/src/content/docs/developing-plugins/user-endpoint.mdx b/website/src/content/docs/developing-plugins/user-endpoint.mdx new file mode 100644 index 00000000..5ce8e7b5 --- /dev/null +++ b/website/src/content/docs/developing-plugins/user-endpoint.mdx @@ -0,0 +1,83 @@ +--- +layout: "layouts/DocLayout.astro" +title: The UserEndpoint +description: "" +order: 4 +--- + +The UserEndpoint is used to fetch user information and manage user-related actions. +In the `src/segments/user.ht` file you can find all the required method definitions. These are the necessary +methods Spotube calls in its lifecycle. + +> Most of these methods should be just a mapping to an API call with minimum latency. Avoid calling plugin APIs like WebView or Forms +> in these methods. User interactions should be avoided here generally. + +```hetu_script +class UserEndpoint { + var client: HttpClient + + construct (this.client) + + fun me() { + // TODO: Implement method + } + + fun savedTracks({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedPlaylists({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedAlbums({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedArtists({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun isSavedPlaylist(playlistId: string) { // Future + // TODO: Implement method + } + + fun isSavedTracks(trackIds: List) { // Future> + // TODO: Implement method + } + + fun isSavedAlbums(albumIds: List) { // Future> + // TODO: Implement method + } + + fun isSavedArtists(artistIds: List) { // Future> + // TODO: Implement method + } +} +``` + +These methods are pretty self-explanatory. You need to implement them to fetch user information from your service. + +| Method | Description | Returns | +| ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `me()` | Fetches the current user's information. | [`SpotubeUserObject`][SpotubeUserObject] | +| `savedTracks()` | Fetches the user's saved tracks with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `savedPlaylists()` | Fetches the user's saved playlists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `savedAlbums()` | Fetches the user's saved albums with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `savedArtists()` | Fetches the user's saved artists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `isSavedPlaylist()` | Checks if a playlist is saved by the user. Returns a `Future`. | `bool` | +| `isSavedTracks()` | Checks if tracks are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to a track ID) | +| `isSavedAlbums()` | Checks if albums are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an album ID) | +| `isSavedArtists()` | Checks if artists are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an artist ID) | + +> Note: The `isSavedTracks`, `isSavedAlbums`, and `isSavedArtists` methods accept a list of IDs and return a list of booleans +> indicating whether each item is saved by the user. The order of the booleans in the list corresponds to the order of the IDs +> in the input list. + +{/* Links */} +[SpotubeUserObject]: /models/spotube-user-object +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[SpotubeFullArtistObject]: /models/spotube-full-artist-object From fbd7b771efde2bc364e0ec1f0847a7c9901db8b2 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 09:55:28 +0600 Subject: [PATCH 08/19] docs: add detailed documentation for BrowseEndpoint, CoreEndpoint, and SearchEndpoint --- .../developing-plugins/browse-endpoint.mdx | 48 ++++++++++++ .../docs/developing-plugins/core-endpoint.mdx | 74 +++++++++++++++++++ .../developing-plugins/search-endpoint.mdx | 59 +++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 website/src/content/docs/developing-plugins/browse-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/core-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/search-endpoint.mdx diff --git a/website/src/content/docs/developing-plugins/browse-endpoint.mdx b/website/src/content/docs/developing-plugins/browse-endpoint.mdx new file mode 100644 index 00000000..9861d02e --- /dev/null +++ b/website/src/content/docs/developing-plugins/browse-endpoint.mdx @@ -0,0 +1,48 @@ +--- +layout: "layouts/DocLayout.astro" +title: The BrowseEndpoint +description: "" +order: 10 +--- + +The BrowseEndpoint is used to fetch recommendations and catalogs of playlists, albums and artists. In the `src/segments/browse.ht` file you can find all the +required method definitions. + +```hetu_script +class BrowseEndpoint { + var client: HttpClient + + construct (this.client) + + fun sections({offset: int, limit: int}) { + // TODO: Implement method + } + + fun sectionItems(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `sections()` | Returns the sections of the home page. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeBrowseSectionObject`][SpotubeBrowseSectionObject] of `Object` | +| `sectionItems()` | Returns the items of a specific section. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of `Object` | + +> In `sectionItems()` The `id` it takes comes from `sections()`. It is basically used in an expanded screen to show the browse section items with pagination. +> +> For sections returned by `sections()` if `browseMore` is `true` that's when `sectionItems()` is used to fetch the items of that section. + +By the way, the `Object` can be any of the following types: + +- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] +- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] +- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeBrowseSectionObject]: /models/spotube-browse-section-object +[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubeFullArtistObject]: /models/spotube-full-artist-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object diff --git a/website/src/content/docs/developing-plugins/core-endpoint.mdx b/website/src/content/docs/developing-plugins/core-endpoint.mdx new file mode 100644 index 00000000..5daedc29 --- /dev/null +++ b/website/src/content/docs/developing-plugins/core-endpoint.mdx @@ -0,0 +1,74 @@ +--- +layout: "layouts/DocLayout.astro" +title: The CoreEndpoint +description: "" +order: 11 +--- + +The CoreEndpoint is a special subclass which is used to check update and scrobbling and to get support text. In the `src/segments/core.ht` file you can find all the +required method definitions. + +```hetu_script +class CorePlugin { + var client: HttpClient + + construct (this.client) + + /// Checks for updates to the plugin. + /// [currentConfig] is just plugin.json's file content. + /// + /// If there's an update available, it will return a map of: + /// - [downloadUrl] -> direct download url to the new plugin.smplug file. + /// - [version] of the new plugin. + /// - [changelog] Optionally, a changelog for the update (markdown supported). + /// + /// If no update is available, it will return null. + fun checkUpdate(currentConfig: Map) -> Future { + // TODO: Check for updates + } + + /// Returns the support information for the plugin in Markdown or plain text. + /// Supports images and links. + get support -> string { + // TODO: Return support information + return "" + } + + /// Scrobble the provided details to the scrobbling service supported by the plugin. + /// "scrobbling" must be set as an ability in the plugin.json + /// [details] is a map containing the scrobble information, such as: + /// - [id] -> The unique identifier of the track. + /// - [title] -> The title of the track. + /// - [artists] -> List of artists + /// - [id] -> The unique identifier of the artist. + /// - [name] -> The name of the artist. + /// - [album] -> The album of the track + /// - [id] -> The unique identifier of the album. + /// - [name] -> The name of the album. + /// - [timestamp] -> The timestamp of the scrobble (optional). + /// - [duration_ms] -> The duration of the track in milliseconds (optional). + /// - [isrc] -> The ISRC code of the track (optional). + fun scrobble(details: Map) { + // TODO: Implement scrobbling + } +} +``` + +| Method | Description | Returns | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `checkUpdate()` | Checks for updates to the plugin. | `Future` with a map containing `downloadUrl`, `version`, and optionally `changelog`. If no update is available, returns `null`. | +| `support` | Returns support information. | `string` containing the support information in Markdown or plain text. | +| `scrobble()` | [Scrobbles][scrobbling_wiki] the provided track details. This is only called if your plugin.json has scrobbling in the `abilities` field | `void` | + +> In the `checkUpdate()` method the `plugin.json`'s content will be passed as map. You can use that to check updates using the `version` field. +> +> Also, the `downloadUrl` it provides should be a direct binary download link (redirect is supported) for the `.smplug` file + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeBrowseSectionObject]: /models/spotube-browse-section-object +[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubeFullArtistObject]: /models/spotube-full-artist-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[scrobbling_wiki]: https://en.wikipedia.org/wiki/Last.fm diff --git a/website/src/content/docs/developing-plugins/search-endpoint.mdx b/website/src/content/docs/developing-plugins/search-endpoint.mdx new file mode 100644 index 00000000..bc76d707 --- /dev/null +++ b/website/src/content/docs/developing-plugins/search-endpoint.mdx @@ -0,0 +1,59 @@ +--- +layout: "layouts/DocLayout.astro" +title: The SearchEndpoint +description: "" +order: 9 +--- + +The SearchEndpoint is used to fetch search playlist, tracks, album and artists. In the `src/segments/search.ht` file you can find all the +required method definitions. + +```hetu_script +class SearchEndpoint { + var client: HttpClient + + construct (this.client) + + get chips -> List { // Set + // can be tracks, playlists, artists, albums and all + return ["all", "tracks", "albums", "artists", "playlists"] + } + + fun all(query: string) { + // TODO: Implement method + } + + fun albums(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun artists(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun tracks(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun playlists(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `chips` | Returns the available search chips. | `List` | +| `all()` | Searches for all types of content. | [`SpotubeSearchResponseObject`][SpotubeSearchResponseObject] | +| `albums()` | Searches only for albums. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `artists()` | Searches only for artists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `tracks()` | Searches only for tracks. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `playlists()` | Searches only for playlists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | + +{/* Urls */} +[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object +[SpotubeSearchResponseObject]: /models/spotube-search-response-object +[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object +[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubeFullArtistObject]: /models/spotube-full-artist-object +[SpotubeFullAlbumObject]: /models/spotube-full-album-object From 858cbd17ad7de8639ea42e19a49686c6fd1d42d4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 14:10:04 +0600 Subject: [PATCH 09/19] docs: update model documentation and add new model files for Spotube objects --- .../components/navigation/DocSideBar.astro | 1 + .../developing-plugins/album-endpoint.mdx | 6 +-- .../developing-plugins/artist-endpoint.mdx | 8 ++-- .../developing-plugins/browse-endpoint.mdx | 12 +++--- .../docs/developing-plugins/core-endpoint.mdx | 6 --- .../developing-plugins/playlist-endpoint.mdx | 6 +-- .../developing-plugins/search-endpoint.mdx | 12 +++--- .../developing-plugins/track-endpoint.mdx | 2 +- .../docs/developing-plugins/user-endpoint.mdx | 12 +++--- .../docs/models/spotube-album-object.mdx | 42 +++++++++++++++++++ .../docs/models/spotube-artist-object.mdx | 33 +++++++++++++++ .../models/spotube-browse-section-object.mdx | 27 ++++++++++++ .../docs/models/spotube-image-object.mdx | 14 +++++++ .../spotube-pagination-response-object.mdx | 18 ++++++++ .../docs/models/spotube-playlist-object.mdx | 40 ++++++++++++++++++ .../models/spotube-search-response-object.mdx | 21 ++++++++++ .../docs/models/spotube-track-object.mdx | 27 ++++++++++++ .../docs/models/spotube-user-object.mdx | 20 +++++++++ 18 files changed, 272 insertions(+), 35 deletions(-) create mode 100644 website/src/content/docs/models/spotube-album-object.mdx create mode 100644 website/src/content/docs/models/spotube-artist-object.mdx create mode 100644 website/src/content/docs/models/spotube-browse-section-object.mdx create mode 100644 website/src/content/docs/models/spotube-image-object.mdx create mode 100644 website/src/content/docs/models/spotube-pagination-response-object.mdx create mode 100644 website/src/content/docs/models/spotube-playlist-object.mdx create mode 100644 website/src/content/docs/models/spotube-search-response-object.mdx create mode 100644 website/src/content/docs/models/spotube-track-object.mdx create mode 100644 website/src/content/docs/models/spotube-user-object.mdx diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index b8570fb0..05892b80 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -58,6 +58,7 @@ const sections: [ ][] = [ ["Get Started", "get-started/", queryCollection], ["Developing Plugins", "developing-plugins/", queryCollection], + ["Models", "models/", queryCollection], // ["Design System", "design/", queryCollection], // ["Tailwind Components", "tailwind/", queryCollection], // ["Functional Components", "components/", queryMetaCollection], diff --git a/website/src/content/docs/developing-plugins/album-endpoint.mdx b/website/src/content/docs/developing-plugins/album-endpoint.mdx index bf8317a5..2d2ec3dd 100644 --- a/website/src/content/docs/developing-plugins/album-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/album-endpoint.mdx @@ -43,6 +43,6 @@ class AlbumEndpoint { | `unsave()` | Removes the specified albums from saved albums. Accepts a list of album IDs. | `void` | {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/artist-endpoint.mdx b/website/src/content/docs/developing-plugins/artist-endpoint.mdx index fef8b44a..7c74d802 100644 --- a/website/src/content/docs/developing-plugins/artist-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/artist-endpoint.mdx @@ -50,7 +50,7 @@ class ArtistEndpoint { | `unsave()` | Removes the specified artists from saved artists. Accepts a list of artist IDs. | `void` | {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object -[SpotubeFullArtistObject]: /models/spotube-full-artist-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/browse-endpoint.mdx b/website/src/content/docs/developing-plugins/browse-endpoint.mdx index 9861d02e..70f31075 100644 --- a/website/src/content/docs/developing-plugins/browse-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/browse-endpoint.mdx @@ -40,9 +40,9 @@ By the way, the `Object` can be any of the following types: - [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeBrowseSectionObject]: /models/spotube-browse-section-object -[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object -[SpotubeFullArtistObject]: /models/spotube-full-artist-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeBrowseSectionObject]: /docs/models/spotube-browse-section-object +[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject diff --git a/website/src/content/docs/developing-plugins/core-endpoint.mdx b/website/src/content/docs/developing-plugins/core-endpoint.mdx index 5daedc29..20f9a651 100644 --- a/website/src/content/docs/developing-plugins/core-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/core-endpoint.mdx @@ -65,10 +65,4 @@ class CorePlugin { > Also, the `downloadUrl` it provides should be a direct binary download link (redirect is supported) for the `.smplug` file {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeBrowseSectionObject]: /models/spotube-browse-section-object -[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object -[SpotubeFullArtistObject]: /models/spotube-full-artist-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object [scrobbling_wiki]: https://en.wikipedia.org/wiki/Last.fm diff --git a/website/src/content/docs/developing-plugins/playlist-endpoint.mdx b/website/src/content/docs/developing-plugins/playlist-endpoint.mdx index 70b0a92a..137f4a60 100644 --- a/website/src/content/docs/developing-plugins/playlist-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/playlist-endpoint.mdx @@ -76,6 +76,6 @@ class PlaylistEndpoint { | `unsave` | Removes a playlist from the user's library. | `void` | {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/search-endpoint.mdx b/website/src/content/docs/developing-plugins/search-endpoint.mdx index bc76d707..4411aa77 100644 --- a/website/src/content/docs/developing-plugins/search-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/search-endpoint.mdx @@ -51,9 +51,9 @@ class SearchEndpoint { | `playlists()` | Searches only for playlists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | {/* Urls */} -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeSearchResponseObject]: /models/spotube-search-response-object -[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object -[SpotubeFullArtistObject]: /models/spotube-full-artist-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeSearchResponseObject]: /docs/models/spotube-search-response-object +[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject diff --git a/website/src/content/docs/developing-plugins/track-endpoint.mdx b/website/src/content/docs/developing-plugins/track-endpoint.mdx index 41b4c8ac..d6e34695 100644 --- a/website/src/content/docs/developing-plugins/track-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/track-endpoint.mdx @@ -40,4 +40,4 @@ class TrackEndpoint { | `radio()` | Fetches related tracks based on specified tracks. Try to return a List of 50 tracks. | [List\][SpotubeFullTrackObject] | {/* Urls */} -[SpotubeFullTrackObject]: /models/spotube-full-track-object +[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/user-endpoint.mdx b/website/src/content/docs/developing-plugins/user-endpoint.mdx index 5ce8e7b5..4b8efb33 100644 --- a/website/src/content/docs/developing-plugins/user-endpoint.mdx +++ b/website/src/content/docs/developing-plugins/user-endpoint.mdx @@ -75,9 +75,9 @@ These methods are pretty self-explanatory. You need to implement them to fetch u > in the input list. {/* Links */} -[SpotubeUserObject]: /models/spotube-user-object -[SpotubePaginationResponseObject]: /models/spotube-pagination-response-object -[SpotubeFullTrackObject]: /models/spotube-full-track-object -[SpotubeFullPlaylistObject]: /models/spotube-full-playlist-object -[SpotubeFullAlbumObject]: /models/spotube-full-album-object -[SpotubeFullArtistObject]: /models/spotube-full-artist-object +[SpotubeUserObject]: /docs/models/spotube-user-object +[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object +[SpotubeFullTrackObject]: /docs/models/spotube-track-object +[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject diff --git a/website/src/content/docs/models/spotube-album-object.mdx b/website/src/content/docs/models/spotube-album-object.mdx new file mode 100644 index 00000000..29197e90 --- /dev/null +++ b/website/src/content/docs/models/spotube-album-object.mdx @@ -0,0 +1,42 @@ +--- +layout: "layouts/DocLayout.astro" +title: Album +description: Different types of album objects used in Spotube. +order: 3 +--- + +### SpotubeSimpleAlbumObject + +Following is the structure of the `SpotubeAlbumObject`: + +| Property | Type | +| ----------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | +| albumType | `album`, `single` or `compilation` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| releaseDate | `string` (YYYY-MM-DD format) or `null` | + +{/* Urls */} + +### SpotubeFullAlbumObject + +Following is the structure of the `SpotubeFullAlbumObject`: + +| Property | Type | +| ----------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | +| albumType | `album`, `single` or `compilation` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| releaseDate | `string` (YYYY-MM-DD format) | +| totalTracks | `number` | +| recordLabel | `string` or `null` | + +{/* Urls */} +[SpotubeImageObject]: /docs/models/spotube-image-object +[SpotubeSimpleArtistObject]: /docs/models/spotube-artist-objects#spotubesimpleartistobject diff --git a/website/src/content/docs/models/spotube-artist-object.mdx b/website/src/content/docs/models/spotube-artist-object.mdx new file mode 100644 index 00000000..ab1716db --- /dev/null +++ b/website/src/content/docs/models/spotube-artist-object.mdx @@ -0,0 +1,33 @@ +--- +layout: "layouts/DocLayout.astro" +title: Artist +description: Different types of artist objects used in Spotube. +order: 2 +--- + +### SpotubeSimpleArtistObject + +Following is the structure of the `SpotubeArtistObject`: + +| Property | Type | +| ----------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | + +### SpotubeFullArtistObject + +Following is the structure of the `SpotubeFullArtistObject`: + +| Property | Type | +| ----------- | ----------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or | +| followers | `number` | +| genres | List of `string` or `null` | + +{/* Urls */} +[SpotubeImageObject]: /docs/models/spotube-image-object diff --git a/website/src/content/docs/models/spotube-browse-section-object.mdx b/website/src/content/docs/models/spotube-browse-section-object.mdx new file mode 100644 index 00000000..29368605 --- /dev/null +++ b/website/src/content/docs/models/spotube-browse-section-object.mdx @@ -0,0 +1,27 @@ +--- +layout: "layouts/DocLayout.astro" +title: Browse Section +description: "" +order: 7 +--- + +Following is the structure of `SpotubeBrowseSectionObject`: + +| Property | Type | +| ----------- | ---------------- | +| id | `string` | +| title | `string` | +| externalUri | `string` | +| browseMore | `boolean` | +| items | List of `Object` | + +The `items` property array can contain multiple type of `Object` in it but it will always be + +- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] +- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] +- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] + +{/* Urls */} +[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject +[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject diff --git a/website/src/content/docs/models/spotube-image-object.mdx b/website/src/content/docs/models/spotube-image-object.mdx new file mode 100644 index 00000000..665e5d0a --- /dev/null +++ b/website/src/content/docs/models/spotube-image-object.mdx @@ -0,0 +1,14 @@ +--- +layout: "layouts/DocLayout.astro" +title: Image +description: How images are represented in Spotube. +order: 0 +--- + +Following is the structure of the `SpotubeImageObject`: + +| Property | Type | +| -------- | --------------- | +| width | `int` or `null` | +| height | `int` or `null` | +| url | `string` | diff --git a/website/src/content/docs/models/spotube-pagination-response-object.mdx b/website/src/content/docs/models/spotube-pagination-response-object.mdx new file mode 100644 index 00000000..a5efebc3 --- /dev/null +++ b/website/src/content/docs/models/spotube-pagination-response-object.mdx @@ -0,0 +1,18 @@ +--- +layout: "layouts/DocLayout.astro" +title: Pagination Response +description: "" +order: 8 +--- + +`SpotubePaginationResponseObject` is generic model. The `items` property can contain any type of `Object` in it. + +This is the structure of `SpotubePaginationResponseObject`: + +| Property | Type | +| ---------- | ----------------------------------------------- | +| limit | `number` | +| nextOffset | `number` or `null` | +| total | `number` | +| hasMore | `boolean` | +| items | List of generic type `T` which extends `Object` | diff --git a/website/src/content/docs/models/spotube-playlist-object.mdx b/website/src/content/docs/models/spotube-playlist-object.mdx new file mode 100644 index 00000000..3783e596 --- /dev/null +++ b/website/src/content/docs/models/spotube-playlist-object.mdx @@ -0,0 +1,40 @@ +--- +layout: "layouts/DocLayout.astro" +title: Playlist +description: Different types of playlist objects used in Spotube. +order: 5 +--- + +### SpotubeSimplePlaylistObject + +Following is the structure of the `SpotubeSimplePlaylistObject`: + +| Property | Type | +| ----------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| description | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | +| owner | [`SpotubeUserObject`][SpotubeUserObject] | + +### SpotubeFullPlaylistObject + +Following is the structure of the `SpotubeFullPlaylistObject`: + +| Property | Type | +| ------------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| description | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | +| owner | [`SpotubeUserObject`][SpotubeUserObject] | +| collaborators | List of [`SpotubeUserObject`][SpotubeUserObject] or `null` | +| collaborative | `boolean` | +| public | `boolean` | + +{/* Urls */} + +[SpotubeImageObject]: /docs/models/spotube-image-object +[SpotubeUserObject]: /docs/models/spotube-simple-user-object diff --git a/website/src/content/docs/models/spotube-search-response-object.mdx b/website/src/content/docs/models/spotube-search-response-object.mdx new file mode 100644 index 00000000..efe51a26 --- /dev/null +++ b/website/src/content/docs/models/spotube-search-response-object.mdx @@ -0,0 +1,21 @@ +--- +layout: "layouts/DocLayout.astro" +title: Search Response +description: "" +order: 6 +--- + +Following is the structure of the `SpotubeSearchResponseObject`: + +| Property | Type | +| --------- | -------------------------------------------------------------------- | +| albums | List of [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | +| artists | List of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| playlists | List of [`SpotubeSimplePlaylistObject`][SpotubeSimplePlaylistObject] | +| tracks | List of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | + +{/* Urls */} +[SpotubeSimpleAlbumObject]: /docs/models/spotube-album-object#spotubesimplealbumobject +[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject +[SpotubeSimplePlaylistObject]: /docs/models/spotube-playlist-object#spotubesimpleplaylistobject +[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/models/spotube-track-object.mdx b/website/src/content/docs/models/spotube-track-object.mdx new file mode 100644 index 00000000..3e4d8b92 --- /dev/null +++ b/website/src/content/docs/models/spotube-track-object.mdx @@ -0,0 +1,27 @@ +--- +layout: "layouts/DocLayout.astro" +title: Track +description: "" +order: 4 +--- + +Following is the structure of the `SpotubeFullTrackObject`: + +| Property | Type | +| ---------------------------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| album | [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | +| durationMs (in milliseconds) | `number` | +| explicit | `boolean` | +| [isrc][isrc_wiki] | `string` | + +> `isrc` stands for International Standard Recording Code, which is a unique identifier for tracks. +> It is used to identify recordings and is often used in music distribution and royalty collection. The format is typically a 12-character alphanumeric code. + +{/* Urls */} +[SpotubeSimpleArtistObject]: /docs/models/spotube-artist-objects#spotubesimpleartistobject +[SpotubeSimpleAlbumObject]: /docs/models/spotube-album-object#spotubesimplealbumobject +[isrc_wiki]: https://en.wikipedia.org/wiki/International_Standard_Recording_Code diff --git a/website/src/content/docs/models/spotube-user-object.mdx b/website/src/content/docs/models/spotube-user-object.mdx new file mode 100644 index 00000000..9cd32f25 --- /dev/null +++ b/website/src/content/docs/models/spotube-user-object.mdx @@ -0,0 +1,20 @@ +--- +layout: "layouts/DocLayout.astro" +title: User +description: "" +order: 1 +--- + +Structure of the SpotubeUserObject, which is used to represent a user in Spotube returned by Spotube Plugins. + +| Property | Type | +| ----------- | -------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | + +> `externalUri` is a URL that points to the user's profile on the external service (e.g. Listenbrainz) + +{/* Urls */} +[SpotubeImageObject]: /docs/models/spotube-image-object From 032309a2c61fdfd8279475b62bebd71f7cac4985 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 15:10:14 +0600 Subject: [PATCH 10/19] feat: enhance markdown configuration and styling with rehype plugins and update global styles --- website/astro.config.mjs | 34 +++++- website/package.json | 3 + website/pnpm-lock.yaml | 114 ++++++++++++------ .../docs/get-started/installing-plugins.mdx | 6 +- website/src/styles/global.css | 13 ++ 5 files changed, 130 insertions(+), 40 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 5f6e3eda..f1a228e2 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,17 +1,45 @@ // @ts-check import { defineConfig } from "astro/config"; - import tailwindcss from "@tailwindcss/vite"; - import react from "@astrojs/react"; - import mdx from "@astrojs/mdx"; +import rehypeSlug from "rehype-slug"; +import rehypeAutolinkHeadings from "rehype-autolink-headings"; // https://astro.build/config export default defineConfig({ vite: { plugins: [tailwindcss()], }, + markdown: { + syntaxHighlight: "shiki", + shikiConfig: { + langAlias: { + hetu_script: "javascript", + }, + }, + gfm: true, + rehypePlugins: [ + [rehypeSlug, {}], + [ + rehypeAutolinkHeadings, + { + behavior: "wrap", // Adds the link at the end of the heading + properties: { + className: ["heading-link"], // Add a class for styling + "aria-hidden": "true", + }, + content: { + // Optional: Use an SVG icon or text for the link + type: "element", + tagName: "span", + properties: { className: ["icon", "icon-link"] }, + children: [{ type: "text", value: " #" }], + }, + }, + ], + ], + }, integrations: [react(), mdx()], redirects: { "/docs": "/docs/get-started/introduction", diff --git a/website/package.json b/website/package.json index 1afec40e..8298f851 100644 --- a/website/package.json +++ b/website/package.json @@ -22,7 +22,10 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-icons": "^5.5.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-slug": "^6.0.0", "sanitize-html": "^2.17.0", + "shiki": "^3.9.2", "tailwindcss": "^4.1.11", "usehooks-ts": "^3.1.1" }, diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 39774bf8..d0f27f9e 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -47,9 +47,18 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.1.1) + rehype-autolink-headings: + specifier: ^7.1.0 + version: 7.1.0 + rehype-slug: + specifier: ^6.0.0 + version: 6.0.0 sanitize-html: specifier: ^2.17.0 version: 2.17.0 + shiki: + specifier: ^3.9.2 + version: 3.9.2 tailwindcss: specifier: ^4.1.11 version: 4.1.11 @@ -645,23 +654,23 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@3.9.1': - resolution: {integrity: sha512-W5Vwen0KJCtR7KFRo+3JLGAqLUPsfW7e+wZ4yaRBGIogwI9ZlnkpRm9ZV8JtfzMxOkIwZwMmmN0hNErLtm3AYg==} + '@shikijs/core@3.9.2': + resolution: {integrity: sha512-3q/mzmw09B2B6PgFNeiaN8pkNOixWS726IHmJEpjDAcneDPMQmUg2cweT9cWXY4XcyQS3i6mOOUgQz9RRUP6HA==} - '@shikijs/engine-javascript@3.9.1': - resolution: {integrity: sha512-4hGenxYpAmtALryKsdli2K58F0s7RBYpj/RSDcAAGfRM6eTEGI5cZnt86mr+d9/4BaZ5sH5s4p3VU5irIdhj9Q==} + '@shikijs/engine-javascript@3.9.2': + resolution: {integrity: sha512-kUTRVKPsB/28H5Ko6qEsyudBiWEDLst+Sfi+hwr59E0GLHV0h8RfgbQU7fdN5Lt9A8R1ulRiZyTvAizkROjwDA==} - '@shikijs/engine-oniguruma@3.9.1': - resolution: {integrity: sha512-WPlL/xqviwS3te4unSGGGfflKsuHLMI6tPdNYvgz/IygcBT6UiwDFSzjBKyebwi5GGSlXsjjdoJLIBnAplmEZw==} + '@shikijs/engine-oniguruma@3.9.2': + resolution: {integrity: sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==} - '@shikijs/langs@3.9.1': - resolution: {integrity: sha512-Vyy2Yv9PP3Veh3VSsIvNncOR+O93wFsNYgN2B6cCCJlS7H9SKFYc55edsqernsg8WT/zam1cfB6llJsQWLnVhA==} + '@shikijs/langs@3.9.2': + resolution: {integrity: sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==} - '@shikijs/themes@3.9.1': - resolution: {integrity: sha512-zAykkGECNICCMXpKeVvq04yqwaSuAIvrf8MjsU5bzskfg4XreU+O0B5wdNCYRixoB9snd3YlZ373WV5E/g5T9A==} + '@shikijs/themes@3.9.2': + resolution: {integrity: sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==} - '@shikijs/types@3.9.1': - resolution: {integrity: sha512-rqM3T7a0iM1oPKz9iaH/cVgNX9Vz1HERcUcXJ94/fulgVdwqfnhXzGxO4bLrAnh/o5CPLy3IcYedogfV+Ns0Qg==} + '@shikijs/types@3.9.2': + resolution: {integrity: sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -1312,6 +1321,9 @@ packages: hast-util-from-parse5@8.0.3: resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + hast-util-is-element@3.0.0: resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} @@ -1333,6 +1345,9 @@ packages: hast-util-to-parse5@8.0.0: resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-to-text@4.0.2: resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} @@ -1884,6 +1899,9 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + rehype-autolink-headings@7.1.0: + resolution: {integrity: sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==} + rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -1893,6 +1911,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + rehype-stringify@10.0.1: resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} @@ -1957,8 +1978,8 @@ packages: resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shiki@3.9.1: - resolution: {integrity: sha512-HogZ8nMnv9VAQMrG+P7BleJFhrKHm3fi6CYyHRbUu61gJ0lpqLr6ecYEui31IYG1Cn9Bad7N2vf332iXHnn0bQ==} + shiki@3.9.2: + resolution: {integrity: sha512-t6NKl5e/zGTvw/IyftLcumolgOczhuroqwXngDeMqJ3h3EQiTY/7wmfgPlsmloD8oYfqkEDqxiaH37Pjm1zUhQ==} simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -2340,7 +2361,7 @@ snapshots: remark-parse: 11.0.0 remark-rehype: 11.1.2 remark-smartypants: 3.0.2 - shiki: 3.9.1 + shiki: 3.9.2 smol-toml: 1.4.1 unified: 11.0.5 unist-util-remove-position: 5.0.0 @@ -2868,33 +2889,33 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true - '@shikijs/core@3.9.1': + '@shikijs/core@3.9.2': dependencies: - '@shikijs/types': 3.9.1 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@3.9.1': + '@shikijs/engine-javascript@3.9.2': dependencies: - '@shikijs/types': 3.9.1 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.3 - '@shikijs/engine-oniguruma@3.9.1': + '@shikijs/engine-oniguruma@3.9.2': dependencies: - '@shikijs/types': 3.9.1 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.9.1': + '@shikijs/langs@3.9.2': dependencies: - '@shikijs/types': 3.9.1 + '@shikijs/types': 3.9.2 - '@shikijs/themes@3.9.1': + '@shikijs/themes@3.9.2': dependencies: - '@shikijs/types': 3.9.1 + '@shikijs/types': 3.9.2 - '@shikijs/types@3.9.1': + '@shikijs/types@3.9.2': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -3338,7 +3359,7 @@ snapshots: prompts: 2.4.2 rehype: 13.0.2 semver: 7.7.2 - shiki: 3.9.1 + shiki: 3.9.2 smol-toml: 1.4.1 tinyexec: 0.3.2 tinyglobby: 0.2.14 @@ -3739,6 +3760,10 @@ snapshots: vfile-location: 5.0.3 web-namespaces: 2.0.1 + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-is-element@3.0.0: dependencies: '@types/hast': 3.0.4 @@ -3828,6 +3853,10 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-text@4.0.2: dependencies: '@types/hast': 3.0.4 @@ -4613,6 +4642,15 @@ snapshots: dependencies: regex-utilities: 2.3.0 + rehype-autolink-headings@7.1.0: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + hast-util-heading-rank: 3.0.0 + hast-util-is-element: 3.0.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + rehype-parse@9.0.1: dependencies: '@types/hast': 3.0.4 @@ -4633,6 +4671,14 @@ snapshots: transitivePeerDependencies: - supports-color + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.0.0 + rehype-stringify@10.0.1: dependencies: '@types/hast': 3.0.4 @@ -4789,14 +4835,14 @@ snapshots: '@img/sharp-win32-x64': 0.33.5 optional: true - shiki@3.9.1: + shiki@3.9.2: dependencies: - '@shikijs/core': 3.9.1 - '@shikijs/engine-javascript': 3.9.1 - '@shikijs/engine-oniguruma': 3.9.1 - '@shikijs/langs': 3.9.1 - '@shikijs/themes': 3.9.1 - '@shikijs/types': 3.9.1 + '@shikijs/core': 3.9.2 + '@shikijs/engine-javascript': 3.9.2 + '@shikijs/engine-oniguruma': 3.9.2 + '@shikijs/langs': 3.9.2 + '@shikijs/themes': 3.9.2 + '@shikijs/types': 3.9.2 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 diff --git a/website/src/content/docs/get-started/installing-plugins.mdx b/website/src/content/docs/get-started/installing-plugins.mdx index 73431747..3c82095b 100644 --- a/website/src/content/docs/get-started/installing-plugins.mdx +++ b/website/src/content/docs/get-started/installing-plugins.mdx @@ -1,5 +1,5 @@ --- -layout: 'layouts/DocLayout.astro' +layout: "layouts/DocLayout.astro" title: Installing plugins description: Learn how to install and manage plugins in Spotube order: 1 @@ -22,7 +22,7 @@ A malicious plugin given full access can easily steal your credentials. So be ca Try to use the `Official` tagged plugins all the time if you don't want to deal with potential security risks. -- **Upload plugin from local file**: You can also install plugins from local file (plugin.smplug) using the *Orange Upload button* on the top right beside the text field. +- **Upload plugin from local file**: You can also install plugins from local file (plugin.smplug) using the _Orange Upload button_ on the top right beside the text field. - **Install plugin from URL**: If you have a direct link to a plugin file, you can just paste the URL in the text field and use the gray download button beside it -> If you're a developer, you can create your own plugins and share them with the community. Check out the [Plugin Development Guide](/docs/developing-plugins) for more information. \ No newline at end of file +> If you're a developer, you can create your own plugins and share them with the community. Check out the [Plugin Development Guide](/docs/developing-plugins) for more information. diff --git a/website/src/styles/global.css b/website/src/styles/global.css index 29df24d9..5a94f657 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -6,3 +6,16 @@ @import "@skeletonlabs/skeleton"; @import "@skeletonlabs/skeleton/optional/presets"; @import "@skeletonlabs/skeleton/themes/wintry"; + +.prose code::before, +.prose code::after { + content: none !important; +} + +.prose code:not(pre code) { + @apply bg-surface-100-900 px-1 py-0.5 rounded-sm; +} + +.prose a code { + @apply text-primary-500 underline decoration-primary-500; +} From 0e34057794f72b97a9cc0f8865dec0611962d6f4 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 20:19:16 +0600 Subject: [PATCH 11/19] docs: merge some endpoints together to reduce too much elements on the side bar --- .../components/navigation/DocSideBar.astro | 2 +- .../developing-plugins/album-endpoint.mdx | 48 -- .../developing-plugins/artist-endpoint.mdx | 56 -- .../docs/developing-plugins/auth-endpoint.mdx | 72 --- .../developing-plugins/browse-endpoint.mdx | 48 -- .../docs/developing-plugins/core-endpoint.mdx | 68 --- .../implementing-endpoints.mdx | 493 ++++++++++++++++++ .../developing-plugins/playlist-endpoint.mdx | 81 --- .../developing-plugins/search-endpoint.mdx | 59 --- .../developing-plugins/track-endpoint.mdx | 43 -- .../docs/developing-plugins/user-endpoint.mdx | 83 --- .../docs/models/spotube-album-object.mdx | 42 -- .../docs/models/spotube-artist-object.mdx | 33 -- .../models/spotube-browse-section-object.mdx | 27 - .../docs/models/spotube-image-object.mdx | 14 - .../spotube-pagination-response-object.mdx | 18 - .../docs/models/spotube-playlist-object.mdx | 40 -- .../models/spotube-search-response-object.mdx | 21 - .../docs/models/spotube-track-object.mdx | 27 - .../docs/models/spotube-user-object.mdx | 20 - website/src/content/docs/reference/models.mdx | 190 +++++++ 21 files changed, 684 insertions(+), 801 deletions(-) delete mode 100644 website/src/content/docs/developing-plugins/album-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/artist-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/auth-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/browse-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/core-endpoint.mdx create mode 100644 website/src/content/docs/developing-plugins/implementing-endpoints.mdx delete mode 100644 website/src/content/docs/developing-plugins/playlist-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/search-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/track-endpoint.mdx delete mode 100644 website/src/content/docs/developing-plugins/user-endpoint.mdx delete mode 100644 website/src/content/docs/models/spotube-album-object.mdx delete mode 100644 website/src/content/docs/models/spotube-artist-object.mdx delete mode 100644 website/src/content/docs/models/spotube-browse-section-object.mdx delete mode 100644 website/src/content/docs/models/spotube-image-object.mdx delete mode 100644 website/src/content/docs/models/spotube-pagination-response-object.mdx delete mode 100644 website/src/content/docs/models/spotube-playlist-object.mdx delete mode 100644 website/src/content/docs/models/spotube-search-response-object.mdx delete mode 100644 website/src/content/docs/models/spotube-track-object.mdx delete mode 100644 website/src/content/docs/models/spotube-user-object.mdx create mode 100644 website/src/content/docs/reference/models.mdx diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index 05892b80..c5a24823 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -58,7 +58,7 @@ const sections: [ ][] = [ ["Get Started", "get-started/", queryCollection], ["Developing Plugins", "developing-plugins/", queryCollection], - ["Models", "models/", queryCollection], + ["Reference", "reference/", queryCollection], // ["Design System", "design/", queryCollection], // ["Tailwind Components", "tailwind/", queryCollection], // ["Functional Components", "components/", queryMetaCollection], diff --git a/website/src/content/docs/developing-plugins/album-endpoint.mdx b/website/src/content/docs/developing-plugins/album-endpoint.mdx deleted file mode 100644 index 2d2ec3dd..00000000 --- a/website/src/content/docs/developing-plugins/album-endpoint.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The AlbumEndpoint -description: "" -order: 6 ---- - -The AlbumEndpoint is used to fetch album information and do album-related actions. In the `src/segments/album.ht` file you can find all the -required method definitions. - -```hetu_script -class AlbumEndpoint { - construct (this.client) - - fun getAlbum(id: string) { - // TODO: Implement method - } - - fun tracks(id: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun releases({offset: int, limit: int}) { - // TODO: Implement method - } - - fun save(albumIds: List) { // List - // TODO: Implement method - } - - fun unsave(albumIds: List) { // List - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `getAlbum()` | Fetches album information by ID. | [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | -| `tracks()` | Fetches tracks of the specified album. Accepts an ID and optional pagination parameters. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | -| `releases()` | Fetches new album releases user followed artists or globally | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | -| `save()` | Saves the specified albums. Accepts a list of album IDs. | `void` | -| `unsave()` | Removes the specified albums from saved albums. Accepts a list of album IDs. | `void` | - -{/* Urls */} -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/artist-endpoint.mdx b/website/src/content/docs/developing-plugins/artist-endpoint.mdx deleted file mode 100644 index 7c74d802..00000000 --- a/website/src/content/docs/developing-plugins/artist-endpoint.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The ArtistEndpoint -description: "" -order: 7 ---- - -The ArtistEndpoint is used to fetch artist information and do artist-related actions. In the `src/segments/artist.ht` file you can find all the -required method definitions. - -```hetu_script -class ArtistEndpoint { - var client: HttpClient - - construct (this.client) - - fun getArtist(id: string) { - // TODO: Implement method - } - - fun related(id: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun topTracks(id: string, {limit: int, offset: int}) { - // TODO: Implement method - } - - fun albums(id: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun save(artistIds: List) { - // TODO: Implement method - } - - fun unsave(artistIds: List) { - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -| `getArtist()` | Fetches artist information by ID. | [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | -| `related()` | Fetches related artists based on the specified artist ID. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | -| `topTracks()` | Fetches top tracks of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | -| `albums()` | Fetches albums of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | -| `save()` | Saves the specified artists. Accepts a list of artist IDs. | `void` | -| `unsave()` | Removes the specified artists from saved artists. Accepts a list of artist IDs. | `void` | - -{/* Urls */} -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/auth-endpoint.mdx b/website/src/content/docs/developing-plugins/auth-endpoint.mdx deleted file mode 100644 index 9a7e6ebb..00000000 --- a/website/src/content/docs/developing-plugins/auth-endpoint.mdx +++ /dev/null @@ -1,72 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The AuthEndpoint -description: "" -order: 3 ---- - -> If your plugin doesn't need authentication support, you can skip this section. - -In the `src/segments/auth.ht` file you can find all the required method definition. These are the necessary -methods Spotube calls in it's lifecycle. - -```hetu_script -class AuthEndpoint { - var client: HttpClient - final controller: StreamController - - get authStateStream -> Stream => controller.stream - - construct (this.client){ - controller = StreamController.broadcast() - } - - fun isAuthenticated() -> bool { - // TODO: Implement method - return false - } - - fun authenticate() -> Future { - // TODO: Implement method - } - - fun logout() -> Future { - // TODO: Implement method - } -} -``` - -For this specific endpoint, you may need `WebView` or `Forms` to get user inputs. The [`hetu_spotube_plugin`][hetu_spotube_plugin] provides -such APIs. - -> Learn more about it in the [Spotube Plugin API][spotube_plugin_api] section - -### The `.authStateStream` property - -The `AuthEndpoint.authStateStream` property is also necessary to notify Spotube about the authentication status. [`hetu_std`][hetu_std] is a built-in -module and it exports `StreamController` which basically 1:1 copy of the Dart's [StreamController][dart_stream_controller]. -If the status of authentication changes you need to add a new event using the `controller.add` -Following events are respected by Spotube: - -| Name | Description | -| ----------- | ------------------------------------------------------------ | -| `login` | When user successfully completes login | -| `logout` | When user logs out of the service | -| `recovered` | When user's cached/saved credentials are recovered from disk | -| `refreshed` | When user's session is refreshed | - -Example of adding a new authentication event: - -```hetu_script -controller.add({ type: "login" }.toJson()) -``` - -By the way, the event type is a `Map` in the Dart side, so make sure to always convert hetu_script's [structs into Maps][hetu_struct_into_map] - -{/* Urls */} -[hetu_script_import_export_docs]: https://hetu-script.github.io/docs/en-US/grammar/import/ -[hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin -[spotube_plugin_api]: / -[hetu_std]: https://github.com/hetu-community/hetu_std -[dart_stream_controller]: https://api.flutter.dev/flutter/dart-async/StreamController-class.html -[hetu_struct_into_map]: https://hetu-script.github.io/docs/en-US/api_reference/hetu/#struct diff --git a/website/src/content/docs/developing-plugins/browse-endpoint.mdx b/website/src/content/docs/developing-plugins/browse-endpoint.mdx deleted file mode 100644 index 70f31075..00000000 --- a/website/src/content/docs/developing-plugins/browse-endpoint.mdx +++ /dev/null @@ -1,48 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The BrowseEndpoint -description: "" -order: 10 ---- - -The BrowseEndpoint is used to fetch recommendations and catalogs of playlists, albums and artists. In the `src/segments/browse.ht` file you can find all the -required method definitions. - -```hetu_script -class BrowseEndpoint { - var client: HttpClient - - construct (this.client) - - fun sections({offset: int, limit: int}) { - // TODO: Implement method - } - - fun sectionItems(id: string, {offset: int, limit: int}) { - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `sections()` | Returns the sections of the home page. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeBrowseSectionObject`][SpotubeBrowseSectionObject] of `Object` | -| `sectionItems()` | Returns the items of a specific section. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of `Object` | - -> In `sectionItems()` The `id` it takes comes from `sections()`. It is basically used in an expanded screen to show the browse section items with pagination. -> -> For sections returned by `sections()` if `browseMore` is `true` that's when `sectionItems()` is used to fetch the items of that section. - -By the way, the `Object` can be any of the following types: - -- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] -- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] -- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] - -{/* Urls */} -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeBrowseSectionObject]: /docs/models/spotube-browse-section-object -[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject diff --git a/website/src/content/docs/developing-plugins/core-endpoint.mdx b/website/src/content/docs/developing-plugins/core-endpoint.mdx deleted file mode 100644 index 20f9a651..00000000 --- a/website/src/content/docs/developing-plugins/core-endpoint.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The CoreEndpoint -description: "" -order: 11 ---- - -The CoreEndpoint is a special subclass which is used to check update and scrobbling and to get support text. In the `src/segments/core.ht` file you can find all the -required method definitions. - -```hetu_script -class CorePlugin { - var client: HttpClient - - construct (this.client) - - /// Checks for updates to the plugin. - /// [currentConfig] is just plugin.json's file content. - /// - /// If there's an update available, it will return a map of: - /// - [downloadUrl] -> direct download url to the new plugin.smplug file. - /// - [version] of the new plugin. - /// - [changelog] Optionally, a changelog for the update (markdown supported). - /// - /// If no update is available, it will return null. - fun checkUpdate(currentConfig: Map) -> Future { - // TODO: Check for updates - } - - /// Returns the support information for the plugin in Markdown or plain text. - /// Supports images and links. - get support -> string { - // TODO: Return support information - return "" - } - - /// Scrobble the provided details to the scrobbling service supported by the plugin. - /// "scrobbling" must be set as an ability in the plugin.json - /// [details] is a map containing the scrobble information, such as: - /// - [id] -> The unique identifier of the track. - /// - [title] -> The title of the track. - /// - [artists] -> List of artists - /// - [id] -> The unique identifier of the artist. - /// - [name] -> The name of the artist. - /// - [album] -> The album of the track - /// - [id] -> The unique identifier of the album. - /// - [name] -> The name of the album. - /// - [timestamp] -> The timestamp of the scrobble (optional). - /// - [duration_ms] -> The duration of the track in milliseconds (optional). - /// - [isrc] -> The ISRC code of the track (optional). - fun scrobble(details: Map) { - // TODO: Implement scrobbling - } -} -``` - -| Method | Description | Returns | -| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| `checkUpdate()` | Checks for updates to the plugin. | `Future` with a map containing `downloadUrl`, `version`, and optionally `changelog`. If no update is available, returns `null`. | -| `support` | Returns support information. | `string` containing the support information in Markdown or plain text. | -| `scrobble()` | [Scrobbles][scrobbling_wiki] the provided track details. This is only called if your plugin.json has scrobbling in the `abilities` field | `void` | - -> In the `checkUpdate()` method the `plugin.json`'s content will be passed as map. You can use that to check updates using the `version` field. -> -> Also, the `downloadUrl` it provides should be a direct binary download link (redirect is supported) for the `.smplug` file - -{/* Urls */} -[scrobbling_wiki]: https://en.wikipedia.org/wiki/Last.fm diff --git a/website/src/content/docs/developing-plugins/implementing-endpoints.mdx b/website/src/content/docs/developing-plugins/implementing-endpoints.mdx new file mode 100644 index 00000000..dd564995 --- /dev/null +++ b/website/src/content/docs/developing-plugins/implementing-endpoints.mdx @@ -0,0 +1,493 @@ +--- +layout: "layouts/DocLayout.astro" +title: Implementing Endpoints +description: "" +order: 2 +--- + +## AuthEndpoint + +> If your plugin doesn't need authentication support, you can skip this section. + +In the `src/segments/auth.ht` file you can find all the required method definition. These are the necessary +methods Spotube calls in it's lifecycle. + +```hetu_script +class AuthEndpoint { + var client: HttpClient + final controller: StreamController + + get authStateStream -> Stream => controller.stream + + construct (this.client){ + controller = StreamController.broadcast() + } + + fun isAuthenticated() -> bool { + // TODO: Implement method + return false + } + + fun authenticate() -> Future { + // TODO: Implement method + } + + fun logout() -> Future { + // TODO: Implement method + } +} +``` + +For this specific endpoint, you may need `WebView` or `Forms` to get user inputs. The [`hetu_spotube_plugin`][hetu_spotube_plugin] provides +such APIs. + +> Learn more about it in the [Spotube Plugin API][spotube_plugin_api] section + +### The `.authStateStream` property + +The `AuthEndpoint.authStateStream` property is also necessary to notify Spotube about the authentication status. [`hetu_std`][hetu_std] is a built-in +module and it exports `StreamController` which basically 1:1 copy of the Dart's [StreamController][dart_stream_controller]. +If the status of authentication changes you need to add a new event using the `controller.add` +Following events are respected by Spotube: + +| Name | Description | +| ----------- | ------------------------------------------------------------ | +| `login` | When user successfully completes login | +| `logout` | When user logs out of the service | +| `recovered` | When user's cached/saved credentials are recovered from disk | +| `refreshed` | When user's session is refreshed | + +Example of adding a new authentication event: + +```hetu_script +controller.add({ type: "login" }.toJson()) +``` + +By the way, the event type is a `Map` in the Dart side, so make sure to always convert hetu_script's [structs into Maps][hetu_struct_into_map] + +## UserEndpoint + +The UserEndpoint is used to fetch user information and manage user-related actions. +In the `src/segments/user.ht` file you can find all the required method definitions. These are the necessary +methods Spotube calls in its lifecycle. + +> Most of these methods should be just a mapping to an API call with minimum latency. Avoid calling plugin APIs like WebView or Forms +> in these methods. User interactions should be avoided here generally. + +```hetu_script +class UserEndpoint { + var client: HttpClient + + construct (this.client) + + fun me() { + // TODO: Implement method + } + + fun savedTracks({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedPlaylists({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedAlbums({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun savedArtists({ offset: int, limit: int }) { + // TODO: Implement method + } + + fun isSavedPlaylist(playlistId: string) { // Future + // TODO: Implement method + } + + fun isSavedTracks(trackIds: List) { // Future> + // TODO: Implement method + } + + fun isSavedAlbums(albumIds: List) { // Future> + // TODO: Implement method + } + + fun isSavedArtists(artistIds: List) { // Future> + // TODO: Implement method + } +} +``` + +These methods are pretty self-explanatory. You need to implement them to fetch user information from your service. + +| Method | Description | Returns | +| ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | +| `me()` | Fetches the current user's information. | [`SpotubeUserObject`][SpotubeUserObject] | +| `savedTracks()` | Fetches the user's saved tracks with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `savedPlaylists()` | Fetches the user's saved playlists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `savedAlbums()` | Fetches the user's saved albums with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `savedArtists()` | Fetches the user's saved artists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `isSavedPlaylist()` | Checks if a playlist is saved by the user. Returns a `Future`. | `bool` | +| `isSavedTracks()` | Checks if tracks are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to a track ID) | +| `isSavedAlbums()` | Checks if albums are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an album ID) | +| `isSavedArtists()` | Checks if artists are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an artist ID) | + +> Note: The `isSavedTracks`, `isSavedAlbums`, and `isSavedArtists` methods accept a list of IDs and return a list of booleans +> indicating whether each item is saved by the user. The order of the booleans in the list corresponds to the order of the IDs +> in the input list. + +## TrackEndpoint + +The TrackEndpoint is used to fetch track information and do track-related actions. In the `src/segments/track.ht` file you can find all the +required method definitions. + +```hetu_script +class TrackEndpoint { + var client: HttpClient + + construct (this.client) + + fun getTrack(id: string) { + // TODO: Implement method + } + + fun save(trackIds: List) { // List + // TODO: Implement method + } + + fun unsave(trackIds: List) { // List + // TODO: Implement method + } + + fun radio(id: string) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------- | +| `getTrack()` | Fetches track information by ID. | [SpotubeFullTrackObject][SpotubeFullTrackObject] | +| `save()` | Saves the specified tracks. Accepts a list of track IDs. | void | +| `unsave()` | Removes the specified tracks from saved tracks. Accepts a list of track IDs. | void | +| `radio()` | Fetches related tracks based on specified tracks. Try to return a List of 50 tracks. | [List\][SpotubeFullTrackObject] | + +{/* Urls */} + +## AlbumEndpoint + +The AlbumEndpoint is used to fetch album information and do album-related actions. In the `src/segments/album.ht` file you can find all the +required method definitions. + +```hetu_script +class AlbumEndpoint { + construct (this.client) + + fun getAlbum(id: string) { + // TODO: Implement method + } + + fun tracks(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun releases({offset: int, limit: int}) { + // TODO: Implement method + } + + fun save(albumIds: List) { // List + // TODO: Implement method + } + + fun unsave(albumIds: List) { // List + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------ | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `getAlbum()` | Fetches album information by ID. | [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `tracks()` | Fetches tracks of the specified album. Accepts an ID and optional pagination parameters. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `releases()` | Fetches new album releases user followed artists or globally | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `save()` | Saves the specified albums. Accepts a list of album IDs. | `void` | +| `unsave()` | Removes the specified albums from saved albums. Accepts a list of album IDs. | `void` | + +## ArtistEndpoint + +The ArtistEndpoint is used to fetch artist information and do artist-related actions. In the `src/segments/artist.ht` file you can find all the +required method definitions. + +```hetu_script +class ArtistEndpoint { + var client: HttpClient + + construct (this.client) + + fun getArtist(id: string) { + // TODO: Implement method + } + + fun related(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun topTracks(id: string, {limit: int, offset: int}) { + // TODO: Implement method + } + + fun albums(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun save(artistIds: List) { + // TODO: Implement method + } + + fun unsave(artistIds: List) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `getArtist()` | Fetches artist information by ID. | [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `related()` | Fetches related artists based on the specified artist ID. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `topTracks()` | Fetches top tracks of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `albums()` | Fetches albums of the specified artist. Accepts optional pagination. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `save()` | Saves the specified artists. Accepts a list of artist IDs. | `void` | +| `unsave()` | Removes the specified artists from saved artists. Accepts a list of artist IDs. | `void` | + +## PlaylistEndpoint + +The PlaylistEndpoint is used to fetch playlist information and do track-related actions. In the `src/segments/playlist.ht` file you can find all the +required method definitions. + +```hetu_script +class PlaylistEndpoint { + var client: HttpClient + + construct (this.client) + + fun getPlaylist(id: string) { + // TODO: Implement method + } + + fun tracks(id: string, { offset: int, limit: int }) { + // TODO: Implement method + } + + fun create(userId: string, { + name: string, + description: string, + public: bool, + collaborative: bool + }) { + // TODO: Implement method + } + + fun update(playlistId: string, { + name: string, + description: string, + public: bool, + collaborative: bool + }) { + // TODO: Implement method + } + + fun deletePlaylist(playlistId: string) { + // TODO: Implement method + } + + fun addTracks(playlistId: string, { trackIds: List, position: int }) { + // TODO: Implement method + } + + + fun removeTracks(playlistId: string, { trackIds: List }) { + // TODO: Implement method + } + + fun save(playlistId: string) { + // TODO: Implement method + } + + fun unsave(playlistId: string) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ---------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| `getPlaylist` | Fetches a playlist by its ID. | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `tracks` | Fetches tracks in a playlist. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `create` | Creates a new playlist and returns | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | +| `update` | Updates an existing playlist. | `void` | +| `deletePlaylist` | Deletes a playlist. | `void` | +| `addTracks` | Adds tracks to a playlist. | `void` | +| `removeTracks` | Removes tracks from a playlist. | `void` | +| `save` | Saves a playlist to the user's library. | `void` | +| `unsave` | Removes a playlist from the user's library. | `void` | + +## SearchEndpoint + +The SearchEndpoint is used to fetch search playlist, tracks, album and artists. In the `src/segments/search.ht` file you can find all the +required method definitions. + +```hetu_script +class SearchEndpoint { + var client: HttpClient + + construct (this.client) + + get chips -> List { // Set + // can be tracks, playlists, artists, albums and all + return ["all", "tracks", "albums", "artists", "playlists"] + } + + fun all(query: string) { + // TODO: Implement method + } + + fun albums(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun artists(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun tracks(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } + + fun playlists(query: string, {offset: int, limit: int}) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `chips` | Returns the available search chips. | `List` | +| `all()` | Searches for all types of content. | [`SpotubeSearchResponseObject`][SpotubeSearchResponseObject] | +| `albums()` | Searches only for albums. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | +| `artists()` | Searches only for artists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| `tracks()` | Searches only for tracks. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | +| `playlists()` | Searches only for playlists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | + +## BrowseEndpoint + +The BrowseEndpoint is used to fetch recommendations and catalogs of playlists, albums and artists. In the `src/segments/browse.ht` file you can find all the +required method definitions. + +```hetu_script +class BrowseEndpoint { + var client: HttpClient + + construct (this.client) + + fun sections({offset: int, limit: int}) { + // TODO: Implement method + } + + fun sectionItems(id: string, {offset: int, limit: int}) { + // TODO: Implement method + } +} +``` + +| Method | Description | Returns | +| ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| `sections()` | Returns the sections of the home page. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeBrowseSectionObject`][SpotubeBrowseSectionObject] of `Object` | +| `sectionItems()` | Returns the items of a specific section. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of `Object` | + +> In `sectionItems()` The `id` it takes comes from `sections()`. It is basically used in an expanded screen to show the browse section items with pagination. +> +> For sections returned by `sections()` if `browseMore` is `true` that's when `sectionItems()` is used to fetch the items of that section. + +By the way, the `Object` can be any of the following types: + +- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] +- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] +- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] + +## CoreEndpoint + +The CoreEndpoint is a special subclass which is used to check update and scrobbling and to get support text. In the `src/segments/core.ht` file you can find all the +required method definitions. + +```hetu_script +class CorePlugin { + var client: HttpClient + + construct (this.client) + + /// Checks for updates to the plugin. + /// [currentConfig] is just plugin.json's file content. + /// + /// If there's an update available, it will return a map of: + /// - [downloadUrl] -> direct download url to the new plugin.smplug file. + /// - [version] of the new plugin. + /// - [changelog] Optionally, a changelog for the update (markdown supported). + /// + /// If no update is available, it will return null. + fun checkUpdate(currentConfig: Map) -> Future { + // TODO: Check for updates + } + + /// Returns the support information for the plugin in Markdown or plain text. + /// Supports images and links. + get support -> string { + // TODO: Return support information + return "" + } + + /// Scrobble the provided details to the scrobbling service supported by the plugin. + /// "scrobbling" must be set as an ability in the plugin.json + /// [details] is a map containing the scrobble information, such as: + /// - [id] -> The unique identifier of the track. + /// - [title] -> The title of the track. + /// - [artists] -> List of artists + /// - [id] -> The unique identifier of the artist. + /// - [name] -> The name of the artist. + /// - [album] -> The album of the track + /// - [id] -> The unique identifier of the album. + /// - [name] -> The name of the album. + /// - [timestamp] -> The timestamp of the scrobble (optional). + /// - [duration_ms] -> The duration of the track in milliseconds (optional). + /// - [isrc] -> The ISRC code of the track (optional). + fun scrobble(details: Map) { + // TODO: Implement scrobbling + } +} +``` + +| Method | Description | Returns | +| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `checkUpdate()` | Checks for updates to the plugin. | `Future` with a map containing `downloadUrl`, `version`, and optionally `changelog`. If no update is available, returns `null`. | +| `support` | Returns support information. | `string` containing the support information in Markdown or plain text. | +| `scrobble()` | [Scrobbles][scrobbling_wiki] the provided track details. This is only called if your plugin.json has scrobbling in the `abilities` field | `void` | + +> In the `checkUpdate()` method the `plugin.json`'s content will be passed as map. You can use that to check updates using the `version` field. +> +> Also, the `downloadUrl` it provides should be a direct binary download link (redirect is supported) for the `.smplug` file + +{/* Urls */} +[scrobbling_wiki]: https://en.wikipedia.org/wiki/Last.fm +[hetu_script_import_export_docs]: https://hetu-script.github.io/docs/en-US/grammar/import/ +[hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin +[spotube_plugin_api]: / +[hetu_std]: https://github.com/hetu-community/hetu_std +[dart_stream_controller]: https://api.flutter.dev/flutter/dart-async/StreamController-class.html +[hetu_struct_into_map]: https://hetu-script.github.io/docs/en-US/api_reference/hetu/#struct +[SpotubeUserObject]: /docs/reference/models#user +[SpotubePaginationResponseObject]: /docs/reference/models#pagination-response +[SpotubeFullAlbumObject]: /docs/reference/models#spotubefullalbumobject +[SpotubeFullArtistObject]: /docs/reference/models#spotubefullartistobject +[SpotubeFullTrackObject]: /docs/reference/models#track +[SpotubeFullPlaylistObject]: /docs/reference/models#spotubefullplaylistobject +[SpotubeSearchResponseObject]: /docs/reference/models#search-response +[SpotubeBrowseSectionObject]: /docs/reference/models#browse-section diff --git a/website/src/content/docs/developing-plugins/playlist-endpoint.mdx b/website/src/content/docs/developing-plugins/playlist-endpoint.mdx deleted file mode 100644 index 137f4a60..00000000 --- a/website/src/content/docs/developing-plugins/playlist-endpoint.mdx +++ /dev/null @@ -1,81 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The PlaylistEndpoint -description: "" -order: 8 ---- - -The PlaylistEndpoint is used to fetch playlist information and do track-related actions. In the `src/segments/playlist.ht` file you can find all the -required method definitions. - -```hetu_script -class PlaylistEndpoint { - var client: HttpClient - - construct (this.client) - - fun getPlaylist(id: string) { - // TODO: Implement method - } - - fun tracks(id: string, { offset: int, limit: int }) { - // TODO: Implement method - } - - fun create(userId: string, { - name: string, - description: string, - public: bool, - collaborative: bool - }) { - // TODO: Implement method - } - - fun update(playlistId: string, { - name: string, - description: string, - public: bool, - collaborative: bool - }) { - // TODO: Implement method - } - - fun deletePlaylist(playlistId: string) { - // TODO: Implement method - } - - fun addTracks(playlistId: string, { trackIds: List, position: int }) { - // TODO: Implement method - } - - - fun removeTracks(playlistId: string, { trackIds: List }) { - // TODO: Implement method - } - - fun save(playlistId: string) { - // TODO: Implement method - } - - fun unsave(playlistId: string) { - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ---------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| `getPlaylist` | Fetches a playlist by its ID. | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | -| `tracks` | Fetches tracks in a playlist. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | -| `create` | Creates a new playlist and returns | [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | -| `update` | Updates an existing playlist. | `void` | -| `deletePlaylist` | Deletes a playlist. | `void` | -| `addTracks` | Adds tracks to a playlist. | `void` | -| `removeTracks` | Removes tracks from a playlist. | `void` | -| `save` | Saves a playlist to the user's library. | `void` | -| `unsave` | Removes a playlist from the user's library. | `void` | - -{/* Urls */} -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/search-endpoint.mdx b/website/src/content/docs/developing-plugins/search-endpoint.mdx deleted file mode 100644 index 4411aa77..00000000 --- a/website/src/content/docs/developing-plugins/search-endpoint.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The SearchEndpoint -description: "" -order: 9 ---- - -The SearchEndpoint is used to fetch search playlist, tracks, album and artists. In the `src/segments/search.ht` file you can find all the -required method definitions. - -```hetu_script -class SearchEndpoint { - var client: HttpClient - - construct (this.client) - - get chips -> List { // Set - // can be tracks, playlists, artists, albums and all - return ["all", "tracks", "albums", "artists", "playlists"] - } - - fun all(query: string) { - // TODO: Implement method - } - - fun albums(query: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun artists(query: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun tracks(query: string, {offset: int, limit: int}) { - // TODO: Implement method - } - - fun playlists(query: string, {offset: int, limit: int}) { - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ------------- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| `chips` | Returns the available search chips. | `List` | -| `all()` | Searches for all types of content. | [`SpotubeSearchResponseObject`][SpotubeSearchResponseObject] | -| `albums()` | Searches only for albums. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | -| `artists()` | Searches only for artists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | -| `tracks()` | Searches only for tracks. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | -| `playlists()` | Searches only for playlists. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | - -{/* Urls */} -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeSearchResponseObject]: /docs/models/spotube-search-response-object -[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject diff --git a/website/src/content/docs/developing-plugins/track-endpoint.mdx b/website/src/content/docs/developing-plugins/track-endpoint.mdx deleted file mode 100644 index d6e34695..00000000 --- a/website/src/content/docs/developing-plugins/track-endpoint.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The TrackEndpoint -description: "" -order: 5 ---- - -The TrackEndpoint is used to fetch track information and do track-related actions. In the `src/segments/track.ht` file you can find all the -required method definitions. - -```hetu_script -class TrackEndpoint { - var client: HttpClient - - construct (this.client) - - fun getTrack(id: string) { - // TODO: Implement method - } - - fun save(trackIds: List) { // List - // TODO: Implement method - } - - fun unsave(trackIds: List) { // List - // TODO: Implement method - } - - fun radio(id: string) { - // TODO: Implement method - } -} -``` - -| Method | Description | Returns | -| ------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------- | -| `getTrack()` | Fetches track information by ID. | [SpotubeFullTrackObject][SpotubeFullTrackObject] | -| `save()` | Saves the specified tracks. Accepts a list of track IDs. | void | -| `unsave()` | Removes the specified tracks from saved tracks. Accepts a list of track IDs. | void | -| `radio()` | Fetches related tracks based on specified tracks. Try to return a List of 50 tracks. | [List\][SpotubeFullTrackObject] | - -{/* Urls */} -[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/developing-plugins/user-endpoint.mdx b/website/src/content/docs/developing-plugins/user-endpoint.mdx deleted file mode 100644 index 4b8efb33..00000000 --- a/website/src/content/docs/developing-plugins/user-endpoint.mdx +++ /dev/null @@ -1,83 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: The UserEndpoint -description: "" -order: 4 ---- - -The UserEndpoint is used to fetch user information and manage user-related actions. -In the `src/segments/user.ht` file you can find all the required method definitions. These are the necessary -methods Spotube calls in its lifecycle. - -> Most of these methods should be just a mapping to an API call with minimum latency. Avoid calling plugin APIs like WebView or Forms -> in these methods. User interactions should be avoided here generally. - -```hetu_script -class UserEndpoint { - var client: HttpClient - - construct (this.client) - - fun me() { - // TODO: Implement method - } - - fun savedTracks({ offset: int, limit: int }) { - // TODO: Implement method - } - - fun savedPlaylists({ offset: int, limit: int }) { - // TODO: Implement method - } - - fun savedAlbums({ offset: int, limit: int }) { - // TODO: Implement method - } - - fun savedArtists({ offset: int, limit: int }) { - // TODO: Implement method - } - - fun isSavedPlaylist(playlistId: string) { // Future - // TODO: Implement method - } - - fun isSavedTracks(trackIds: List) { // Future> - // TODO: Implement method - } - - fun isSavedAlbums(albumIds: List) { // Future> - // TODO: Implement method - } - - fun isSavedArtists(artistIds: List) { // Future> - // TODO: Implement method - } -} -``` - -These methods are pretty self-explanatory. You need to implement them to fetch user information from your service. - -| Method | Description | Returns | -| ------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- | -| `me()` | Fetches the current user's information. | [`SpotubeUserObject`][SpotubeUserObject] | -| `savedTracks()` | Fetches the user's saved tracks with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | -| `savedPlaylists()` | Fetches the user's saved playlists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] | -| `savedAlbums()` | Fetches the user's saved albums with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] | -| `savedArtists()` | Fetches the user's saved artists with pagination support. | [`SpotubePaginationResponseObject`][SpotubePaginationResponseObject] of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | -| `isSavedPlaylist()` | Checks if a playlist is saved by the user. Returns a `Future`. | `bool` | -| `isSavedTracks()` | Checks if tracks are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to a track ID) | -| `isSavedAlbums()` | Checks if albums are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an album ID) | -| `isSavedArtists()` | Checks if artists are saved by the user. Returns a `Future>`. | `List` (each boolean corresponds to an artist ID) | - -> Note: The `isSavedTracks`, `isSavedAlbums`, and `isSavedArtists` methods accept a list of IDs and return a list of booleans -> indicating whether each item is saved by the user. The order of the booleans in the list corresponds to the order of the IDs -> in the input list. - -{/* Links */} -[SpotubeUserObject]: /docs/models/spotube-user-object -[SpotubePaginationResponseObject]: /docs/models/spotube-pagination-response-object -[SpotubeFullTrackObject]: /docs/models/spotube-track-object -[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject diff --git a/website/src/content/docs/models/spotube-album-object.mdx b/website/src/content/docs/models/spotube-album-object.mdx deleted file mode 100644 index 29197e90..00000000 --- a/website/src/content/docs/models/spotube-album-object.mdx +++ /dev/null @@ -1,42 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Album -description: Different types of album objects used in Spotube. -order: 3 ---- - -### SpotubeSimpleAlbumObject - -Following is the structure of the `SpotubeAlbumObject`: - -| Property | Type | -| ----------- | ---------------------------------------------------------------- | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] | -| albumType | `album`, `single` or `compilation` | -| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | -| releaseDate | `string` (YYYY-MM-DD format) or `null` | - -{/* Urls */} - -### SpotubeFullAlbumObject - -Following is the structure of the `SpotubeFullAlbumObject`: - -| Property | Type | -| ----------- | ---------------------------------------------------------------- | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] | -| albumType | `album`, `single` or `compilation` | -| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | -| releaseDate | `string` (YYYY-MM-DD format) | -| totalTracks | `number` | -| recordLabel | `string` or `null` | - -{/* Urls */} -[SpotubeImageObject]: /docs/models/spotube-image-object -[SpotubeSimpleArtistObject]: /docs/models/spotube-artist-objects#spotubesimpleartistobject diff --git a/website/src/content/docs/models/spotube-artist-object.mdx b/website/src/content/docs/models/spotube-artist-object.mdx deleted file mode 100644 index ab1716db..00000000 --- a/website/src/content/docs/models/spotube-artist-object.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Artist -description: Different types of artist objects used in Spotube. -order: 2 ---- - -### SpotubeSimpleArtistObject - -Following is the structure of the `SpotubeArtistObject`: - -| Property | Type | -| ----------- | ------------------------------------------------------------ | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | - -### SpotubeFullArtistObject - -Following is the structure of the `SpotubeFullArtistObject`: - -| Property | Type | -| ----------- | ----------------------------------------------------- | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] or | -| followers | `number` | -| genres | List of `string` or `null` | - -{/* Urls */} -[SpotubeImageObject]: /docs/models/spotube-image-object diff --git a/website/src/content/docs/models/spotube-browse-section-object.mdx b/website/src/content/docs/models/spotube-browse-section-object.mdx deleted file mode 100644 index 29368605..00000000 --- a/website/src/content/docs/models/spotube-browse-section-object.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Browse Section -description: "" -order: 7 ---- - -Following is the structure of `SpotubeBrowseSectionObject`: - -| Property | Type | -| ----------- | ---------------- | -| id | `string` | -| title | `string` | -| externalUri | `string` | -| browseMore | `boolean` | -| items | List of `Object` | - -The `items` property array can contain multiple type of `Object` in it but it will always be - -- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] -- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] -- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] - -{/* Urls */} -[SpotubeFullPlaylistObject]: /docs/models/spotube-playlist-object#spotubefullplaylistobject -[SpotubeFullAlbumObject]: /docs/models/spotube-album-object#spotubefullalbumobject -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject diff --git a/website/src/content/docs/models/spotube-image-object.mdx b/website/src/content/docs/models/spotube-image-object.mdx deleted file mode 100644 index 665e5d0a..00000000 --- a/website/src/content/docs/models/spotube-image-object.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Image -description: How images are represented in Spotube. -order: 0 ---- - -Following is the structure of the `SpotubeImageObject`: - -| Property | Type | -| -------- | --------------- | -| width | `int` or `null` | -| height | `int` or `null` | -| url | `string` | diff --git a/website/src/content/docs/models/spotube-pagination-response-object.mdx b/website/src/content/docs/models/spotube-pagination-response-object.mdx deleted file mode 100644 index a5efebc3..00000000 --- a/website/src/content/docs/models/spotube-pagination-response-object.mdx +++ /dev/null @@ -1,18 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Pagination Response -description: "" -order: 8 ---- - -`SpotubePaginationResponseObject` is generic model. The `items` property can contain any type of `Object` in it. - -This is the structure of `SpotubePaginationResponseObject`: - -| Property | Type | -| ---------- | ----------------------------------------------- | -| limit | `number` | -| nextOffset | `number` or `null` | -| total | `number` | -| hasMore | `boolean` | -| items | List of generic type `T` which extends `Object` | diff --git a/website/src/content/docs/models/spotube-playlist-object.mdx b/website/src/content/docs/models/spotube-playlist-object.mdx deleted file mode 100644 index 3783e596..00000000 --- a/website/src/content/docs/models/spotube-playlist-object.mdx +++ /dev/null @@ -1,40 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Playlist -description: Different types of playlist objects used in Spotube. -order: 5 ---- - -### SpotubeSimplePlaylistObject - -Following is the structure of the `SpotubeSimplePlaylistObject`: - -| Property | Type | -| ----------- | ------------------------------------------------------------ | -| id | `string` | -| name | `string` | -| description | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | -| owner | [`SpotubeUserObject`][SpotubeUserObject] | - -### SpotubeFullPlaylistObject - -Following is the structure of the `SpotubeFullPlaylistObject`: - -| Property | Type | -| ------------- | ------------------------------------------------------------ | -| id | `string` | -| name | `string` | -| description | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | -| owner | [`SpotubeUserObject`][SpotubeUserObject] | -| collaborators | List of [`SpotubeUserObject`][SpotubeUserObject] or `null` | -| collaborative | `boolean` | -| public | `boolean` | - -{/* Urls */} - -[SpotubeImageObject]: /docs/models/spotube-image-object -[SpotubeUserObject]: /docs/models/spotube-simple-user-object diff --git a/website/src/content/docs/models/spotube-search-response-object.mdx b/website/src/content/docs/models/spotube-search-response-object.mdx deleted file mode 100644 index efe51a26..00000000 --- a/website/src/content/docs/models/spotube-search-response-object.mdx +++ /dev/null @@ -1,21 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Search Response -description: "" -order: 6 ---- - -Following is the structure of the `SpotubeSearchResponseObject`: - -| Property | Type | -| --------- | -------------------------------------------------------------------- | -| albums | List of [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | -| artists | List of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | -| playlists | List of [`SpotubeSimplePlaylistObject`][SpotubeSimplePlaylistObject] | -| tracks | List of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | - -{/* Urls */} -[SpotubeSimpleAlbumObject]: /docs/models/spotube-album-object#spotubesimplealbumobject -[SpotubeFullArtistObject]: /docs/models/spotube-artist-object#spotubefullartistobject -[SpotubeSimplePlaylistObject]: /docs/models/spotube-playlist-object#spotubesimpleplaylistobject -[SpotubeFullTrackObject]: /docs/models/spotube-track-object diff --git a/website/src/content/docs/models/spotube-track-object.mdx b/website/src/content/docs/models/spotube-track-object.mdx deleted file mode 100644 index 3e4d8b92..00000000 --- a/website/src/content/docs/models/spotube-track-object.mdx +++ /dev/null @@ -1,27 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: Track -description: "" -order: 4 ---- - -Following is the structure of the `SpotubeFullTrackObject`: - -| Property | Type | -| ---------------------------- | ---------------------------------------------------------------- | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | -| album | [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | -| durationMs (in milliseconds) | `number` | -| explicit | `boolean` | -| [isrc][isrc_wiki] | `string` | - -> `isrc` stands for International Standard Recording Code, which is a unique identifier for tracks. -> It is used to identify recordings and is often used in music distribution and royalty collection. The format is typically a 12-character alphanumeric code. - -{/* Urls */} -[SpotubeSimpleArtistObject]: /docs/models/spotube-artist-objects#spotubesimpleartistobject -[SpotubeSimpleAlbumObject]: /docs/models/spotube-album-object#spotubesimplealbumobject -[isrc_wiki]: https://en.wikipedia.org/wiki/International_Standard_Recording_Code diff --git a/website/src/content/docs/models/spotube-user-object.mdx b/website/src/content/docs/models/spotube-user-object.mdx deleted file mode 100644 index 9cd32f25..00000000 --- a/website/src/content/docs/models/spotube-user-object.mdx +++ /dev/null @@ -1,20 +0,0 @@ ---- -layout: "layouts/DocLayout.astro" -title: User -description: "" -order: 1 ---- - -Structure of the SpotubeUserObject, which is used to represent a user in Spotube returned by Spotube Plugins. - -| Property | Type | -| ----------- | -------------------------------------------------- | -| id | `string` | -| name | `string` | -| externalUri | `string` | -| images | List of [`SpotubeImageObject`][SpotubeImageObject] | - -> `externalUri` is a URL that points to the user's profile on the external service (e.g. Listenbrainz) - -{/* Urls */} -[SpotubeImageObject]: /docs/models/spotube-image-object diff --git a/website/src/content/docs/reference/models.mdx b/website/src/content/docs/reference/models.mdx new file mode 100644 index 00000000..8e29bb6c --- /dev/null +++ b/website/src/content/docs/reference/models.mdx @@ -0,0 +1,190 @@ +--- +layout: "layouts/DocLayout.astro" +title: Plugin Models +description: "Different types of objects used in Spotube." +order: 0 +--- + +## Image + +Following is the structure of the `SpotubeImageObject`: + +| Property | Type | +| -------- | --------------- | +| width | `int` or `null` | +| height | `int` or `null` | +| url | `string` | + +## User + +Structure of the `SpotubeUserObject`, which is used to represent a user in Spotube returned by Spotube Plugins. + +| Property | Type | +| ----------- | -------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | + +> `externalUri` is a URL that points to the user's profile on the external service (e.g. Listenbrainz) + +## Artist + +### SpotubeSimpleArtistObject + +Following is the structure of the `SpotubeArtistObject`: + +| Property | Type | +| ----------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | + +### SpotubeFullArtistObject + +Following is the structure of the `SpotubeFullArtistObject`: + +| Property | Type | +| ----------- | ----------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or | +| followers | `number` | +| genres | List of `string` or `null` | + +## Album + +### SpotubeSimpleAlbumObject + +Following is the structure of the `SpotubeAlbumObject`: + +| Property | Type | +| ----------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | +| albumType | `album`, `single` or `compilation` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| releaseDate | `string` (YYYY-MM-DD format) or `null` | + +### SpotubeFullAlbumObject + +Following is the structure of the `SpotubeFullAlbumObject`: + +| Property | Type | +| ----------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] | +| albumType | `album`, `single` or `compilation` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| releaseDate | `string` (YYYY-MM-DD format) | +| totalTracks | `number` | +| recordLabel | `string` or `null` | + +## Track + +Following is the structure of the `SpotubeFullTrackObject`: + +| Property | Type | +| ---------------------------- | ---------------------------------------------------------------- | +| id | `string` | +| name | `string` | +| externalUri | `string` | +| artists | List of [`SpotubeSimpleArtistObject`][SpotubeSimpleArtistObject] | +| album | [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | +| durationMs (in milliseconds) | `number` | +| explicit | `boolean` | +| [isrc][isrc_wiki] | `string` | + +> `isrc` stands for International Standard Recording Code, which is a unique identifier for tracks. +> It is used to identify recordings and is often used in music distribution and royalty collection. The format is typically a 12-character alphanumeric code. + +## Playlist + +### SpotubeSimplePlaylistObject + +Following is the structure of the `SpotubeSimplePlaylistObject`: + +| Property | Type | +| ----------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| description | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | +| owner | [`SpotubeUserObject`][SpotubeUserObject] | + +### SpotubeFullPlaylistObject + +Following is the structure of the `SpotubeFullPlaylistObject`: + +| Property | Type | +| ------------- | ------------------------------------------------------------ | +| id | `string` | +| name | `string` | +| description | `string` | +| externalUri | `string` | +| images | List of [`SpotubeImageObject`][SpotubeImageObject] or `null` | +| owner | [`SpotubeUserObject`][SpotubeUserObject] | +| collaborators | List of [`SpotubeUserObject`][SpotubeUserObject] or `null` | +| collaborative | `boolean` | +| public | `boolean` | + +## Search Response + +Following is the structure of the `SpotubeSearchResponseObject`: + +| Property | Type | +| --------- | -------------------------------------------------------------------- | +| albums | List of [`SpotubeSimpleAlbumObject`][SpotubeSimpleAlbumObject] | +| artists | List of [`SpotubeFullArtistObject`][SpotubeFullArtistObject] | +| playlists | List of [`SpotubeSimplePlaylistObject`][SpotubeSimplePlaylistObject] | +| tracks | List of [`SpotubeFullTrackObject`][SpotubeFullTrackObject] | + +## Browse Section + +Following is the structure of `SpotubeBrowseSectionObject`: + +| Property | Type | +| ----------- | ---------------- | +| id | `string` | +| title | `string` | +| externalUri | `string` | +| browseMore | `boolean` | +| items | List of `Object` | + +The `items` property array can contain multiple type of `Object` in it but it will always be + +- [`SpotubeFullPlaylistObject`][SpotubeFullPlaylistObject] +- [`SpotubeFullAlbumObject`][SpotubeFullAlbumObject] +- [`SpotubeFullArtistObject`][SpotubeFullArtistObject] + +## Pagination Response + +`SpotubePaginationResponseObject` is generic model. The `items` property can contain any type of `Object` in it. + +This is the structure of `SpotubePaginationResponseObject`: + +| Property | Type | +| ---------- | ----------------------------------------------- | +| limit | `number` | +| nextOffset | `number` or `null` | +| total | `number` | +| hasMore | `boolean` | +| items | List of generic type `T` which extends `Object` | + +[isrc_wiki]: https://en.wikipedia.org/wiki/International_Standard_Recording_Code +[SpotubeImageObject]: /docs/reference/models#image +[SpotubeSimpleArtistObject]: /docs/reference/models#spotubesimpleartistobject +[SpotubeSimpleAlbumObject]: /docs/reference/models#spotubesimplealbumobject +[SpotubeUserObject]: /docs/reference/models#user +[SpotubeFullArtistObject]: /docs/reference/models#spotubefullartistobject +[SpotubeSimplePlaylistObject]: /docs/reference/models#spotubesimpleplaylistobject +[SpotubeFullTrackObject]: /docs/reference/models#track +[SpotubeFullPlaylistObject]: /docs/reference/models#spotubefullplaylistobject +[SpotubeFullAlbumObject]: /docs/reference/models#spotubefullalbumobject From 30fd4acf3752a428583dfa40c554d8de12a4b487 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 21:21:09 +0600 Subject: [PATCH 12/19] docs: add plugin API docs --- .../components/navigation/DocSideBar.astro | 7 +- .../src/components/navigation/TopBar.astro | 5 +- .../components/navigation/sidebar-button.tsx | 2 +- .../src/content/docs/plugin-apis/forms.mdx | 72 ++++++++++++ .../content/docs/plugin-apis/localstorage.mdx | 103 ++++++++++++++++++ .../src/content/docs/plugin-apis/timezone.mdx | 37 +++++++ .../src/content/docs/plugin-apis/webview.mdx | 75 +++++++++++++ 7 files changed, 292 insertions(+), 9 deletions(-) create mode 100644 website/src/content/docs/plugin-apis/forms.mdx create mode 100644 website/src/content/docs/plugin-apis/localstorage.mdx create mode 100644 website/src/content/docs/plugin-apis/timezone.mdx create mode 100644 website/src/content/docs/plugin-apis/webview.mdx diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index c5a24823..d30b8fac 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -58,13 +58,8 @@ const sections: [ ][] = [ ["Get Started", "get-started/", queryCollection], ["Developing Plugins", "developing-plugins/", queryCollection], + ["Plugin APIs", "plugin-apis/", queryCollection], ["Reference", "reference/", queryCollection], - // ["Design System", "design/", queryCollection], - // ["Tailwind Components", "tailwind/", queryCollection], - // ["Functional Components", "components/", queryMetaCollection], - // ["Headless Components", "headless/", queryCollection], - // ["Integrations", "integrations/", queryMetaCollection], - // ["Resources", "resources/", queryCollection], ]; // Build navigation dynamically diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 3da5feab..98319f28 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -4,10 +4,11 @@ import { FaGithub } from "react-icons/fa6"; import SidebarButton from "./sidebar-button"; const pathname = Astro.url.pathname; -console.log("pathname:", pathname); --- -
+
diff --git a/website/src/components/navigation/sidebar-button.tsx b/website/src/components/navigation/sidebar-button.tsx index 96139917..850f320b 100644 --- a/website/src/components/navigation/sidebar-button.tsx +++ b/website/src/components/navigation/sidebar-button.tsx @@ -13,7 +13,7 @@ export default function SidebarButton() { return <>
diff --git a/website/src/content/docs/plugin-apis/forms.mdx b/website/src/content/docs/plugin-apis/forms.mdx new file mode 100644 index 00000000..43199023 --- /dev/null +++ b/website/src/content/docs/plugin-apis/forms.mdx @@ -0,0 +1,72 @@ +--- +layout: "layouts/DocLayout.astro" +title: Forms +description: Documentation for the Forms API for spotube plugins +order: 1 +--- + +Spotube provides a Forms API that allows plugin developers to create and manage forms within the Spotube application. + +## Usage + +Following will show a form with 2 text fields and text in between them: + +```hetu_script +import "module:spotube_plugin" as spotube + +spotube.SpotubeForm.show( + "The form page title", + [ + { + objectType: "input", + id: "name", + variant: "text", + placeholder: "Enter your name", + required: true, + }.toJson(), + { + objectType: "input", + id: "password", + variant: "password", // This will obfuscate the input + placeholder: "Enter your password", + required: true, + }.toJson(), + { + objectType: "text", + text: "This is some text after the two fields.", + }.toJson(), + ] +).then((result) { + // Handle the result + print(result) +}) +``` + +The method `spotube.SpotubeForm.show` takes a title and a list of form field declaration map. The map should be, well obviously a `Map`. +Following are field map properties: + +| Property | Type | Description | +| -------------- | ----------------- | ---------------------------------------------------------------------------------- | +| `objectType` | `text` or `input` | Type of the object, should be `text` for text fields and `input` for input fields. | +| `id` | `String` | Unique identifier for the field. (`input` type only) | +| `variant` | `String` | Variant of the field, can be `text`, `password` or `number`. (`input` type only) | +| `placeholder` | `String` | Optional placeholder text for the field. (`input` type only) | +| `required` | `Boolean` | Whether the field is required or not. (`input` type only) | +| `defaultValue` | `String` | Optional default value for the field. (`input` type only) | +| `regex` | `String` | Optional regex pattern to validate the input. (`input` type only) | +| `text` | `String` | Optional text for `text` object type. (Only applicable for `text` type) | + +The method `spotube.SpotubeForm.show` returns a following format: + +```json +[ + { + "id": "name", + "value": "John Doe" + }, + { + "id": "password", + "value": "12345678" + } +] +``` diff --git a/website/src/content/docs/plugin-apis/localstorage.mdx b/website/src/content/docs/plugin-apis/localstorage.mdx new file mode 100644 index 00000000..9df39757 --- /dev/null +++ b/website/src/content/docs/plugin-apis/localstorage.mdx @@ -0,0 +1,103 @@ +--- +layout: "layouts/DocLayout.astro" +title: LocalStorage +description: Documentation for the LocalStorage API for spotube plugins +order: 2 +--- + +The `LocalStorage` API is a plain text key/value holding persistent storage for spotube plugins. It's similar to the `localStorage` API in web browsers. +The API is a 1:1 port of [shared_preferences][shared_preferences] package from [pub.dev](https://pub.dev) (Flutter package registry) + +The only difference is that the `LocalStorage` API is 100% asynchronous. So every method returns a `Future` + +## Usage + +#### Get values + +Retrieve stored information by key: + +```hetu_script +import "module:spotube_plugin" as spotube + +var LocalStorage = spotube.LocalStorage + +// Get a string value by key +LocalStorage.getString("key").then((value) { + print("Value for 'key': $value") +}) + +// Get an integer value by key +LocalStorage.getInt("key").then((value) { + print("Value for 'key': $value") +}) + +// Get a double value by key +LocalStorage.getDouble("key").then((value) { + print("Value for 'key': $value") +}) + +// Get a boolean value by key +LocalStorage.getBool("key").then((value) { + print("Value for 'key': $value") +}) + +// Get a list of strings by key +LocalStorage.getStringList("key").then((value) { + for (var item in value) { + print("Item in list: $item") + } +}) +``` + +#### Set values + +To set or store data in the local storage, you can use the following methods: + +```hetu_script +// Set a string value by key +LocalStorage.setString("key", "value") + +// Set an integer value by key +LocalStorage.setInt("key", 42) + +// Set a double value by key +LocalStorage.setDouble("key", 3.14) + +// Set a boolean value by key +LocalStorage.setBool("key", true) + +// Set a list of strings by key +LocalStorage.setStringList("key", ["item1", "item2", "item3"]) +``` + +#### Key operations + +To remove a value from the local storage, you can use the `remove` method: + +```hetu_script +// Remove a value by key +LocalStorage.remove("key") +``` + +To clear all values from the local storage, you can use the `clear` method: + +```hetu_script +// Clear all values from local storage +LocalStorage.clear() +``` + +To check if a key exists in the local storage, you can use the `containsKey` method: + +```hetu_script +// Check if a key exists +LocalStorage.containsKey("key").then((exists) { + if (exists) { + print("Key 'key' exists in local storage") + } else { + print("Key 'key' does not exist in local storage") + } +}) +``` + +{/* Links */} +[shared_preferences]: https://pub.dev/packages/shared_preferences diff --git a/website/src/content/docs/plugin-apis/timezone.mdx b/website/src/content/docs/plugin-apis/timezone.mdx new file mode 100644 index 00000000..bafe0220 --- /dev/null +++ b/website/src/content/docs/plugin-apis/timezone.mdx @@ -0,0 +1,37 @@ +--- +layout: "layouts/DocLayout.astro" +title: TimeZone +description: Documentation for the TimeZone API for spotube plugins +order: 3 +--- + +The `TimeZone` API provides access to the current time zone of the device running Spotube. This can be useful for plugins that need to display +or handle time-related information based on the user's local time zone. + +## Usage + +To use the `TimeZone` API, you can import the `spotube_plugin` module and access the `TimeZone` class. + +```hetu_script +import "module:spotube_plugin" as spotube + +var TimeZone = spotube.TimeZone +``` + +To get current local time zone for the device, you can use the `getLocalTimeZone` method: + +```hetu_script +TimeZone.getLocalTimeZone().then((timeZone) { + print("Current local time zone: $timeZone") // e.g., "America/New_York" +}) +``` + +To get all available time zones, you can use the `getAvailableTimeZones` method: + +```hetu_script +TimeZone.getAvailableTimeZones().then((timeZones) { + for (var tz in timeZones) { + print("Available time zone: $tz") // e.g., "America/New_York", "Europe/London", etc. + } +}) +``` diff --git a/website/src/content/docs/plugin-apis/webview.mdx b/website/src/content/docs/plugin-apis/webview.mdx new file mode 100644 index 00000000..775e262a --- /dev/null +++ b/website/src/content/docs/plugin-apis/webview.mdx @@ -0,0 +1,75 @@ +--- +layout: "layouts/DocLayout.astro" +title: WebView +description: Documentation for the WebView API for spotube plugins +order: 0 +--- + +The [hetu_spotube_plugin][hetu_spotube_plugin] is a built-in module that plugin developers can use in their plugins. + +```hetu_script +import "module:spotube_plugin" as spotube +``` + +## WebView API + +The WebView API allows plugins to create and manage web views within the Spotube application. + +### Usage + +First, an WebView instance needs to be created with `uri`. + +```hetu_script +import "module:spotube_plugin" as spotube + +let webview = spotube.Webview(uri: "https://example.com") +``` + +To open the webview, you can use the `open` method: + +```hetu_script +webview.open() // returns Future +``` + +To close the webview, you can use the `close` method: + +```hetu_script +webview.close() // returns Future +``` + +### Listening to URL changes + +You can listen to url change events by using the `onUrlRequestStream` method. It's emitted when the URL of the webview changes, +such as when the user navigates to a different page or clicks a link. + +```hetu_script +// Make sure to import the hetu_std and Stream +import "module:hetu_std" as std + +var Stream = std.Stream + +// ... created webview instance and other stuff + +var subscription = webview.onUrlRequestStream().listen((url) { + // Handle the URL change + print("URL changed to: $url") +}) + +// Don't forget to cancel the subscription when it's no longer needed +subscription.cancel() +``` + +### Retrieving cookies + +To get cookies from the webview, you can use the `getCookies` method: + +```hetu_script +webview.getCookies("https://example.com") // returns Future> +``` + +You can find the [`Cookie` class][spotube_plugin_cookie] and all it's methods and properties in the +`hetu_spotube_plugin` module source code + +{/* Links */} +[hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin +[spotube_plugin_cookie]: https://github.com/KRTirtho/hetu_spotube_plugin/blob/main/lib/assets/hetu/webview.ht From 3f18f35c0b572968de19409ae5aa8b56931c5ab6 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 14 Aug 2025 22:20:09 +0600 Subject: [PATCH 13/19] docs: add search bar and libraries page --- website/astro.config.mjs | 3 +- website/package.json | 1 + website/pnpm-lock.yaml | 93 +++++++++++++++++++ .../components/navigation/DocSideBar.astro | 37 +++----- .../src/components/navigation/TopBar.astro | 11 +++ .../src/content/docs/reference/libraries.mdx | 15 +++ website/src/styles/global.css | 63 +++++++++++++ 7 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 website/src/content/docs/reference/libraries.mdx diff --git a/website/astro.config.mjs b/website/astro.config.mjs index f1a228e2..cbc0582d 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,6 +5,7 @@ import react from "@astrojs/react"; import mdx from "@astrojs/mdx"; import rehypeSlug from "rehype-slug"; import rehypeAutolinkHeadings from "rehype-autolink-headings"; +import pagefind from "astro-pagefind"; // https://astro.build/config export default defineConfig({ @@ -40,7 +41,7 @@ export default defineConfig({ ], ], }, - integrations: [react(), mdx()], + integrations: [react(), mdx(), pagefind()], redirects: { "/docs": "/docs/get-started/introduction", "/docs/get-started": "/docs/get-started/introduction", diff --git a/website/package.json b/website/package.json index 8298f851..9f1eb71a 100644 --- a/website/package.json +++ b/website/package.json @@ -17,6 +17,7 @@ "@types/react": "^19.1.9", "@types/react-dom": "^19.1.7", "astro": "^5.12.8", + "astro-pagefind": "^1.8.3", "date-fns": "^4.1.0", "markdown-it": "^14.1.0", "react": "^19.1.1", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index d0f27f9e..d297e8c6 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: astro: specifier: ^5.12.8 version: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) + astro-pagefind: + specifier: ^1.8.3 + version: 1.8.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)) date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -542,6 +545,37 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@pagefind/darwin-arm64@1.3.0': + resolution: {integrity: sha512-365BEGl6ChOsauRjyVpBjXybflXAOvoMROw3TucAROHIcdBvXk9/2AmEvGFU0r75+vdQI4LJdJdpH4Y6Yqaj4A==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.3.0': + resolution: {integrity: sha512-zlGHA23uuXmS8z3XxEGmbHpWDxXfPZ47QS06tGUq0HDcZjXjXHeLG+cboOy828QIV5FXsm9MjfkP5e4ZNbOkow==} + cpu: [x64] + os: [darwin] + + '@pagefind/default-ui@1.3.0': + resolution: {integrity: sha512-CGKT9ccd3+oRK6STXGgfH+m0DbOKayX6QGlq38TfE1ZfUcPc5+ulTuzDbZUnMo+bubsEOIypm4Pl2iEyzZ1cNg==} + + '@pagefind/linux-arm64@1.3.0': + resolution: {integrity: sha512-8lsxNAiBRUk72JvetSBXs4WRpYrQrVJXjlRRnOL6UCdBN9Nlsz0t7hWstRk36+JqHpGWOKYiuHLzGYqYAqoOnQ==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.3.0': + resolution: {integrity: sha512-hAvqdPJv7A20Ucb6FQGE6jhjqy+vZ6pf+s2tFMNtMBG+fzcdc91uTw7aP/1Vo5plD0dAOHwdxfkyw0ugal4kcQ==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-x64@1.3.0': + resolution: {integrity: sha512-BR1bIRWOMqkf8IoU576YDhij1Wd/Zf2kX/kCI0b2qzCKC8wcc2GQJaaRMCpzvCCrmliO4vtJ6RITp/AnoYUUmQ==} + cpu: [x64] + os: [win32] + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -985,6 +1019,11 @@ packages: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} hasBin: true + astro-pagefind@1.8.3: + resolution: {integrity: sha512-Nfo1TdlEHdkXTiI0KpimLqX6awK3qWTil7IOJvk5Q8x+0VBTpIEp9QvGgoAxXDe3upAHLVsg4y7U1uUPm7GC9w==} + peerDependencies: + astro: ^2.0.4 || ^3 || ^4 || ^5 + astro@5.12.8: resolution: {integrity: sha512-KkJ7FR+c2SyZYlpakm48XBiuQcRsrVtdjG5LN5an0givI/tLik+ePJ4/g3qrAVhYMjJOxBA2YgFQxANPiWB+Mw==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} @@ -1796,6 +1835,10 @@ packages: package-manager-detector@1.3.0: resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + pagefind@1.3.0: + resolution: {integrity: sha512-8KPLGT5g9s+olKMRTU9LFekLizkVIu9tes90O1/aigJ0T5LmyPqTzGJrETnSw3meSYg58YH7JTzhTTW/3z6VAw==} + hasBin: true + pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} @@ -1984,6 +2027,10 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -2048,6 +2095,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -2819,6 +2870,25 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@pagefind/darwin-arm64@1.3.0': + optional: true + + '@pagefind/darwin-x64@1.3.0': + optional: true + + '@pagefind/default-ui@1.3.0': {} + + '@pagefind/linux-arm64@1.3.0': + optional: true + + '@pagefind/linux-x64@1.3.0': + optional: true + + '@pagefind/windows-x64@1.3.0': + optional: true + + '@polka/url@1.0.0-next.29': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/pluginutils@5.2.0(rollup@4.46.2)': @@ -3313,6 +3383,13 @@ snapshots: astring@1.9.0: {} + astro-pagefind@1.8.3(astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2)): + dependencies: + '@pagefind/default-ui': 1.3.0 + astro: 5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2) + pagefind: 1.3.0 + sirv: 3.0.1 + astro@5.12.8(@types/node@24.1.0)(jiti@2.5.1)(lightningcss@1.30.1)(rollup@4.46.2)(typescript@5.9.2): dependencies: '@astrojs/compiler': 2.12.2 @@ -4527,6 +4604,14 @@ snapshots: package-manager-detector@1.3.0: {} + pagefind@1.3.0: + optionalDependencies: + '@pagefind/darwin-arm64': 1.3.0 + '@pagefind/darwin-x64': 1.3.0 + '@pagefind/linux-arm64': 1.3.0 + '@pagefind/linux-x64': 1.3.0 + '@pagefind/windows-x64': 1.3.0 + pako@0.2.9: {} parse-entities@4.0.2: @@ -4851,6 +4936,12 @@ snapshots: is-arrayish: 0.3.2 optional: true + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + sisteransi@1.0.5: {} smol-toml@1.4.1: {} @@ -4916,6 +5007,8 @@ snapshots: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 + totalist@3.0.1: {} + tr46@0.0.3: {} trim-lines@3.0.1: {} diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index d30b8fac..f4456c33 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -18,7 +18,7 @@ interface Props { classList?: string; bottomGroups?: NavigationGroup[]; } -const { topGroups, bottomGroups, classList } = Astro.props; +const { classList } = Astro.props; const sortByOrder = (a: CollectionEntry<"docs">, b: CollectionEntry<"docs">) => a.data.order - b.data.order; @@ -33,17 +33,6 @@ async function queryCollection(startsWith: string) { }) ).toSorted(sortByOrder); } - -async function queryMetaCollection(startsWith: string) { - return ( - await getCollection("docs", (entry) => { - if (!entry.id.startsWith(startsWith)) return false; - if (!entry.id.endsWith("meta")) return false; - return true; - }) - ).toSorted(sortByOrder); -} - const toNavItems = (entries: CollectionEntry<"docs">[]) => entries.map((page) => ({ title: page.data.title, @@ -63,16 +52,16 @@ const sections: [ ]; // Build navigation dynamically -const navigation: NavigationGroup[] = [ - ...(topGroups ?? []), - ...(await Promise.all( - sections.map(async ([title, prefix, queryFn]) => ({ - title, - items: toNavItems(await queryFn(prefix)), - })) - )), - ...(bottomGroups ?? []), -]; +const navigation: NavigationGroup[] = await Promise.all( + sections.map(async ([title, prefix, queryFn]) => ({ + title, + items: toNavItems(await queryFn(prefix)), + })) +); + +const pathname = Astro.url.pathname.endsWith("/") + ? Astro.url.pathname.slice(0, -1) + : Astro.url.pathname; ---
+ + + Date: Thu, 14 Aug 2025 22:29:12 +0600 Subject: [PATCH 14/19] docs: fix invalid links and redirection --- website/astro.config.mjs | 3 +++ .../docs/developing-plugins/implementing-endpoints.mdx | 2 +- website/src/content/docs/get-started/installing-plugins.mdx | 6 ++++-- website/src/content/docs/reference/libraries.mdx | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index cbc0582d..ab0ba99e 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -45,5 +45,8 @@ export default defineConfig({ redirects: { "/docs": "/docs/get-started/introduction", "/docs/get-started": "/docs/get-started/introduction", + "/docs/developing-plugins": "/docs/developing-plugins/introduction", + "/docs/plugin-apis": "/docs/plugin-apis/webview", + "/docs/reference": "/docs/reference/models", }, }); diff --git a/website/src/content/docs/developing-plugins/implementing-endpoints.mdx b/website/src/content/docs/developing-plugins/implementing-endpoints.mdx index dd564995..2c75c037 100644 --- a/website/src/content/docs/developing-plugins/implementing-endpoints.mdx +++ b/website/src/content/docs/developing-plugins/implementing-endpoints.mdx @@ -479,10 +479,10 @@ class CorePlugin { [scrobbling_wiki]: https://en.wikipedia.org/wiki/Last.fm [hetu_script_import_export_docs]: https://hetu-script.github.io/docs/en-US/grammar/import/ [hetu_spotube_plugin]: https://github.com/KRTirtho/hetu_spotube_plugin -[spotube_plugin_api]: / [hetu_std]: https://github.com/hetu-community/hetu_std [dart_stream_controller]: https://api.flutter.dev/flutter/dart-async/StreamController-class.html [hetu_struct_into_map]: https://hetu-script.github.io/docs/en-US/api_reference/hetu/#struct +[spotube_plugin_api]: /docs/plugin-apis [SpotubeUserObject]: /docs/reference/models#user [SpotubePaginationResponseObject]: /docs/reference/models#pagination-response [SpotubeFullAlbumObject]: /docs/reference/models#spotubefullalbumobject diff --git a/website/src/content/docs/get-started/installing-plugins.mdx b/website/src/content/docs/get-started/installing-plugins.mdx index 3c82095b..872506f0 100644 --- a/website/src/content/docs/get-started/installing-plugins.mdx +++ b/website/src/content/docs/get-started/installing-plugins.mdx @@ -12,7 +12,7 @@ Let's first learn how to install plugins in Spotube. It's pretty simple. 1. Then go to the top option, "Metadata provider plugins" 1. You can see a list of all the plugins that are available to install -![Lmafo](/docs/getting-started/installing-plugins/navigate.webp) +![Navigate to plugins page](/docs/getting-started/installing-plugins/navigate.webp) ## More ways to install new plugins @@ -25,4 +25,6 @@ Try to use the `Official` tagged plugins all the time if you don't want to deal - **Upload plugin from local file**: You can also install plugins from local file (plugin.smplug) using the _Orange Upload button_ on the top right beside the text field. - **Install plugin from URL**: If you have a direct link to a plugin file, you can just paste the URL in the text field and use the gray download button beside it -> If you're a developer, you can create your own plugins and share them with the community. Check out the [Plugin Development Guide](/docs/developing-plugins) for more information. +> If you're a developer, you can create your own plugins and share them with the community. Check out the [Plugin Development Guide][developing_plugins] for more information. + +[developing_plugins]: /docs/developing-plugins/introduction diff --git a/website/src/content/docs/reference/libraries.mdx b/website/src/content/docs/reference/libraries.mdx index 8297c912..149b5264 100644 --- a/website/src/content/docs/reference/libraries.mdx +++ b/website/src/content/docs/reference/libraries.mdx @@ -2,7 +2,7 @@ layout: "layouts/DocLayout.astro" title: Libraries description: List of libraries for Spotube Plugins. -order: 0 +order: 1 --- - [`hetu_std`][hetu_std] (built-in) - A standard library for hetu_script that provides standard set of functions and utilities. From edcd784335c7c0a8e036ef582684814fd39c0a06 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 15 Aug 2025 11:19:43 +0600 Subject: [PATCH 15/19] website: update button styles and enhance background in global CSS --- .../src/components/navigation/TopBar.astro | 6 +-- .../src/modules/downloads/download-item.astro | 2 +- website/src/pages/downloads/index.astro | 2 +- website/src/pages/index.astro | 25 +++++++++---- website/src/styles/global.css | 37 +++++++++++-------- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index ef00448e..9a6136ff 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -8,7 +8,7 @@ const pathname = Astro.url.pathname; ---

diff --git a/website/src/styles/global.css b/website/src/styles/global.css index 41bcdff8..5773035e 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -7,6 +7,23 @@ @import "@skeletonlabs/skeleton/optional/presets"; @import "@skeletonlabs/skeleton/themes/wintry"; +body { + background-image: radial-gradient( + at 50% 0%, + var(--color-secondary-100-900) 0px, + transparent 75% + ), + radial-gradient( + at 100% 0%, + var(--color-tertiary-300-700) 0px, + transparent 50% + ); + background-attachment: fixed; + background-position: center; + background-repeat: no-repeat; + background-size: cover; +} + h1, h2, h3, @@ -22,11 +39,11 @@ h6 { } .prose code:not(pre code) { - @apply bg-surface-100-900 px-1 py-0.5 rounded-sm; + @apply bg-primary-100-900 px-1 py-0.5 rounded-sm text-primary-900-100; } .prose a code { - @apply text-primary-500 underline decoration-primary-500; + @apply text-primary-500! underline decoration-primary-500; } /* Astro PageFind */ @@ -54,7 +71,7 @@ h6 { border-bottom-right-radius: var(--pagefind-ui-border-radius); border-bottom-left-radius: var(--pagefind-ui-border-radius); - @apply bg-gray-50 dark:bg-surface-900; + @apply bg-white dark:bg-surface-900; } .pagefind-ui .pagefind-ui__result-link { @@ -66,19 +83,9 @@ h6 { } .pagefind-ui .pagefind-ui__search-input { - background-color: var(--color-white) !important; + @apply bg-white/50! dark:bg-surface-900/50!; } .pagefind-ui .pagefind-ui__search-clear { - background: var(--color-white) !important; -} - -@media (prefers-color-scheme: dark) { - .pagefind-ui .pagefind-ui__search-input { - background-color: var(--color-surface-900) !important; - } - - .pagefind-ui .pagefind-ui__search-clear { - background: var(--color-surface-900) !important; - } + @apply bg-inherit!; } From a2894db6528918e1ae01ce86c8908395a3d5b1fe Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 15 Aug 2025 13:49:41 +0600 Subject: [PATCH 16/19] website: add ads and mobile screen adaptability --- website/src/components/ads/Ads.astro | 38 ++++++++++++++++ website/src/components/drawer/drawer.tsx | 45 +++++++++++++++++++ .../src/components/navigation/TopBar.astro | 18 ++++++-- .../components/navigation/sidebar-button.tsx | 4 +- website/src/layouts/RootLayout.astro | 7 +++ website/src/pages/downloads/index.astro | 7 +-- .../src/pages/downloads/nightly/index.astro | 5 ++- .../src/pages/downloads/packages/index.mdx | 10 +++-- website/src/pages/index.astro | 6 ++- website/src/styles/global.css | 1 + 10 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 website/src/components/ads/Ads.astro create mode 100644 website/src/components/drawer/drawer.tsx diff --git a/website/src/components/ads/Ads.astro b/website/src/components/ads/Ads.astro new file mode 100644 index 00000000..b2e4bb27 --- /dev/null +++ b/website/src/components/ads/Ads.astro @@ -0,0 +1,38 @@ +--- +interface Props { + adSlot: number; + adFormat: "auto" | "fluid"; + fullWidthResponsive?: boolean; + style?: string; + adLayout?: "in-article" | "in-feed" | "in-page"; + adLayoutKey?: string; +} + +const { + adSlot, + adFormat, + fullWidthResponsive = true, + style, + adLayout, + adLayoutKey, +} = Astro.props; + +const AD_CLIENT = "ca-pub-6419300932495863"; +--- + + + + diff --git a/website/src/components/drawer/drawer.tsx b/website/src/components/drawer/drawer.tsx new file mode 100644 index 00000000..4f61cd65 --- /dev/null +++ b/website/src/components/drawer/drawer.tsx @@ -0,0 +1,45 @@ +import React, { useState } from "react"; +import { LuMenu } from "react-icons/lu"; + + + +interface DrawerProps { + buttonLabel?: React.ReactNode; + children: React.ReactNode; + className?: string; +} + + +export const Drawer: React.FC = ({ + buttonLabel = , + children, + className = "", +}) => { + const [open, setOpen] = useState(false); + + return ( + <> + + + + {/* Drawer */} +
+ +
{children}
+
+ + ); +}; diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 9a6136ff..5b33d80a 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -3,17 +3,27 @@ import { routes } from "~/collections/app"; import { FaGithub } from "react-icons/fa6"; import SidebarButton from "./sidebar-button"; import Search from "astro-pagefind/components/Search"; +import { Drawer } from "../drawer/drawer"; +import DocSideBar from "./DocSideBar.astro"; const pathname = Astro.url.pathname; ---
- -

+ { + pathname.startsWith("/docs") ? ( + + + + ) : ( + + ) + } +

Spotube Logo Spotube @@ -31,7 +41,7 @@ const pathname = Astro.url.pathname; /> diff --git a/website/src/components/navigation/sidebar-button.tsx b/website/src/components/navigation/sidebar-button.tsx index 850f320b..5b4a20e4 100644 --- a/website/src/components/navigation/sidebar-button.tsx +++ b/website/src/components/navigation/sidebar-button.tsx @@ -13,7 +13,7 @@ export default function SidebarButton() { return <>
@@ -34,7 +34,7 @@ export default function SidebarButton() { }


- + diff --git a/website/src/pages/downloads/nightly/index.astro b/website/src/pages/downloads/nightly/index.astro index 0e79c8c6..30536300 100644 --- a/website/src/pages/downloads/nightly/index.astro +++ b/website/src/pages/downloads/nightly/index.astro @@ -1,6 +1,7 @@ --- import { LuBug, LuSparkles, LuTriangleAlert } from "react-icons/lu"; -import { extendedNightlyDownloadLinks } from "~/collections/app"; +import { ADS_SLOTS, extendedNightlyDownloadLinks } from "~/collections/app"; +import Ads from "~/components/ads/Ads.astro"; import RootLayout from "~/layouts/RootLayout.astro"; import DownloadItems from "~/modules/downloads/download-item.astro"; --- @@ -41,7 +42,7 @@ import DownloadItems from "~/modules/downloads/download-item.astro";
- +
diff --git a/website/src/pages/downloads/packages/index.mdx b/website/src/pages/downloads/packages/index.mdx index 13f2587c..d2d6c4ea 100644 --- a/website/src/pages/downloads/packages/index.mdx +++ b/website/src/pages/downloads/packages/index.mdx @@ -1,6 +1,8 @@ import { FaLinux, FaWindows, FaApple } from 'react-icons/fa6'; import RootLayout from 'layouts/RootLayout.astro'; import MarkdownLayout from 'layouts/MarkdownLayout.astro'; +import Ads from 'components/ads/Ads.astro'; +import { ADS_SLOTS } from 'collections/app'; @@ -25,13 +27,13 @@ import MarkdownLayout from 'layouts/MarkdownLayout.astro'; ```bash $ paru -Sy spotube-bin ``` - {/* */} + /> ## MacOS ### Homebrew🍻 Spotube can be installed through Homebrew. We host our own cask definition thus you'll need to add our tap first: @@ -39,13 +41,13 @@ import MarkdownLayout from 'layouts/MarkdownLayout.astro'; $ brew tap krtirtho/apps $ brew install --cask spotube ``` - {/* */} + /> ## Windows ### Chocolatey🍫 Spotube is available in [community.chocolatey.org](https://community.chocolatey.org) repo. If you have chocolatey install in your system just run following command in an Elevated Command Prompt or PowerShell: diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 72a71257..180aefc8 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -3,6 +3,8 @@ import { FaAndroid, FaApple, FaLinux, FaWindows } from "react-icons/fa6"; import RootLayout from "../layouts/RootLayout.astro"; import { LuDownload, LuHeart } from "react-icons/lu"; import { Supporters } from "~/modules/root/supporters"; +import Ads from "~/components/ads/Ads.astro"; +import { ADS_SLOTS } from "~/collections/app"; --- @@ -58,7 +60,7 @@ import { Supporters } from "~/modules/root/supporters";

- +
@@ -82,6 +84,6 @@ import { Supporters } from "~/modules/root/supporters";

- + diff --git a/website/src/styles/global.css b/website/src/styles/global.css index 5773035e..eb6ed28e 100644 --- a/website/src/styles/global.css +++ b/website/src/styles/global.css @@ -56,6 +56,7 @@ h6 { --pagefind-ui-border-radius: 0.5rem; width: 50%; + @apply hidden md:block; } .pagefind-ui .pagefind-ui__drawer:not(.pagefind-ui__hidden) { From dbba55606bd8308ee42ff73ea607bfe2743218fe Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 15 Aug 2025 19:21:40 +0600 Subject: [PATCH 17/19] website: fix astro not compiling because it can't detect server-side component --- website/src/components/drawer/Drawer.astro | 43 ++++++++++++++ website/src/components/drawer/drawer.tsx | 45 -------------- .../components/navigation/DocSideBar.astro | 58 ++----------------- .../src/components/navigation/TopBar.astro | 6 +- website/src/pages/docs/[...slug]/index.astro | 5 ++ website/src/utils/get-collection.ts | 57 ++++++++++++++++++ 6 files changed, 110 insertions(+), 104 deletions(-) create mode 100644 website/src/components/drawer/Drawer.astro delete mode 100644 website/src/components/drawer/drawer.tsx create mode 100644 website/src/utils/get-collection.ts diff --git a/website/src/components/drawer/Drawer.astro b/website/src/components/drawer/Drawer.astro new file mode 100644 index 00000000..eba12c8c --- /dev/null +++ b/website/src/components/drawer/Drawer.astro @@ -0,0 +1,43 @@ +--- +import { LuMenu } from "react-icons/lu"; +--- + + + +
+ +
+ +
+
+ + diff --git a/website/src/components/drawer/drawer.tsx b/website/src/components/drawer/drawer.tsx deleted file mode 100644 index 4f61cd65..00000000 --- a/website/src/components/drawer/drawer.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { useState } from "react"; -import { LuMenu } from "react-icons/lu"; - - - -interface DrawerProps { - buttonLabel?: React.ReactNode; - children: React.ReactNode; - className?: string; -} - - -export const Drawer: React.FC = ({ - buttonLabel = , - children, - className = "", -}) => { - const [open, setOpen] = useState(false); - - return ( - <> - - - - {/* Drawer */} -
- -
{children}
-
- - ); -}; diff --git a/website/src/components/navigation/DocSideBar.astro b/website/src/components/navigation/DocSideBar.astro index f4456c33..83a08bc9 100644 --- a/website/src/components/navigation/DocSideBar.astro +++ b/website/src/components/navigation/DocSideBar.astro @@ -1,63 +1,13 @@ --- -import type { HTMLAttributes } from "astro/types"; -import type { CollectionEntry } from "astro:content"; -import { getCollection } from "astro:content"; - -interface NavigationItem extends HTMLAttributes<"a"> { - title: string; - tag?: string; -} - -interface NavigationGroup { - title: string; - items: NavigationItem[]; -} +import { getNavigationCollection } from "~/utils/get-collection"; interface Props { - topGroups?: NavigationGroup[]; - classList?: string; - bottomGroups?: NavigationGroup[]; + classList?: string[]; } + const { classList } = Astro.props; -const sortByOrder = (a: CollectionEntry<"docs">, b: CollectionEntry<"docs">) => - a.data.order - b.data.order; - -async function queryCollection(startsWith: string) { - return ( - await getCollection("docs", (entry) => { - if (!entry.id.startsWith(startsWith)) return false; - if (entry.id.split("/").length > 2) return false; - if (entry.id.endsWith("meta")) return false; - return true; - }) - ).toSorted(sortByOrder); -} -const toNavItems = (entries: CollectionEntry<"docs">[]) => - entries.map((page) => ({ - title: page.data.title, - href: `/docs/${page.id}`, - })); - -// Define navigation sections -const sections: [ - string, - string, - (prefix: string) => Promise[]>, -][] = [ - ["Get Started", "get-started/", queryCollection], - ["Developing Plugins", "developing-plugins/", queryCollection], - ["Plugin APIs", "plugin-apis/", queryCollection], - ["Reference", "reference/", queryCollection], -]; - -// Build navigation dynamically -const navigation: NavigationGroup[] = await Promise.all( - sections.map(async ([title, prefix, queryFn]) => ({ - title, - items: toNavItems(await queryFn(prefix)), - })) -); +const navigation = await getNavigationCollection(); const pathname = Astro.url.pathname.endsWith("/") ? Astro.url.pathname.slice(0, -1) diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index 5b33d80a..cbe03e19 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -3,8 +3,6 @@ import { routes } from "~/collections/app"; import { FaGithub } from "react-icons/fa6"; import SidebarButton from "./sidebar-button"; import Search from "astro-pagefind/components/Search"; -import { Drawer } from "../drawer/drawer"; -import DocSideBar from "./DocSideBar.astro"; const pathname = Astro.url.pathname; --- @@ -16,9 +14,7 @@ const pathname = Astro.url.pathname;
{ pathname.startsWith("/docs") ? ( - - - +
) : ( ) diff --git a/website/src/pages/docs/[...slug]/index.astro b/website/src/pages/docs/[...slug]/index.astro index be019bdb..5a4fbc0b 100644 --- a/website/src/pages/docs/[...slug]/index.astro +++ b/website/src/pages/docs/[...slug]/index.astro @@ -3,6 +3,8 @@ import RootLayout from "layouts/RootLayout.astro"; import type { GetStaticPaths } from "astro"; import { render } from "astro:content"; import { getCollection, getEntry } from "astro:content"; +import DocSideBar from "~/components/navigation/DocSideBar.astro"; +import Drawer from "~/components/drawer/Drawer.astro"; export const getStaticPaths = (async () => { const pages = await getCollection("docs"); @@ -29,5 +31,8 @@ if (page.id.startsWith("components/") || page.id.startsWith("integrations/")) { --- + + + diff --git a/website/src/utils/get-collection.ts b/website/src/utils/get-collection.ts new file mode 100644 index 00000000..9e0ade80 --- /dev/null +++ b/website/src/utils/get-collection.ts @@ -0,0 +1,57 @@ +import type { HTMLAttributes } from "astro/types"; +import { getCollection, type CollectionEntry } from "astro:content"; + +interface NavigationItem extends HTMLAttributes<"a"> { + title: string; + tag?: string; +} + +export interface NavigationGroup { + title: string; + items: NavigationItem[]; +} + +function sortByOrder(a: CollectionEntry<"docs">, b: CollectionEntry<"docs">) { + return a.data.order - b.data.order; +} + +async function queryCollection(startsWith: string) { + return ( + await getCollection("docs", (entry) => { + if (!entry.id.startsWith(startsWith)) return false; + if (entry.id.split("/").length > 2) return false; + if (entry.id.endsWith("meta")) return false; + return true; + }) + ).toSorted(sortByOrder); +} +function toNavItems(entries: CollectionEntry<"docs">[]) { + return entries.map((page) => ({ + title: page.data.title, + href: `/docs/${page.id}`, + })); +} + +export async function getNavigationCollection() { + // Define navigation sections + const sections: [ + string, + string, + (prefix: string) => Promise[]> + ][] = [ + ["Get Started", "get-started/", queryCollection], + ["Developing Plugins", "developing-plugins/", queryCollection], + ["Plugin APIs", "plugin-apis/", queryCollection], + ["Reference", "reference/", queryCollection], + ]; + + // Build navigation dynamically + const navigation: NavigationGroup[] = await Promise.all( + sections.map(async ([title, prefix, queryFn]) => ({ + title, + items: toNavItems(await queryFn(prefix)), + })) + ); + + return navigation; +} From 089a3445a16bc52bfc81fa31516994225100d21a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 15 Aug 2025 20:43:12 +0600 Subject: [PATCH 18/19] website: add SEO metadata --- .../src/components/navigation/TopBar.astro | 2 +- website/src/layouts/RootLayout.astro | 36 ++++++++++++++++--- website/src/pages/docs/[...slug]/index.astro | 7 +++- website/src/pages/index.astro | 2 +- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/website/src/components/navigation/TopBar.astro b/website/src/components/navigation/TopBar.astro index cbe03e19..a2b6598a 100644 --- a/website/src/components/navigation/TopBar.astro +++ b/website/src/components/navigation/TopBar.astro @@ -14,7 +14,7 @@ const pathname = Astro.url.pathname;
{ pathname.startsWith("/docs") ? ( -
+
) : ( ) diff --git a/website/src/layouts/RootLayout.astro b/website/src/layouts/RootLayout.astro index c3bcb271..497efa84 100644 --- a/website/src/layouts/RootLayout.astro +++ b/website/src/layouts/RootLayout.astro @@ -2,6 +2,17 @@ import { FaGithub } from "react-icons/fa6"; import "../styles/global.css"; import TopBar from "~/components/navigation/TopBar.astro"; + +interface Props { + metadata?: { + title?: string; + description?: string; + keywords?: string; + author?: string; + }; +} + +const { metadata } = Astro.props as Props; --- @@ -11,16 +22,33 @@ import TopBar from "~/components/navigation/TopBar.astro"; - Spotube + {metadata?.title || "Spotube"} - + + + + + + + + + + + + diff --git a/website/src/pages/docs/[...slug]/index.astro b/website/src/pages/docs/[...slug]/index.astro index 5a4fbc0b..73c3be7e 100644 --- a/website/src/pages/docs/[...slug]/index.astro +++ b/website/src/pages/docs/[...slug]/index.astro @@ -30,7 +30,12 @@ if (page.id.startsWith("components/") || page.id.startsWith("integrations/")) { } --- - + diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 180aefc8..af569c38 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -14,7 +14,7 @@ import { ADS_SLOTS } from "~/collections/app";

Spotube


- A cross-platform Extensible open-source Music Streaming platform + A cross-platform extensible open-source music streaming platform