website: remove vercel trashes
@ -1,13 +0,0 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
@ -1,31 +0,0 @@
|
||||
/** @type { import("eslint").Linter.Config } */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:svelte/recommended',
|
||||
'prettier'
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
33
website/.gitignore
vendored
@ -1,11 +1,24 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
# build output
|
||||
dist/
|
||||
|
||||
# generated types
|
||||
.astro/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.netlify
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# jetbrains setting folder
|
||||
.idea/
|
||||
|
@ -1 +1 @@
|
||||
20.11.0
|
||||
22.17.0
|
@ -1 +0,0 @@
|
||||
engine-strict=true
|
@ -1,4 +0,0 @@
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
4
website/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
11
website/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
120
website/.vscode/settings.json
vendored
@ -1,120 +0,0 @@
|
||||
{
|
||||
"prettier.documentSelectors": [
|
||||
"**/*.svelte"
|
||||
],
|
||||
"tailwindCSS.classAttributes": [
|
||||
"class",
|
||||
"accent",
|
||||
"active",
|
||||
"animIndeterminate",
|
||||
"aspectRatio",
|
||||
"background",
|
||||
"badge",
|
||||
"bgBackdrop",
|
||||
"bgDark",
|
||||
"bgDrawer",
|
||||
"bgLight",
|
||||
"blur",
|
||||
"border",
|
||||
"button",
|
||||
"buttonAction",
|
||||
"buttonBack",
|
||||
"buttonClasses",
|
||||
"buttonComplete",
|
||||
"buttonDismiss",
|
||||
"buttonNeutral",
|
||||
"buttonNext",
|
||||
"buttonPositive",
|
||||
"buttonTextCancel",
|
||||
"buttonTextConfirm",
|
||||
"buttonTextFirst",
|
||||
"buttonTextLast",
|
||||
"buttonTextNext",
|
||||
"buttonTextPrevious",
|
||||
"buttonTextSubmit",
|
||||
"caretClosed",
|
||||
"caretOpen",
|
||||
"chips",
|
||||
"color",
|
||||
"controlSeparator",
|
||||
"controlVariant",
|
||||
"cursor",
|
||||
"display",
|
||||
"element",
|
||||
"fill",
|
||||
"fillDark",
|
||||
"fillLight",
|
||||
"flex",
|
||||
"flexDirection",
|
||||
"gap",
|
||||
"gridColumns",
|
||||
"height",
|
||||
"hover",
|
||||
"inactive",
|
||||
"indent",
|
||||
"justify",
|
||||
"meter",
|
||||
"padding",
|
||||
"position",
|
||||
"regionAnchor",
|
||||
"regionBackdrop",
|
||||
"regionBody",
|
||||
"regionCaption",
|
||||
"regionCaret",
|
||||
"regionCell",
|
||||
"regionChildren",
|
||||
"regionChipList",
|
||||
"regionChipWrapper",
|
||||
"regionCone",
|
||||
"regionContent",
|
||||
"regionControl",
|
||||
"regionDefault",
|
||||
"regionDrawer",
|
||||
"regionFoot",
|
||||
"regionFootCell",
|
||||
"regionFooter",
|
||||
"regionHead",
|
||||
"regionHeadCell",
|
||||
"regionHeader",
|
||||
"regionIcon",
|
||||
"regionInput",
|
||||
"regionInterface",
|
||||
"regionInterfaceText",
|
||||
"regionLabel",
|
||||
"regionLead",
|
||||
"regionLegend",
|
||||
"regionList",
|
||||
"regionListItem",
|
||||
"regionNavigation",
|
||||
"regionPage",
|
||||
"regionPanel",
|
||||
"regionRowHeadline",
|
||||
"regionRowMain",
|
||||
"regionSummary",
|
||||
"regionSymbol",
|
||||
"regionTab",
|
||||
"regionTrail",
|
||||
"ring",
|
||||
"rounded",
|
||||
"select",
|
||||
"shadow",
|
||||
"slotDefault",
|
||||
"slotFooter",
|
||||
"slotHeader",
|
||||
"slotLead",
|
||||
"slotMessage",
|
||||
"slotMeta",
|
||||
"slotPageContent",
|
||||
"slotPageFooter",
|
||||
"slotPageHeader",
|
||||
"slotSidebarLeft",
|
||||
"slotSidebarRight",
|
||||
"slotTrail",
|
||||
"spacing",
|
||||
"text",
|
||||
"track",
|
||||
"transition",
|
||||
"width",
|
||||
"zIndex"
|
||||
]
|
||||
}
|
@ -1,38 +1,46 @@
|
||||
# create-svelte
|
||||
# Astro Starter Kit: Basics
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
pnpm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
pnpm create svelte@latest my-app
|
||||
```sh
|
||||
pnpm create astro@latest -- --template basics
|
||||
```
|
||||
|
||||
## Developing
|
||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
||||
|
||||
Once you've created a project and installed dependencies with `pnpm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
## 🚀 Project Structure
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
Inside of your Astro project, you'll see the following folders and files:
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
pnpm run dev -- --open
|
||||
```text
|
||||
/
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
├── src
|
||||
│ ├── assets
|
||||
│ │ └── astro.svg
|
||||
│ ├── components
|
||||
│ │ └── Welcome.astro
|
||||
│ ├── layouts
|
||||
│ │ └── Layout.astro
|
||||
│ └── pages
|
||||
│ └── index.astro
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Building
|
||||
To learn more about the folder structure of an Astro project, refer to [our guide on project structure](https://docs.astro.build/en/basics/project-structure/).
|
||||
|
||||
To create a production version of your app:
|
||||
## 🧞 Commands
|
||||
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
You can preview the production build with `pnpm run preview`.
|
||||
| Command | Action |
|
||||
| :------------------------ | :----------------------------------------------- |
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro -- --help` | Get help using the Astro CLI |
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
## 👀 Want to learn more?
|
||||
|
||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
||||
|
15
website/astro.config.mjs
Normal file
@ -0,0 +1,15 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import react from '@astrojs/react';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
|
||||
integrations: [react()]
|
||||
});
|
@ -1,74 +1,27 @@
|
||||
{
|
||||
"name": "website",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "playwright test",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.41.2",
|
||||
"@skeletonlabs/skeleton": "2.8.0",
|
||||
"@skeletonlabs/tw-plugin": "0.3.1",
|
||||
"@sveltejs/adapter-cloudflare": "^4.1.0",
|
||||
"@sveltejs/kit": "^2.5.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@tailwindcss/typography": "0.5.10",
|
||||
"@types/eslint": "8.56.0",
|
||||
"@types/node": "^20.11.16",
|
||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^6.21.0",
|
||||
"autoprefixer": "10.4.17",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"mdsvex": "^0.11.0",
|
||||
"postcss": "8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.10",
|
||||
"svelte-check": "^3.6.3",
|
||||
"tailwindcss": "3.4.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.0",
|
||||
"vite-plugin-tailwind-purgecss": "0.2.0"
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.6.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@octokit/openapi-types": "^22.2.0",
|
||||
"@octokit/rest": "^21.0.2",
|
||||
"date-fns": "^3.3.1",
|
||||
"highlight.js": "11.9.0",
|
||||
"lucide-svelte": "^0.323.0",
|
||||
"mdsvex-relative-images": "^1.0.3",
|
||||
"rehype-auto-ads": "^1.2.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-container": "^0.1.2",
|
||||
"remark-external-links": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-github": "^12.0.0",
|
||||
"remark-reading-time": "^1.0.1",
|
||||
"svelte-fa": "^4.0.2",
|
||||
"svelte-markdown": "^0.4.1"
|
||||
"@astrojs/react": "^4.3.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",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.4.0+sha512.6b849d0787d97f8f4e1f03a9b8ff8f038e79e153d6f11ae539ae7c435ff9e796df6a862c991502695c7f9e8fac8aeafc1ac5a8dab47e36148d183832d886dd52",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@fortawesome/fontawesome-common-types",
|
||||
"@fortawesome/free-brands-svg-icons",
|
||||
"@sveltejs/kit",
|
||||
"esbuild",
|
||||
"svelte-preprocess"
|
||||
]
|
||||
"devDependencies": {
|
||||
"@skeletonlabs/skeleton": "^3.1.7"
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
command: 'npm run build && npm run preview',
|
||||
port: 4173
|
||||
},
|
||||
testDir: 'tests',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
};
|
||||
|
||||
export default config;
|
2
website/pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@tailwindcss/oxide'
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
---
|
||||
title: From Idea to Impact
|
||||
author: Prottoy Roy
|
||||
date: 2024-12-22
|
||||
published: true
|
||||
cover_img: /images/from-idea-to-impact/cover.jpg
|
||||
---
|
||||
|
||||
> An school magazine article by the beloved brother of the founder of the Spotube app
|
||||
|
||||
In the vibrant city of Narayanganj, Dhaka, Bangladesh, a young man named Kingkor Roy Tirtho was carving out his path in the world of technology. Currently a second-year Computer Science and Engineering (CSE) student at East West University, Kingkor had always been captivated by the magic of coding. From a young age, he spent countless hours tinkering with computers, teaching himself programming languages and exploring the digital realm.
|
||||
|
||||
Kingkor's passion wasn't just about writing code; it was about solving problems and creating innovative solutions. Inspired by the way technology could enhance everyday life, he dreamed of building apps that would bring joy and convenience to users. His dedication was evident; he often participated in hackathons and coding competitions, where he showcased his talent and creativity.
|
||||
|
||||
The turning point in his journey came when he envisioned an app that would revolutionize music streaming. With millions of people seeking accessible music, he wanted to create a platform that could bridge gaps and provide a seamless experience. Thus, Spotube was born.
|
||||
|
||||
Initially, Kingkor faced numerous challenges. Balancing his academic responsibilities with app development was no easy feat. There were nights filled with coding, debugging, and sleepless hours fueled by caffeine and determination. Despite setbacks and moments of self-doubt, Kingkor remained resilient. He sought feedback, learned from criticisms, and continually iterated on his project.
|
||||
|
||||
As Spotube gained traction, it garnered attention for its user-friendly interface and innovative features. Kingkor’s ability to blend technical skills with an understanding of user needs made the app a hit among music lovers. He received positive reviews, not just for the functionality, but for the passion evident in his work.
|
||||
|
||||
Kingkor’s story is one of perseverance and innovation. He embodies the spirit of a new generation of tech enthusiasts who believe that with dedication, anything is possible. His journey serves as an inspiration to his peers at East West University and beyond, reminding them that the intersection of creativity and technology can lead to remarkable achievements.
|
||||
|
||||
Today, Kingkor continues to evolve as a developer, always looking for ways to improve Spotube and explore new ideas. His story illustrates that genius isn't just about raw talent; it's about hard work, resilience, and the willingness to dream big. Kingkor Roy Tirtho is a shining example of what can be achieved when passion meets perseverance, and he is just getting international attentions.
|
||||
|
||||
Here is some key features of Spotube:
|
||||
|
||||
1. **Seamless Music Streaming**: Spotube offers a smooth streaming experience with a vast library of tracks, allowing users to easily find and play their favorite songs.
|
||||
1. **Offline Listening**: Users can download their favorite tracks for offline playback, making it convenient to enjoy music anytime, anywhere, without relying on an internet connection.
|
||||
1. **User-Friendly Interface**: The app is designed with an intuitive interface, making navigation easy for users of all ages. Its clean layout ensures a pleasant user experience.
|
||||
1. **Cross-Platform Compatibility**: Spotube is accessible on multiple devices, enabling users to enjoy their music on smartphones, tablets, and desktops seamlessly.
|
||||
1. **Personalized Playlists**: Users can create and manage their playlists, helping them curate their listening experience based on their mood and preferences.
|
||||
1. **Social Sharing Features**: The app allows users to share their favorite tracks and playlists with friends and family, fostering a community of music lovers.
|
||||
1. **Regular Updates**: Spotube is continually updated with new features and improvements based on user feedback, reflecting Kingkor's commitment to enhancing the app's performance and user satisfaction.
|
||||
1. **Global Reach**: With its growing popularity, Spotube is gaining attention worldwide, attracting users from various countries and cultures, showcasing Kingkor’s vision of accessible music for everyone. He's recently got mentioned in a Spanish well known magazine for his invention.
|
||||
|
||||
As Spotube continues to evolve, Kingkor Roy Tirtho's innovative approach is positioning him and his app as significant players in the music streaming landscape, capturing the attention of users and industry experts alike.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 390 B After Width: | Height: | Size: 390 B |
Before Width: | Height: | Size: 777 B After Width: | Height: | Size: 777 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
26
website/src/app.d.ts
vendored
@ -1,26 +0,0 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
// and what to do when importing types
|
||||
declare namespace App {
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Error {}
|
||||
// interface Platform {}
|
||||
interface Platform {
|
||||
env: {
|
||||
COUNTER: DurableObjectNamespace;
|
||||
};
|
||||
context: {
|
||||
waitUntil(promise: Promise<any>): void;
|
||||
};
|
||||
caches: CacheStorage & { default: Cache };
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace globalThis {
|
||||
declare var adsbygoogle: any[];
|
||||
}
|
||||
|
||||
declare module "@fortawesome/pro-solid-svg-icons/index.es" {
|
||||
export * from "@fortawesome/pro-solid-svg-icons";
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
|
||||
<!-- favicon 32x32 -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%sveltekit.assets%/favicon-32x32.png" />
|
||||
<!-- favicon 16x16 -->
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="%sveltekit.assets%/favicon-16x16.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<!-- Apple icons -->
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/apple-touch-icon.png" />
|
||||
<!-- Android Chrome -->
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-6419300932495863"
|
||||
data-overlays="bottom" crossorigin="anonymous"></script>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" data-theme="wintry">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,23 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind variants;
|
||||
|
||||
/* vintage theme */
|
||||
@font-face {
|
||||
font-family: 'Abril Fatface';
|
||||
src: url('/fonts/AbrilFatface.ttf');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
.text-stroke {
|
||||
text-shadow:
|
||||
-1px -1px 0 #000,
|
||||
1px -1px 0 #000,
|
||||
-1px 1px 0 #000,
|
||||
1px 1px 0 #000,
|
||||
-1px 0 0 #000,
|
||||
1px 0 0 #000,
|
||||
0 -1px 0 #000,
|
||||
0 1px 0 #000;
|
||||
}
|
@ -1,66 +1,66 @@
|
||||
import type { IconType } from "react-icons";
|
||||
import {
|
||||
faAndroid,
|
||||
faApple,
|
||||
faDebian,
|
||||
faFedora,
|
||||
faOpensuse,
|
||||
faUbuntu,
|
||||
faWindows,
|
||||
faRedhat,
|
||||
} from "@fortawesome/free-brands-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/free-brands-svg-icons/index";
|
||||
import { Home, Newspaper, Download } from "lucide-svelte";
|
||||
FaAndroid,
|
||||
FaApple,
|
||||
FaDebian,
|
||||
FaFedora,
|
||||
FaOpensuse,
|
||||
FaUbuntu,
|
||||
FaWindows,
|
||||
FaRedhat,
|
||||
} from "react-icons/fa6";
|
||||
import { LuHouse, LuNewspaper, LuDownload } from "react-icons/lu";
|
||||
|
||||
export const routes: Record<string, [string, any]> = {
|
||||
"/": ["Home", Home],
|
||||
"/blog": ["Blog", Newspaper],
|
||||
"/downloads": ["Downloads", Download],
|
||||
export const routes: Record<string, [string, IconType|null]> = {
|
||||
"/": ["Home", LuHouse],
|
||||
"/blog": ["Blog", LuNewspaper],
|
||||
"/downloads": ["Downloads", LuDownload],
|
||||
"/about": ["About", null],
|
||||
};
|
||||
|
||||
const releasesUrl =
|
||||
"https://github.com/KRTirtho/Spotube/releases/latest/download";
|
||||
|
||||
export const downloadLinks: Record<string, [string, IconDefinition[]]> = {
|
||||
"Android Apk": [`${releasesUrl}/Spotube-android-all-arch.apk`, [faAndroid]],
|
||||
export const downloadLinks: Record<string, [string, IconType[]]> = {
|
||||
"Android Apk": [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid]],
|
||||
"Windows Executable": [
|
||||
`${releasesUrl}/Spotube-windows-x86_64-setup.exe`,
|
||||
[faWindows],
|
||||
[FaWindows],
|
||||
],
|
||||
"macOS Dmg": [`${releasesUrl}/Spotube-macos-universal.dmg`, [faApple]],
|
||||
"macOS Dmg": [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple]],
|
||||
"Ubuntu, Debian": [
|
||||
`${releasesUrl}/Spotube-linux-x86_64.deb`,
|
||||
[faUbuntu, faDebian],
|
||||
[FaUbuntu, FaDebian],
|
||||
],
|
||||
"Fedora, Redhat, Opensuse": [
|
||||
`${releasesUrl}/Spotube-linux-x86_64.rpm`,
|
||||
[faFedora, faRedhat, faOpensuse],
|
||||
[FaFedora, FaRedhat, FaOpensuse],
|
||||
],
|
||||
"iPhone Ipa": [`${releasesUrl}/Spotube-iOS.ipa`, [faApple]],
|
||||
"iPhone Ipa": [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple]],
|
||||
};
|
||||
|
||||
export const extendedDownloadLinks: Record<
|
||||
string,
|
||||
[string, IconDefinition[], string]
|
||||
[string, IconType[], string]
|
||||
> = {
|
||||
Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [faAndroid], "apk"],
|
||||
Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [FaAndroid], "apk"],
|
||||
Windows: [
|
||||
`${releasesUrl}/Spotube-windows-x86_64-setup.exe`,
|
||||
[faWindows],
|
||||
[FaWindows],
|
||||
"exe",
|
||||
],
|
||||
macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [faApple], "dmg"],
|
||||
macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"],
|
||||
"Ubuntu, Debian": [
|
||||
`${releasesUrl}/Spotube-linux-x86_64.deb`,
|
||||
[faUbuntu, faDebian],
|
||||
[FaUbuntu, FaDebian],
|
||||
"deb",
|
||||
],
|
||||
"Fedora, Redhat, Opensuse": [
|
||||
`${releasesUrl}/Spotube-linux-x86_64.rpm`,
|
||||
[faFedora, faRedhat, faOpensuse],
|
||||
[FaFedora, FaRedhat, FaOpensuse],
|
||||
"rpm",
|
||||
],
|
||||
iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [faApple], "ipa"],
|
||||
iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"],
|
||||
};
|
||||
|
||||
const nightlyReleaseUrl =
|
||||
@ -68,30 +68,30 @@ const nightlyReleaseUrl =
|
||||
|
||||
export const extendedNightlyDownloadLinks: Record<
|
||||
string,
|
||||
[string, IconDefinition[], string]
|
||||
[string, IconType[], string]
|
||||
> = {
|
||||
Android: [
|
||||
`${nightlyReleaseUrl}/Spotube-android-all-arch.apk`,
|
||||
[faAndroid],
|
||||
[FaAndroid],
|
||||
"apk",
|
||||
],
|
||||
Windows: [
|
||||
`${nightlyReleaseUrl}/Spotube-windows-x86_64-setup.exe`,
|
||||
[faWindows],
|
||||
[FaWindows],
|
||||
"exe",
|
||||
],
|
||||
macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [faApple], "dmg"],
|
||||
macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [FaApple], "dmg"],
|
||||
"Ubuntu, Debian": [
|
||||
`${nightlyReleaseUrl}/Spotube-linux-x86_64.deb`,
|
||||
[faUbuntu, faDebian],
|
||||
[FaUbuntu, FaDebian],
|
||||
"deb",
|
||||
],
|
||||
"Fedora, Redhat, Opensuse": [
|
||||
`${nightlyReleaseUrl}/Spotube-linux-x86_64.rpm`,
|
||||
[faFedora, faRedhat, faOpensuse],
|
||||
[FaFedora, FaRedhat, FaOpensuse],
|
||||
"rpm",
|
||||
],
|
||||
iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [faApple], "ipa"],
|
||||
iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [FaApple], "ipa"],
|
||||
};
|
||||
|
||||
export const ADS_SLOTS = Object.freeze({
|
51
website/src/components/navigation/TopBar.astro
Normal file
@ -0,0 +1,51 @@
|
||||
---
|
||||
import { routes } from "~/collections/app";
|
||||
import { FaGithub } from "react-icons/fa6";
|
||||
import SidebarButton from "./sidebar-button";
|
||||
|
||||
const pathname = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<header class="flex justify-between items-center p-4 bg-surface">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<SidebarButton client:only />
|
||||
<h2 class="text-3xl">
|
||||
<a href="/" class="flex gap-2 items-center">
|
||||
<img src="/images/spotube-logo.png" width="40px" alt="Spotube Logo" />
|
||||
Spotube
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
<a
|
||||
class="mw-2 md:me-4"
|
||||
href="https://github.com/KRTirtho/spotube?referrer=spotube.krtirtho.dev"
|
||||
target="_blank"
|
||||
>
|
||||
<button class="btn preset-filled flex items-center gap-2">
|
||||
<FaGithub />
|
||||
Star us
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<nav class="hidden md:flex gap-3 items-center">
|
||||
{
|
||||
Object.entries(routes).map((route) => {
|
||||
const Icon = route[1][1];
|
||||
|
||||
return (
|
||||
<a href={route[0]}>
|
||||
<button
|
||||
type="button"
|
||||
class={`btn flex gap-2 ${route[0] === "/downloads" ? "preset-tonal-secondary" : "preset-tonal-surface"} ${pathname.endsWith(route[0]) ? "underline" : ""}`}
|
||||
>
|
||||
{Icon && <Icon />}
|
||||
{route[1][0]}
|
||||
</button>
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
</nav>
|
||||
</header>
|
45
website/src/components/navigation/sidebar-button.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { LuMenu } from "react-icons/lu";
|
||||
import { useOnClickOutside } from "usehooks-ts";
|
||||
import { routes } from "~/collections/app";
|
||||
|
||||
export default function SidebarButton() {
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
useOnClickOutside(ref as React.RefObject<HTMLDivElement>, () => {
|
||||
setIsOpen(false);
|
||||
})
|
||||
|
||||
return <>
|
||||
<div className={
|
||||
`fixed h-screen w-72 bg-surface-100 dark:bg-surface-900 top-0 left-0 bg-surface z-50 transition-all duration-300 ${isOpen ? "" : "-translate-x-full"}`
|
||||
}
|
||||
ref={ref}
|
||||
>
|
||||
{
|
||||
Object.entries(routes).map((route) => {
|
||||
const Icon = route[1][1];
|
||||
return (
|
||||
<a
|
||||
key={route[0]}
|
||||
href={route[0]}
|
||||
className="flex items-center gap-2 p-4 hover:bg-surface/80 transition-colors duration-200"
|
||||
>
|
||||
{Icon && <Icon />}
|
||||
<span className="text-lg">{route[1][0]}</span>
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-icon md:hidden"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<LuMenu />
|
||||
</button>
|
||||
</>;
|
||||
}
|
28
website/src/layouts/RootLayout.astro
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
import "../styles/global.css";
|
||||
import TopBar from "~/components/navigation/TopBar.astro";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="wintry">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Spotube</title>
|
||||
</head>
|
||||
<body>
|
||||
<TopBar />
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
@ -1,32 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let adSlot: number;
|
||||
export let adFormat: 'auto' | 'fluid';
|
||||
// biome-ignore lint/style/useConst: This is just props
|
||||
export let fullWidthResponsive = true;
|
||||
// biome-ignore lint/style/useConst: This is just props
|
||||
export let style = 'display:block';
|
||||
// biome-ignore lint/style/useConst: This is just props
|
||||
export let adLayout: 'in-article' | 'in-feed' | 'in-page' | undefined = undefined;
|
||||
// biome-ignore lint/style/useConst: This is just props
|
||||
export let adLayoutKey: string | undefined = undefined;
|
||||
|
||||
const AD_CLIENT = 'ca-pub-6419300932495863';
|
||||
|
||||
onMount(() => {
|
||||
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
|
||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||||
});
|
||||
</script>
|
||||
|
||||
<ins
|
||||
class="adsbygoogle"
|
||||
{style}
|
||||
data-ad-layout={adLayout}
|
||||
data-ad-client={AD_CLIENT}
|
||||
data-ad-slot={adSlot}
|
||||
data-ad-format={adFormat}
|
||||
data-full-width-responsive={fullWidthResponsive}
|
||||
data-ad-layout-key={adLayoutKey}
|
||||
></ins>
|
@ -1,25 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { IconDefinition } from '@fortawesome/free-brands-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
|
||||
export let links: Record<string, [string, IconDefinition[], string]>;
|
||||
</script>
|
||||
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
{#each Object.entries(links) as link}
|
||||
<a
|
||||
href={link[1][0]}
|
||||
class="flex flex-col btn variant-ghost-primary rounded-xl p-0 overflow-hidden"
|
||||
>
|
||||
<div class="relative bg-primary-500 p-4 flex gap-4 justify-center rounded-t-xl w-full">
|
||||
{#each link[1][1] as icon}
|
||||
<Fa {icon} />
|
||||
{/each}
|
||||
<p class="chip variant-ghost-warning text-warning-400 absolute right-2 uppercase">
|
||||
{link[1][2]}
|
||||
</p>
|
||||
</div>
|
||||
<p class="p-4">{link[0]}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
@ -1,3 +0,0 @@
|
||||
<article class="prose lg:prose-lg dark:prose-invert max-w-5xl mx-auto">
|
||||
<slot />
|
||||
</article>
|
@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SvelteMarkdown from 'svelte-markdown';
|
||||
import Layout from '$lib/components/markdown/layout.svelte';
|
||||
import { X } from 'lucide-svelte';
|
||||
import { getModalStore } from '@skeletonlabs/skeleton';
|
||||
|
||||
const modalStore = getModalStore();
|
||||
|
||||
const mdContent = `
|
||||
## Spotube reborn 🦄
|
||||
|
||||
After facing a legal threat(s) from Spotify™, Spotube has moved from using Spotify™ API for music metadata purposes.
|
||||
Now we're aiming to make Spotube an extensible music streaming platform and will continue to use free to use and open source music APIs.
|
||||
|
||||
So users can bring their own metadata APIs (already finished) and playback APIs (WIP). And can plug things together using community plugins.
|
||||
|
||||
To reduce friction, by default, from Spotube v5, it will contain MusicBrainz and ListenBrainz plugin (including support for custom instances). But you can always
|
||||
bring your own metadata and playback APIs using plugins (there will be documentation on how to do that soon!).
|
||||
|
||||
Currently, the v5 is still under beta. So only nightly builds are downloadable. Please continue to use Nightly versions (it can contains bugs or not work at all) until stable Spotube v5 is released.
|
||||
|
||||
Btw, please support the [MetaBrainz Foundation](https://metabrainz.org/) for their amazing work on MusicBrainz and ListenBrainz.
|
||||
They're open source and non-profit, so they need your support to keep the lights on and continue their work.
|
||||
`;
|
||||
</script>
|
||||
|
||||
<div class="bg-primary-100 p-5 rounded-lg overflow-scroll max-h-[95vh] relative">
|
||||
<button
|
||||
type="button"
|
||||
class="btn variant-soft absolute top-2 right-2"
|
||||
on:click={() => modalStore.close()}
|
||||
>
|
||||
<X />
|
||||
</button>
|
||||
<Layout>
|
||||
<SvelteMarkdown source={mdContent} />
|
||||
</Layout>
|
||||
|
||||
<p class="text-surface-500 mt-5">
|
||||
Spotube has no affiliation with Spotify™ or any of its subsidiaries.
|
||||
</p>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button type="button" class="btn variant-filled-primary" on:click={() => modalStore.close()}>
|
||||
Understood
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { SlideToggle } from '@skeletonlabs/skeleton';
|
||||
import { persisted } from '$lib/persisted-store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const isDark = persisted('dark-mode', false);
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
$isDark
|
||||
? document.documentElement.classList.add('dark')
|
||||
: document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
|
||||
export let label: string | undefined;
|
||||
</script>
|
||||
|
||||
<div class="inline-flex gap-2">
|
||||
<SlideToggle
|
||||
label={label}
|
||||
active="bg-primary-backdrop-token"
|
||||
size="sm"
|
||||
name="dark-mode"
|
||||
checked={$isDark}
|
||||
on:change={() => {
|
||||
isDark.update((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
</div>
|
@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { routes } from '$lib';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import { SlideToggle, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { Menu } from 'lucide-svelte';
|
||||
import Fa from 'svelte-fa';
|
||||
import DarkmodeToggle from './darkmode-toggle.svelte';
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
</script>
|
||||
|
||||
<header class="flex justify-between">
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="btn btn-icon md:hidden"
|
||||
on:click={() => {
|
||||
drawerStore.set({ id: 'navdrawer', width: '400px', open: !$drawerStore.open });
|
||||
}}
|
||||
>
|
||||
<Menu />
|
||||
</button>
|
||||
<h2 class="text-3xl">
|
||||
<a href="/" class="flex gap-2 items-center">
|
||||
<img src="/images/spotube-logo.png" width="40px" alt="Spotube Logo" />
|
||||
Spotube
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
<a
|
||||
class="mw-2 md:me-4"
|
||||
href="https://github.com/KRTirtho/spotube?referrer=spotube.krtirtho.dev"
|
||||
target="_blank"
|
||||
>
|
||||
<button class="btn variant-filled flex items-center gap-2">
|
||||
<Fa icon={faGithub} />
|
||||
Star us
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<nav class="hidden md:flex gap-3 items-center">
|
||||
{#each Object.entries(routes) as route}
|
||||
<a href={route[0]}>
|
||||
<button
|
||||
class={`btn rounded-full flex gap-2 ${route[0] === '/downloads' ? 'variant-glass-primary' : 'variant-glass-surface'} ${$page.url.pathname.endsWith(route[0]) ? 'underline' : ''}`}
|
||||
>
|
||||
{#if route[1][1] !== null}
|
||||
<svelte:component this={route[1][1]} size={16} />
|
||||
{/if}
|
||||
{route[1][0]}
|
||||
</button>
|
||||
</a>
|
||||
{/each}
|
||||
<DarkmodeToggle />
|
||||
</nav>
|
||||
</header>
|
@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { routes } from '$lib';
|
||||
import { ListBox, ListBoxItem, getDrawerStore } from '@skeletonlabs/skeleton';
|
||||
import { X } from 'lucide-svelte';
|
||||
import DarkmodeToggle from '../navbar/darkmode-toggle.svelte';
|
||||
|
||||
let currentRoute: string = $page.url.pathname;
|
||||
const drawerStore = getDrawerStore();
|
||||
</script>
|
||||
|
||||
<nav class="px-2">
|
||||
<div class="flex justify-end">
|
||||
<button class="btn btn-icon" on:click={drawerStore.close}>
|
||||
<X />
|
||||
</button>
|
||||
</div>
|
||||
<ListBox>
|
||||
{#each Object.entries(routes) as route}
|
||||
<ListBoxItem
|
||||
bind:group={currentRoute}
|
||||
name="item"
|
||||
value={route[0]}
|
||||
on:click={() => {
|
||||
goto(route[0]);
|
||||
}}
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<svelte:component this={route[1][1]} size={16} />
|
||||
{route[1][0]}
|
||||
</div>
|
||||
</ListBoxItem>
|
||||
{/each}
|
||||
<DarkmodeToggle label="Theme" />
|
||||
</ListBox>
|
||||
</nav>
|
@ -1,106 +0,0 @@
|
||||
import { writable as internal, type Writable } from 'svelte/store';
|
||||
|
||||
declare type Updater<T> = (value: T) => T;
|
||||
declare type StoreDict<T> = { [key: string]: Writable<T> };
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
interface Stores {
|
||||
local: StoreDict<any>;
|
||||
session: StoreDict<any>;
|
||||
}
|
||||
|
||||
const stores: Stores = {
|
||||
local: {},
|
||||
session: {}
|
||||
};
|
||||
|
||||
export interface Serializer<T> {
|
||||
parse(text: string): T;
|
||||
stringify(object: T): string;
|
||||
}
|
||||
|
||||
export type StorageType = 'local' | 'session';
|
||||
|
||||
export interface Options<T> {
|
||||
serializer?: Serializer<T>;
|
||||
storage?: StorageType;
|
||||
syncTabs?: boolean;
|
||||
onError?: (e: unknown) => void;
|
||||
}
|
||||
|
||||
function getStorage(type: StorageType) {
|
||||
return type === 'local' ? localStorage : sessionStorage;
|
||||
}
|
||||
|
||||
/** @deprecated `writable()` has been renamed to `persisted()` */
|
||||
export function writable<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
|
||||
console.warn(
|
||||
"writable() has been deprecated. Please use persisted() instead.\n\nchange:\n\nimport { writable } from 'svelte-persisted-store'\n\nto:\n\nimport { persisted } from 'svelte-persisted-store'"
|
||||
);
|
||||
return persisted<T>(key, initialValue, options);
|
||||
}
|
||||
export function persisted<T>(key: string, initialValue: T, options?: Options<T>): Writable<T> {
|
||||
const serializer = options?.serializer ?? JSON;
|
||||
const storageType = options?.storage ?? 'local';
|
||||
const syncTabs = options?.syncTabs ?? true;
|
||||
const onError =
|
||||
options?.onError ??
|
||||
((e) =>
|
||||
console.error(`Error when writing value from persisted store "${key}" to ${storageType}`, e));
|
||||
const browser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
||||
const storage = browser ? getStorage(storageType) : null;
|
||||
|
||||
function updateStorage(key: string, value: T) {
|
||||
try {
|
||||
storage?.setItem(key, serializer.stringify(value));
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeLoadInitial(): T {
|
||||
const json = storage?.getItem(key);
|
||||
|
||||
if (json) {
|
||||
return <T>serializer.parse(json);
|
||||
}
|
||||
|
||||
return initialValue;
|
||||
}
|
||||
|
||||
if (!stores[storageType][key]) {
|
||||
const initial = maybeLoadInitial();
|
||||
const store = internal(initial, (set) => {
|
||||
if (browser && storageType == 'local' && syncTabs) {
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key === key) set(event.newValue ? serializer.parse(event.newValue) : null);
|
||||
};
|
||||
|
||||
window.addEventListener('storage', handleStorage);
|
||||
|
||||
return () => window.removeEventListener('storage', handleStorage);
|
||||
}
|
||||
});
|
||||
|
||||
const { subscribe, set } = store;
|
||||
|
||||
stores[storageType][key] = {
|
||||
set(value: T) {
|
||||
set(value);
|
||||
updateStorage(key, value);
|
||||
},
|
||||
update(callback: Updater<T>) {
|
||||
return store.update((last) => {
|
||||
const value = callback(last);
|
||||
|
||||
updateStorage(key, value);
|
||||
|
||||
return value;
|
||||
});
|
||||
},
|
||||
subscribe
|
||||
};
|
||||
}
|
||||
|
||||
return stores[storageType][key];
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
export interface Post {
|
||||
date: string;
|
||||
title: string;
|
||||
tags: string[];
|
||||
published: boolean;
|
||||
author: string;
|
||||
cover_img: string | null;
|
||||
readingTime: {
|
||||
text: string;
|
||||
minutes: number;
|
||||
time: number;
|
||||
words: number;
|
||||
};
|
||||
reading_time_text: string;
|
||||
preview_html: string;
|
||||
preview: string;
|
||||
previewHtml: string;
|
||||
slug: string | null;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const getPosts = async () => {
|
||||
// Fetch posts from local Markdown files
|
||||
const posts: Post[] = await Promise.all(
|
||||
Object.entries(import.meta.glob("../../posts/**/*.md")).map(
|
||||
async ([path, resolver]) => {
|
||||
const resolved = (await resolver()) as { metadata: Post };
|
||||
const { metadata } = resolved;
|
||||
const slug = path.split("/").pop()?.slice(0, -3) ?? "";
|
||||
return { ...metadata, slug };
|
||||
},
|
||||
),
|
||||
).then((posts) => posts.filter((post) => post.published));
|
||||
|
||||
let sortedPosts = posts.sort((a, b) => +new Date(b.date) - +new Date(a.date));
|
||||
|
||||
sortedPosts = sortedPosts.map((post) => ({
|
||||
...post,
|
||||
}));
|
||||
|
||||
return {
|
||||
posts: sortedPosts,
|
||||
};
|
||||
};
|
5
website/src/pages/about/index.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import RootLayout from "~/layouts/RootLayout.astro";
|
||||
---
|
||||
|
||||
<RootLayout />
|
5
website/src/pages/blog/index.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import RootLayout from "~/layouts/RootLayout.astro";
|
||||
---
|
||||
|
||||
<RootLayout />
|
5
website/src/pages/downloads/index.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import RootLayout from "~/layouts/RootLayout.astro";
|
||||
---
|
||||
|
||||
<RootLayout />
|
5
website/src/pages/index.astro
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
import RootLayout from "../layouts/RootLayout.astro";
|
||||
---
|
||||
|
||||
<RootLayout />
|
@ -1,87 +0,0 @@
|
||||
<script lang="ts">
|
||||
import '../app.postcss';
|
||||
import Navbar from '$lib/components/navbar/navbar.svelte';
|
||||
|
||||
// Highlight JS
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import { Drawer, getDrawerStore, getModalStore, Modal, storeHighlightJs, type ModalComponent } from '@skeletonlabs/skeleton';
|
||||
import xml from 'highlight.js/lib/languages/xml'; // for HTML
|
||||
import css from 'highlight.js/lib/languages/css';
|
||||
import javascript from 'highlight.js/lib/languages/javascript';
|
||||
import typescript from 'highlight.js/lib/languages/typescript';
|
||||
|
||||
hljs.registerLanguage('xml', xml); // for HTML
|
||||
hljs.registerLanguage('css', css);
|
||||
hljs.registerLanguage('javascript', javascript);
|
||||
hljs.registerLanguage('typescript', typescript);
|
||||
storeHighlightJs.set(hljs);
|
||||
|
||||
// Floating UI for Popups
|
||||
import { computePosition, autoUpdate, flip, shift, offset, arrow } from '@floating-ui/dom';
|
||||
import { storePopup } from '@skeletonlabs/skeleton';
|
||||
storePopup.set({ computePosition, autoUpdate, flip, shift, offset, arrow });
|
||||
|
||||
import { initializeStores } from '@skeletonlabs/skeleton';
|
||||
import NavDrawer from '../lib/components/navdrawer/navdrawer.svelte';
|
||||
import Fa from 'svelte-fa';
|
||||
import { faGithub } from '@fortawesome/free-brands-svg-icons';
|
||||
import Legal from '$lib/components/misc/legal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
initializeStores();
|
||||
|
||||
const drawerStore = getDrawerStore();
|
||||
const modalStore = getModalStore();
|
||||
|
||||
const modalRegistry: Record<string, ModalComponent> = {
|
||||
legal: { ref: Legal }
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Set the default modal to be open
|
||||
modalStore.trigger({
|
||||
type: "component",
|
||||
component: "legal",
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<main class="p-2 md:p-4 min-h-[90vh]">
|
||||
<Modal components={modalRegistry} />
|
||||
<Drawer>
|
||||
{#if $drawerStore.id === 'navdrawer'}
|
||||
<NavDrawer />
|
||||
{/if}
|
||||
</Drawer>
|
||||
<Navbar />
|
||||
<slot />
|
||||
<br /><br />
|
||||
</main>
|
||||
<footer class="w-full bg-tertiary-backdrop-token p-4 flex justify-between">
|
||||
<div>
|
||||
<h3 class="h3">Spotube</h3>
|
||||
<p>
|
||||
Copyright © {new Date().getFullYear()} Spotube
|
||||
</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/KRTirtho/spotube">
|
||||
<Fa class="inline mr-1" icon={faGithub} size="lg" />
|
||||
Github
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://opencollective.org/spotube">
|
||||
<img
|
||||
src="https://avatars0.githubusercontent.com/u/13403593?v=4"
|
||||
alt="OpenCollective"
|
||||
height="20"
|
||||
width="20"
|
||||
class="inline mr-1"
|
||||
/>
|
||||
OpenCollective
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
@ -1,111 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { faAndroid, faWindows, faApple, faLinux } from '@fortawesome/free-brands-svg-icons/index';
|
||||
import Fa from 'svelte-fa';
|
||||
import { Download, Heart } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { Avatar } from '@skeletonlabs/skeleton';
|
||||
import Ads from '$lib/components/ads/ads.svelte';
|
||||
import { ADS_SLOTS } from '$lib';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formatter = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
compactDisplay: 'short',
|
||||
maximumFractionDigits: 0
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Spotube</title>
|
||||
<meta name="description" content="An Open Source Music Client for every platform" />
|
||||
<meta name="keywords" content="music, client, open source, music, streaming" />
|
||||
<meta name="author" content="KRTirtho" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#1DB954" />
|
||||
</svelte:head>
|
||||
|
||||
<section class="ps-4 pt-16 md:ps-24 md:pt-24">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<h1 class="h1">Spotube</h1>
|
||||
<br />
|
||||
<h3 class="h3">
|
||||
A cross-platform Extensible open-source Music Streaming platform
|
||||
<div class="inline-flex gap-3 items-center">
|
||||
<Fa class="inline text-[#3DDC84]" icon={faAndroid} />
|
||||
<Fa class="inline text-[#00A2F0]" icon={faWindows} />
|
||||
<Fa class="inline" icon={faLinux} />
|
||||
<Fa class="inline" icon={faApple} />
|
||||
</div>
|
||||
</h3>
|
||||
<p class="text-surface-500">
|
||||
And it's <span class="text-error-500 underline decoration-dashed">not</span>
|
||||
built with Electron (web technologies)
|
||||
</p>
|
||||
<br />
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="https://news.ycombinator.com/item?id=39066136" target="_blank">
|
||||
<img src="https://hackerbadge.vercel.app/api?id=39066136" alt="HackerNews" />
|
||||
</a>
|
||||
<!-- <a href="https://flathub.org/apps/com.github.KRTirtho.Spotube" target="_blank">
|
||||
<img
|
||||
width="160"
|
||||
alt="Download on Flathub"
|
||||
src="https://flathub.org/api/badge?locale=en"
|
||||
/>
|
||||
</a> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<a href="/downloads" class="flex gap-2 btn variant-filled">
|
||||
Download Nightly
|
||||
<Download />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<Ads adSlot={ADS_SLOTS.rootPageDisplay} adFormat="auto" />
|
||||
<br />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<h2 class="h2">
|
||||
Supporters
|
||||
<Heart class="inline-block" color="red" />
|
||||
</h2>
|
||||
<p class="text-surface-500">
|
||||
We are grateful for the support of individuals and organizations who have made Spotube
|
||||
possible.
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<a href="https://opencollective.com/spotube/donate" target="_blank">
|
||||
<img
|
||||
src="https://opencollective.com/webpack/donate/button@2x.png?color=blue"
|
||||
width="300"
|
||||
alt="Open Collective"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{#each data.props.members as member}
|
||||
<a href={member.profile} target="_blank">
|
||||
<div
|
||||
class="flex flex-col items-center gap-2 overflow-ellipsis w-40 btn variant-ghost-secondary rounded-lg"
|
||||
>
|
||||
<Avatar src={member.image} initials={member.name} class="w-12 h-12" />
|
||||
<p>{member.name}</p>
|
||||
<p class="capitalize text-sm underline decoration-dotted">
|
||||
{formatter.format(member.totalAmountDonated)}
|
||||
({member.role.toLowerCase()})
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<Ads adSlot={ADS_SLOTS.rootPageDisplay} adFormat="auto" />
|
||||
</section>
|
@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
export const load = async () => {
|
||||
const res = await fetch('https://opencollective.com/spotube/members/all.json');
|
||||
const members = (await res.json()) as Member[];
|
||||
|
||||
return {
|
||||
props: {
|
||||
members: members
|
||||
.filter((m) => m.totalAmountDonated > 0)
|
||||
.sort((a, b) => b.totalAmountDonated - a.totalAmountDonated)
|
||||
}
|
||||
};
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
<section class="p-4 md:p-16">
|
||||
<h2 class="h2">About</h2>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<h4 class="h4">Author & Developer</h4>
|
||||
<br />
|
||||
<a
|
||||
href="https://github.com/KRTirtho"
|
||||
target="_blank"
|
||||
class="btn variant-ghost-tertiary max-w-44 flex flex-col items-center p-4 rounded-2xl"
|
||||
>
|
||||
<img
|
||||
alt="Author of Spotube"
|
||||
src="https://github.com/KRTirtho.png"
|
||||
class="h-auto w-40 rounded-full"
|
||||
/>
|
||||
<br />
|
||||
<h5>Kingkor Roy Tirtho</h5>
|
||||
<p>Flutter developer</p>
|
||||
</a>
|
||||
</section>
|
@ -1,9 +0,0 @@
|
||||
import { getPosts } from '$lib/posts';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const { posts } = await getPosts();
|
||||
|
||||
return json(posts);
|
||||
};
|
@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ADS_SLOTS } from '$lib';
|
||||
import Ads from '$lib/components/ads/ads.svelte';
|
||||
import type { Post } from '$lib/posts';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const formatter = Intl.DateTimeFormat('en-US', {
|
||||
dateStyle: 'medium'
|
||||
});
|
||||
|
||||
// insert a special Post as ad type in the posts array
|
||||
const adAddedPosts: Post[] = [];
|
||||
|
||||
for (const post of data.posts) {
|
||||
adAddedPosts.push(post);
|
||||
const index = adAddedPosts.indexOf(post);
|
||||
|
||||
if (index % 3 === 0) {
|
||||
adAddedPosts.push({
|
||||
title: 'Ad',
|
||||
author: 'Ad',
|
||||
cover_img: 'ad.jpg',
|
||||
date: new Date().toISOString(),
|
||||
path: '/ad',
|
||||
preview: 'Ad',
|
||||
preview_html: 'Ad',
|
||||
previewHtml: 'Ad',
|
||||
published: true,
|
||||
reading_time_text: 'Ad',
|
||||
readingTime: { minutes: 1, words: 1, text: 'Ad', time: 1 },
|
||||
slug: 'ad',
|
||||
tags: ['Ad']
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="p-4 md:p-16 flex flex-col gap-4">
|
||||
<h2 class="h2">Blog Posts</h2>
|
||||
<br />
|
||||
<article class="grid sm:grid-cols-2 md:grid-cols-3 2xl:grid-cols-4">
|
||||
{#each adAddedPosts as post}
|
||||
{#if post.slug === 'ad'}
|
||||
<p></p>
|
||||
<Ads
|
||||
adSlot={ADS_SLOTS.blogPageInFeed}
|
||||
adFormat="fluid"
|
||||
adLayoutKey="-6l+eh+17-40+59"
|
||||
fullWidthResponsive={false}
|
||||
/>
|
||||
{:else}
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
class="card hover:brightness-95 active:bg-secondary-hover-token active:scale-95 transition-all variant-ghost-secondary p-4"
|
||||
>
|
||||
<img
|
||||
src={`/posts/${post.cover_img}`}
|
||||
alt={post.title}
|
||||
class="rounded h-56 w-full object-cover"
|
||||
/>
|
||||
<h4 class="h4">{post.title}</h4>
|
||||
<p>By {post.author}</p>
|
||||
<br />
|
||||
<p class="text-end">
|
||||
Published on
|
||||
<span class="font-medium underline decoration-dotted">
|
||||
{formatter.format(new Date(post.date))}
|
||||
</span>
|
||||
</p>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</article>
|
||||
</section>
|
@ -1,10 +0,0 @@
|
||||
import type { Post } from "$lib/posts.js";
|
||||
|
||||
export const load = async ({ fetch }) => {
|
||||
const res = await fetch("api/posts");
|
||||
if (res.ok) {
|
||||
const posts: Post[] = await res.json();
|
||||
return { posts };
|
||||
}
|
||||
return { posts: [] };
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Layout from '$lib/components/markdown/layout.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const {
|
||||
Content,
|
||||
meta: { date, title, readingTime, cover_img, author }
|
||||
} = data as Required<PageData>;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Blog | {title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<article class="p-4 md:p-16">
|
||||
<section
|
||||
class={cover_img
|
||||
? 'bg-black/30 h-56 md:h-80 xl:h-96 bg-cover bg-center flex flex-col justify-end p-4 pb-0 md:p-8 md:pb-0 rounded-lg'
|
||||
: null}
|
||||
style={cover_img ? `background-image: url(/posts/${cover_img});` : ''}
|
||||
>
|
||||
<h1 class={`h1 text-stroke ${cover_img ? 'text-white' : ''}`}>{title}</h1>
|
||||
<h4 class={`h4 text-stroke text-gray-400`}>By {author}</h4>
|
||||
<br />
|
||||
<p class={cover_img ? 'text-gray-400' : ''}>{new Date(date).toDateString()}</p>
|
||||
<p class={`mb-16 ${cover_img ? 'text-gray-400' : ''}`}>{readingTime?.text ?? ''}</p>
|
||||
</section>
|
||||
<br />
|
||||
<Layout>
|
||||
<svelte:component this={Content} />
|
||||
</Layout>
|
||||
</article>
|
@ -1,23 +0,0 @@
|
||||
import type { Post } from '$lib/posts.js';
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const { slug } = params;
|
||||
|
||||
try {
|
||||
const post = await import(`../../../../posts/${slug}.md`);
|
||||
return {
|
||||
Content: post.default as ConstructorOfATypedSvelteComponent,
|
||||
meta: {
|
||||
...post.metadata,
|
||||
slug,
|
||||
path: `/blog/${slug}`
|
||||
} as Post
|
||||
};
|
||||
} catch (err) {
|
||||
console.error('Error loading the post:', err);
|
||||
return {
|
||||
status: 500,
|
||||
error: `Could not load the post: ${(err as Error).message || err}`
|
||||
};
|
||||
}
|
||||
};
|
@ -1,63 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { ADS_SLOTS, extendedDownloadLinks } from '$lib';
|
||||
import { Download } from 'lucide-svelte';
|
||||
import { History, Sparkles, Package } from 'lucide-svelte';
|
||||
// import DownloadItems from '$lib/components/downloads/download-items.svelte';
|
||||
import Ads from '$lib/components/ads/ads.svelte';
|
||||
|
||||
const otherDownloads: [string, string, any][] = [
|
||||
// ['/downloads/packages', 'CLI Packages Managers', Package],
|
||||
// ['/downloads/older', 'Older Versions', History],
|
||||
['/downloads/nightly', 'Nightly Builds', Sparkles]
|
||||
];
|
||||
</script>
|
||||
|
||||
<section class="p-4 md:p-16 md:pb-4">
|
||||
<h2 class="h2 flex items-center gap-4">
|
||||
Download
|
||||
<Download class="inline" size={30} />
|
||||
</h2>
|
||||
<br /><br />
|
||||
<h5 class="h5">Spotube is available for every platform</h5>
|
||||
<br />
|
||||
|
||||
<!-- <DownloadItems links={extendedDownloadLinks} /> -->
|
||||
|
||||
<h3 class="h3 text-red-500">
|
||||
Versions of Spotube (<=v4.0.2) are ceased to work with Spotify™ API.
|
||||
<br />
|
||||
So users can no longer use/download those versions.
|
||||
<br />
|
||||
Please wait for the next version that will remedy this issue by not using such APIs.
|
||||
</h3>
|
||||
<p class="text-surface-500 mt-5">
|
||||
Spotube has no affiliation with Spotify™ or any of its subsidiaries.
|
||||
</p>
|
||||
<br />
|
||||
<br />
|
||||
<h6 class="h6 mb-5">
|
||||
The new Spotube v5 is still under beta. Please use the Nightly version until stable release.
|
||||
</h6>
|
||||
<a href="/downloads/nightly">
|
||||
<button type="button" class="btn variant-filled"> Download Nightly </button>
|
||||
</a>
|
||||
|
||||
<br />
|
||||
<Ads adSlot={ADS_SLOTS.downloadPageDisplay} adFormat="auto" />
|
||||
<br />
|
||||
|
||||
<h2 class="h2">Other Downloads</h2>
|
||||
<br /><br />
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2 max-w-3xl">
|
||||
{#each otherDownloads as download}
|
||||
<a href={download[0]}>
|
||||
<div class="btn rounded variant-soft-secondary flex flex-col items-center p-4 gap-4">
|
||||
<svelte:component this={download[2]} />
|
||||
<h5 class="h5">{download[1]}</h5>
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<br />
|
||||
<Ads adSlot={ADS_SLOTS.downloadPageDisplay} adFormat="auto" />
|
||||
</section>
|
@ -1,39 +0,0 @@
|
||||
<script>
|
||||
import { AlertTriangle, Bug, Sparkles } from 'lucide-svelte';
|
||||
import DownloadItems from '$lib/components/downloads/download-items.svelte';
|
||||
import { ADS_SLOTS, extendedNightlyDownloadLinks } from '$lib';
|
||||
import Ads from '$lib/components/ads/ads.svelte';
|
||||
</script>
|
||||
|
||||
<section class="p-4 md:p-16">
|
||||
<h2 class="h2 flex items-center gap-4">
|
||||
Nightly Downloads
|
||||
<Sparkles class="inline" size={30} />
|
||||
</h2>
|
||||
<br /><br />
|
||||
<aside class="alert variant-ghost-warning">
|
||||
<div><AlertTriangle class="text-warning-500" /></div>
|
||||
<div class="alert-message">
|
||||
<h3 class="h3">Nightly versions may contain bugs <Bug class="inline" /></h3>
|
||||
<p>
|
||||
Although Nightly versions are packed with newest and greatest features, it's often unstable
|
||||
and not tested by the maintainers and publisher(s).
|
||||
<br />
|
||||
<span class="text-error-500 underline decoration-dotted">
|
||||
So use it at your own risk.
|
||||
</span>
|
||||
<span>
|
||||
Go to <a href="/downloads" class="anchor">Downloads</a> for more stable releases.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
<br />
|
||||
|
||||
<p class="mb-4">Following are the new v5 Nightly versions:</p>
|
||||
<DownloadItems links={extendedNightlyDownloadLinks} />
|
||||
|
||||
<br />
|
||||
<Ads adSlot={ADS_SLOTS.downloadPageDisplay} adFormat="auto" />
|
||||
<br />
|
||||
</section>
|
@ -1,149 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SvelteMarkdown from 'svelte-markdown';
|
||||
import type { PageData } from './$types';
|
||||
import { formatDistanceToNow, formatRelative } from 'date-fns';
|
||||
import Layout from '$lib/components/markdown/layout.svelte';
|
||||
import { Accordion, AccordionItem } from '@skeletonlabs/skeleton';
|
||||
import { Book, History } from 'lucide-svelte';
|
||||
import {
|
||||
faAndroid,
|
||||
faApple,
|
||||
faGit,
|
||||
faGooglePlay,
|
||||
faLinux,
|
||||
faWindows,
|
||||
type IconDefinition
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import type { RestEndpointMethodTypes } from '@octokit/rest';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
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')) return [`Linux`, extension];
|
||||
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<string, [IconDefinition, string]> = {
|
||||
android: [faAndroid, '#3DDC84'],
|
||||
mac: [faApple, ''],
|
||||
ios: [faApple, ''],
|
||||
linux: [faLinux, '#000000'],
|
||||
windows: [faWindows, '#0078D7']
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="p-4 md:p-24">
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="h2">Older versions</h2>
|
||||
<History />
|
||||
</div>
|
||||
<br /><br />
|
||||
<h3 class="h3 text-red-500">
|
||||
Versions of Spotube (<=v4.0.2) are ceased to work with Spotify™ API.
|
||||
<br />
|
||||
So users can no longer use/download those versions.
|
||||
<br />
|
||||
Please wait for the next version that will remedy this issue by not using such APIs.
|
||||
</h3>
|
||||
|
||||
<p class="text-surface-500 mt-20">
|
||||
Spotube has no affiliation with Spotify™ or any of its subsidiaries.
|
||||
</p>
|
||||
<Accordion>
|
||||
<div class="flex flex-col gap-5">
|
||||
{#each data.releases as release}
|
||||
<h4 class="h4" title={formatRelative(release.published_at ?? new Date(), new Date())}>
|
||||
{release.tag_name}
|
||||
<span class="text-sm font-normal">
|
||||
({formatDistanceToNow(release.published_at ?? new Date(), { addSuffix: true })})
|
||||
</span>
|
||||
</h4>
|
||||
<div class="flex flex-col gap-5">
|
||||
{#each Object.entries(groupByOS(release.assets)) as [osName, assets]}
|
||||
<div class="flex flex-col gap-4">
|
||||
<h5 class="h5 capitalize">
|
||||
<Fa class="inline" icon={icons[osName][0]} color={icons[osName][1]} />
|
||||
{osName}
|
||||
</h5>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
{#each assets as asset}
|
||||
<a href={asset.browser_download_url}>
|
||||
<button class="btn variant-glass-primary rounded p-0 flex flex-col gap-2">
|
||||
<span class="bg-primary-500 rounded-t p-3 w-full">
|
||||
<Fa class="inline" icon={getIcon(asset.browser_download_url)} />
|
||||
</span>
|
||||
<span class="p-4">
|
||||
{formatName(asset.name)[0]}
|
||||
<span class="chip variant-ghost-error">
|
||||
{formatName(asset.name)[1]}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<AccordionItem>
|
||||
<svelte:fragment slot="lead">
|
||||
<Book />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="summary">Release Notes & Changelogs</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<Layout>
|
||||
<SvelteMarkdown source={release.body} />
|
||||
</Layout>
|
||||
</svelte:fragment>
|
||||
</AccordionItem>
|
||||
<hr />
|
||||
{/each}
|
||||
</div>
|
||||
</Accordion>
|
||||
</div>
|
@ -1,14 +0,0 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
// import { Octokit } from "@octokit/rest";
|
||||
|
||||
// const github = new Octokit();
|
||||
export const load: PageLoad = async () => {
|
||||
// const { data: releases } = await github.repos.listReleases({
|
||||
// owner: "KRTirtho",
|
||||
// repo: "spotube",
|
||||
// });
|
||||
|
||||
return {
|
||||
releases: [],
|
||||
};
|
||||
};
|
@ -1,112 +0,0 @@
|
||||
---
|
||||
title: CLI Packages Managers
|
||||
author: Kingkor Roy Tirtho
|
||||
---
|
||||
|
||||
<script lang="ts">
|
||||
import { faLinux, faWindows, faApple } from '@fortawesome/free-brands-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
// import Ads from '$lib/components/ads/ads.svelte';
|
||||
// import { ADS_SLOTS } from '$lib';
|
||||
</script>
|
||||
|
||||
<div class="p-4 md:ps-24">
|
||||
<h2 class="h2">Package Managers</h2>
|
||||
|
||||
Spotube is available in various Package Managers supported by Platform
|
||||
|
||||
<h3 class="h3 text-red-500">
|
||||
Versions of Spotube (<=v4.0.2) are ceased to work with Spotify™ API.
|
||||
<br />
|
||||
So users can no longer use/download those versions.
|
||||
<br />
|
||||
Please wait for the next version that will remedy this issue by not using such APIs.
|
||||
</h3>
|
||||
|
||||
<p class="text-surface-500 mt-20">
|
||||
Spotube has no affiliation with Spotify™ or any of its subsidiaries.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
## <Fa class="inline" icon={faLinux} /> 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
|
||||
```
|
||||
|
||||
<Ads
|
||||
style="display:block; text-align:center;"
|
||||
adSlot={ADS_SLOTS.packagePageArticle}
|
||||
adLayout="in-article"
|
||||
adFormat="fluid"
|
||||
fullWidthResponsive={false}
|
||||
/>
|
||||
## <Fa class="inline" icon={faApple} /> 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
|
||||
```
|
||||
|
||||
<Ads
|
||||
style="display:block; text-align:center;"
|
||||
adSlot={ADS_SLOTS.packagePageArticle}
|
||||
adLayout="in-article"
|
||||
adFormat="fluid"
|
||||
fullWidthResponsive={false}
|
||||
/>
|
||||
|
||||
## <Fa class="inline" icon={faWindows} color="#00A2F0" /> 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
|
||||
```
|
||||
|
||||
-->
|
||||
</div>
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
|
||||
export function load(){
|
||||
redirect(301, "/downloads");
|
||||
}
|
7
website/src/styles/global.css
Normal file
@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@source '../../node_modules/@skeletonlabs/skeleton-react/dist';
|
||||
|
||||
@import "@skeletonlabs/skeleton";
|
||||
@import "@skeletonlabs/skeleton/optional/presets";
|
||||
@import "@skeletonlabs/skeleton/themes/wintry";
|
Before Width: | Height: | Size: 168 KiB |
Before Width: | Height: | Size: 204 KiB |
@ -1,77 +0,0 @@
|
||||
import adapter from '@sveltejs/adapter-cloudflare';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
import { mdsvex } from 'mdsvex';
|
||||
import readingTime from 'remark-reading-time';
|
||||
import remarkExternalLinks from 'remark-external-links';
|
||||
import slugPlugin from 'rehype-slug';
|
||||
import autolinkHeadings from 'rehype-autolink-headings';
|
||||
import relativeImages from 'mdsvex-relative-images';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeAutoAds from 'rehype-auto-ads';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
extensions: ['.svelte', '.svx', '.md'],
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
mdsvex({
|
||||
extensions: ['.svx', '.md'],
|
||||
highlight: {},
|
||||
layout: './src/lib/components/markdown/layout.svelte',
|
||||
smartypants: {
|
||||
dashes: 'oldschool'
|
||||
},
|
||||
|
||||
remarkPlugins: [
|
||||
remarkGfm,
|
||||
// adds a `readingTime` frontmatter attribute
|
||||
readingTime(),
|
||||
relativeImages,
|
||||
// external links open in a new tab
|
||||
[remarkExternalLinks, { target: '_blank', rel: 'noopener' }]
|
||||
],
|
||||
rehypePlugins: [
|
||||
slugPlugin,
|
||||
[
|
||||
autolinkHeadings,
|
||||
{
|
||||
behavior: 'wrap'
|
||||
}
|
||||
],
|
||||
[
|
||||
rehypeAutoAds,
|
||||
{
|
||||
adCode: `
|
||||
<br/>
|
||||
<ins class="adsbygoogle"
|
||||
style="display:block; text-align:center;"
|
||||
data-ad-layout="in-article"
|
||||
data-ad-format="fluid"
|
||||
data-ad-client="ca-pub-6419300932495863"
|
||||
data-ad-slot="6788673194"
|
||||
></ins>
|
||||
<br/>
|
||||
<script>
|
||||
(adsbygoogle = window.adsbygoogle || []).push({});
|
||||
</script>
|
||||
`,
|
||||
paragraphInterval: 2,
|
||||
maxAds: 5,
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
],
|
||||
vitePlugin: {
|
||||
inspector: true
|
||||
},
|
||||
kit: {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
export default config;
|
@ -1,28 +0,0 @@
|
||||
import { join } from 'path';
|
||||
import type { Config } from 'tailwindcss';
|
||||
import typography from '@tailwindcss/typography';
|
||||
import { skeleton } from '@skeletonlabs/tw-plugin';
|
||||
|
||||
export default {
|
||||
darkMode: 'class',
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
|
||||
],
|
||||
theme: {
|
||||
extend: {}
|
||||
},
|
||||
plugins: [
|
||||
typography,
|
||||
skeleton({
|
||||
themes: {
|
||||
preset: [
|
||||
{
|
||||
name: 'wintry',
|
||||
enhancements: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
} satisfies Config;
|
@ -1,6 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible();
|
||||
});
|
@ -1,18 +1,20 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": [
|
||||
".astro/types.d.ts",
|
||||
"**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"dist"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "react",
|
||||
"paths": {
|
||||
"~/*": [
|
||||
"*"
|
||||
],
|
||||
},
|
||||
"baseUrl": "./src",
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { purgeCss } from "vite-plugin-tailwind-purgecss";
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
purgeCss({
|
||||
safelist: {
|
||||
// any selectors that begin with "hljs-" will not be purged
|
||||
greedy: [/^hljs-/],
|
||||
},
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
fs: {
|
||||
// Allow serving files from one level up to the project root
|
||||
// posts, copy
|
||||
allow: [".."],
|
||||
},
|
||||
port: 3000,
|
||||
},
|
||||
});
|