website: new sveltekit based website (#1239)

* feat: initialize website project

* feat: add initial homepage with download links

* feat: initial download page

* fix: linux icon color

* feat: add mobile nav and github star button

* feat: add older and nightly downloads page

* feat: add supporters and footer

* feat: add author details in about page

* feat: add darkmode toggle for website

* feat: add playstore and flathub download buttons and contribution button

* feat: add blogs support

* feat: remove netlify deploy config and add cloudflare config and favicons + manifest

* chore: add robots.txt

* feat: add spotube logo in navbar and fix build errors

* chore: add gap
This commit is contained in:
Kingkor Roy Tirtho 2024-02-17 13:48:27 +06:00 committed by GitHub
parent 8ed65bfa17
commit cd669e22c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
96 changed files with 7913 additions and 6885 deletions

13
website/.eslintignore Normal file
View File

@ -0,0 +1,13 @@
.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

31
website/.eslintrc.cjs Normal file
View File

@ -0,0 +1,31 @@
/** @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'
}
}
]
};

View File

@ -1,6 +0,0 @@
{
"extends": [
"next/core-web-vitals",
"prettier"
]
}

45
website/.gitignore vendored Executable file → Normal file
View File

@ -1,38 +1,11 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store .DS_Store
*.pem node_modules
/build
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
/.svelte-kit /.svelte-kit
/.netlify /package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.netlify

1
website/.node-version Normal file
View File

@ -0,0 +1 @@
20.11.0

1
website/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

4
website/.prettierignore Normal file
View File

@ -0,0 +1,4 @@
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,4 +1,8 @@
{ {
"singleQuote": false, "useTabs": true,
"useTabs": false "singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }

120
website/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,120 @@
{
"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"
]
}

50
website/README.md Executable file → Normal file
View File

@ -1,34 +1,38 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). # create-svelte
## Getting Started Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
First, run the development server: ## 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
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash ```bash
npm run dev npm run dev
# or
yarn dev # or start the server and open the app in a new browser tab
npm run dev -- --open
``` ```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. ## Building
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. To create a production version of your app:
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. ```bash
npm run build
```
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. You can preview the production build with `npm run preview`.
## Learn More > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@ -1,133 +0,0 @@
import {
Button,
CloseButton,
Heading,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Stack,
Text,
Tooltip,
useDisclosure,
VStack,
} from "@chakra-ui/react";
import { FC, ReactNode, useEffect, useState } from "react";
const AdDetector: FC<{ children: ReactNode }> = ({ children }) => {
const [adBlockEnabled, setAdBlockEnabled] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();
const [joke, setJoke] = useState<Record<string, any>>({});
useEffect(() => {
(async () => {
const googleAdUrl =
"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js";
try {
await fetch(new Request(googleAdUrl));
} catch (e) {
setAdBlockEnabled(true);
setJoke(
await (
await fetch(
"https://v2.jokeapi.dev/joke/Any?blacklistFlags=racist,sexist"
)
).json()
);
onOpen();
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent mt="5" mx="3">
<ModalHeader>Support the Creator💚</ModalHeader>
<ModalBody>
<p>
Open source developers work really hard to provide the best,
secure & efficient software experience for you & people all around
the world. Most of the time we work without any wages at all but
we need minimum support to live & these <b> Ads Helps Us</b> earn
the minimum wage that we need to live.{" "}
<Text color="green.500" fontWeight="bold" textAlign="justify">
So, please support Spotube by disabling the AdBlocker on this
page or by sponsoring or donating to our collectives directly
</Text>
</p>
</ModalBody>
<ModalFooter>
<Button onClick={() => window.location.reload()}>
Reload without AdBlocker
</Button>
</ModalFooter>
</ModalContent>
</Modal>
{!adBlockEnabled ? (
children
) : (
<Stack
direction="column"
w="100vw"
h="100vh"
justifyContent="space-between"
alignItems="center"
p="5"
pos="relative"
>
<Tooltip label="You made me sad 😢">
<CloseButton
pos="absolute"
right="5"
variant="ghost"
onClick={() => setAdBlockEnabled(false)}
/>
</Tooltip>
<VStack spacing="2" alignItems="flex-start">
<Heading size="sm">Here&apos;s something interesting:</Heading>
<Heading size="md">
{joke.joke ?? (
<>
<p>{joke.setup}</p>
<p>{joke.delivery}</p>
</>
)}
</Heading>
</VStack>
<VStack justifySelf="flex-end">
<Heading
mt="10"
size={{
base: "lg",
lg: "xl",
}}
maxW="700px"
textAlign="justify"
lineHeight="1.5"
>
Be grateful for all the favors you get. But don&apos;t let it
become a pile of debt. Try returning them as soon as you can.
You&apos;ll feel relieved
</Heading>
<Heading
size={{
lg: "lg",
base: "md",
}}
alignSelf="flex-end"
>
- Kingkor Roy Tirtho
</Heading>
</VStack>
</Stack>
)}
</>
);
};
export default AdDetector;

View File

@ -1,138 +0,0 @@
import { Link, Flex, Box, chakra, Image, Button, Text } from "@chakra-ui/react";
import { BlogPost } from "pages/blog";
import { FC } from "react";
import NavLink from "next/link";
const ArticleCard: FC<BlogPost> = ({
metadata: {
author,
author_avatar_url,
cover_image,
tags,
title,
summary,
date,
},
slug,
}) => {
return (
<Box
mx="auto"
rounded="lg"
shadow="md"
bg="white"
_dark={{
bg: "#212121",
}}
maxW="2xl"
>
<Image
roundedTop="lg"
w="full"
h={64}
fit="cover"
src={cover_image}
alt="Article"
/>
<Box p={6}>
<Box>
{tags.map((tag, i) => {
return (
<chakra.span
key={i}
px={3}
py={1}
mx="1"
bg="gray.600"
color="gray.100"
fontSize="sm"
fontWeight="700"
rounded="md"
>
{tag}
</chakra.span>
);
})}
<NavLink href={`/blog/${slug}`} passHref>
<Link
display="block"
color="gray.800"
_dark={{
color: "white",
}}
fontWeight="bold"
fontSize="2xl"
mt={2}
_hover={{
color: "gray.600",
textDecor: "underline",
}}
>
{title}
</Link>
</NavLink>
<chakra.p
mt={2}
fontSize="sm"
color="gray.600"
_dark={{
color: "gray.400",
}}
>
{summary}
</chakra.p>
</Box>
<Box mt={4}>
<Flex
alignItems="center"
justify="space-between"
flexDirection={{ base: "column", md: "row" }}
>
<Flex alignItems="center">
<Image
h={10}
fit="cover"
rounded="full"
src={author_avatar_url}
alt="Avatar"
/>
<Text
mx={2}
fontWeight="bold"
color="gray.700"
_dark={{
color: "gray.200",
}}
>
{author}
</Text>
<chakra.span
mx={1}
fontSize="sm"
color="gray.600"
_dark={{
color: "gray.300",
}}
>
{date}
</chakra.span>
</Flex>
<NavLink href={`/blog/${slug}`} passHref>
<Button
_hover={{ textDecor: "none" }}
colorScheme="purple"
as={Link}
>
Read the Full Article
</Button>
</NavLink>
</Flex>
</Box>
</Box>
</Box>
);
};
export default ArticleCard;

View File

@ -1,25 +0,0 @@
import { chakra } from "@chakra-ui/react";
import { FC, ReactNode } from "react";
type Props = {
children: ReactNode;
};
export const CodeBlock: FC<Props> = ({ children }) => {
return (
<chakra.pre
bgColor="gray.200"
p="3"
borderRadius="md"
_dark={{
bgColor: "gray.700",
}}
w="100%"
whiteSpace="pre-wrap"
overflowX="auto"
wordBreak="break-word"
>
<chakra.code>{children}</chakra.code>
</chakra.pre>
);
};

View File

@ -1,118 +0,0 @@
import {
Menu,
ButtonGroup,
Button,
MenuButton,
IconButton,
MenuList,
MenuItem,
Link as Anchor,
} from "@chakra-ui/react";
import { Platform, usePlatform } from "hooks/usePlatform";
import React from "react";
import {
FaApple,
FaCaretDown,
FaUbuntu,
FaLinux,
FaWindows,
FaAndroid,
} from "react-icons/fa";
import { MdOutlineFileDownload } from "react-icons/md";
const baseURL = "https://github.com/KRTirtho/spotube/releases/latest/download/";
const DownloadLinks = Object.freeze({
[Platform.linux]: [
{
name: "deb",
url: baseURL + "Spotube-linux-x86_64.deb",
icon: <FaUbuntu />,
},
{
name: "tar",
url: baseURL + "Spotube-linux-x86_64.tar.xz",
icon: <FaLinux />,
},
{
name: "AppImage",
url: baseURL + "Spotube-linux-x86_64.AppImage",
icon: <FaLinux />,
},
],
[Platform.android]: [
{
name: "apk",
url: baseURL + "Spotube-android-all-arch.apk",
icon: <FaAndroid />,
},
],
[Platform.mac]: [
{
name: "dmg",
url: baseURL + "Spotube-macos-universal.dmg",
icon: <FaApple />,
},
],
[Platform.windows]: [
{
name: "exe",
url: baseURL + "Spotube-windows-x86_64-setup.exe",
icon: <FaWindows />,
},
{
name: "nupkg",
url: baseURL + "Spotube-windows-x86_64.nupkg ",
icon: <FaWindows />,
},
],
});
const DownloadButton = () => {
const platform = usePlatform();
const allPlatforms = Object.entries(Platform)
.map(([, value]) => {
return DownloadLinks[value].map((s) => ({
...s,
name: `${value} (.${s.name})`,
}));
})
.flat(1);
const currentPlatform = DownloadLinks[platform][0];
return (
<Menu placement="bottom-end">
<ButtonGroup spacing="0.5">
<Button
variant="solid"
as={Anchor}
href={currentPlatform.url}
_hover={{ textDecoration: "none" }}
leftIcon={
<MdOutlineFileDownload fontSize="24"/>
}
>
Download for {platform} (.{currentPlatform.name})
</Button>
<MenuButton
aria-label="Show More Downloads"
as={IconButton}
variant="solid"
icon={<FaCaretDown />}
/>
</ButtonGroup>
<MenuList>
{allPlatforms.map(({ name, url, icon }) => {
return (
<MenuItem key={url} as={Anchor} href={url} icon={icon}>
{name}
</MenuItem>
);
})}
</MenuList>
</Menu>
);
};
export default DownloadButton;

View File

@ -1,88 +0,0 @@
import { Flex, chakra, Link, IconButton } from "@chakra-ui/react";
import { FaGithub, FaRedditAlien } from "react-icons/fa";
import { FiTwitter } from "react-icons/fi";
const Footer = () => {
return (
<Flex
w="full"
as="footer"
flexDir={{
base: "column",
sm: "row",
}}
align="center"
justify="space-between"
px="6"
py="4"
bg="white"
_dark={{
bg: "#282828",
}}
>
<chakra.a
href="#"
fontSize="xl"
fontWeight="bold"
color="gray.600"
_dark={{
color: "white",
_hover: {
color: "gray.300",
},
}}
_hover={{
color: "gray.700",
}}
>
Spotube
</chakra.a>
<chakra.p
py={{
base: "2",
sm: "0",
}}
color="gray.800"
_dark={{
color: "white",
}}
>
© {new Date().getFullYear()}, Spotube. All rights reserved
</chakra.p>
<Flex mx="-2">
<IconButton
colorScheme="gray"
as={Link}
aria-label="Github Link"
href="https://github.com/KRTirtho/spotube"
target="_blank"
icon={<FaGithub />}
variant="link"
/>
<IconButton
colorScheme="gray"
as={Link}
aria-label="Twitter Link"
href="https://twitter.com/@KrTirtho"
target="_blank"
icon={<FiTwitter />}
variant="link"
/>
<IconButton
colorScheme="gray"
as={Link}
aria-label="Reddit Link"
href="https://reddit.com/r/FlutterDev/search/?q=spotube&restrict_sr=1&sr_nsfw="
target="_blank"
icon={<FaRedditAlien />}
variant="link"
/>
</Flex>
</Flex>
);
};
export default Footer;

View File

@ -1,218 +0,0 @@
import {
Box,
Button,
chakra,
CloseButton,
Flex,
Heading,
HStack,
IconButton,
Link,
useColorMode,
useColorModeValue,
useDisclosure,
VisuallyHidden,
VStack,
} from "@chakra-ui/react";
import NavLink from "next/link";
import { GoLightBulb } from "react-icons/go";
import { FiGithub, FiSun } from "react-icons/fi";
import Image from "next/image";
import React from "react";
import { AiOutlineMenu } from "react-icons/ai";
import { BsHeartFill } from "react-icons/bs";
const Navbar = () => {
const bg = useColorModeValue("white", "gray.800");
const mobileNav = useDisclosure();
const { colorMode, toggleColorMode } = useColorMode();
return (
<React.Fragment>
<chakra.header
bg={bg}
w="full"
px={{
base: 1,
sm: 3,
}}
py={2}
shadow="md"
_dark={{
bgColor: "#212121",
}}
>
<Flex alignItems="center" justifyContent="space-between">
<Flex align="center">
<NavLink href="/">
<Image
src="/spotube-logo.svg"
alt="Logo"
height="60"
width="60"
layout="fixed"
/>
</NavLink>
<VisuallyHidden>Spotube</VisuallyHidden>
<NavLink href="/" passHref>
<Heading p="2" as="a" size="lg" mr="2">
Spotube
</Heading>
</NavLink>
</Flex>
<HStack display="flex" alignItems="center" spacing={1}>
<HStack
spacing={1}
mr={1}
color="brand.500"
display={{
base: "none",
md: "inline-flex",
}}
>
<NavLink href="/other-downloads" passHref>
<Button as="a" colorScheme="gray" variant="ghost">
Downloads
</Button>
</NavLink>
<NavLink href="/blog" passHref>
<Button as="a" variant="ghost" colorScheme="gray">
Blog
</Button>
</NavLink>
<NavLink href="/about" passHref>
<Button as="a" variant="ghost" colorScheme="gray">
About
</Button>
</NavLink>
<Button
as={Link}
href="https://github.com/KRTirtho/spotube"
bgColor="black"
color="white"
target="_blank"
_hover={{
textDecor: "none",
bgColor: "blackAlpha.800",
}}
_active={{
bgColor: "blackAlpha.700",
}}
rightIcon={<FiGithub />}
>
Give us a on
</Button>
</HStack>
<Button
size={{
base: "sm",
md: "sm",
lg: "md",
}}
as={Link}
href="https://opencollective.com/spotube"
bgColor="pink.100"
color="pink.500"
_hover={{
bgColor: "pink.200",
textDecor: "none",
}}
_active={{
bgColor: "pink.100",
}}
rightIcon={<BsHeartFill />}
target="_blank"
>
Donate us
</Button>
<IconButton
variant="ghost"
icon={colorMode == "light" ? <GoLightBulb /> : <FiSun />}
aria-label="Dark Mode Toggle"
onClick={() => {
toggleColorMode();
}}
/>
<Box
display={{
base: "inline-flex",
md: "none",
}}
>
<IconButton
display={{
base: "flex",
md: "none",
}}
aria-label="Open menu"
fontSize="20px"
color="gray.800"
_dark={{
color: "inherit",
}}
variant="ghost"
icon={<AiOutlineMenu />}
onClick={mobileNav.onOpen}
/>
<VStack
pos="absolute"
top={0}
left={0}
right={0}
display={mobileNav.isOpen ? "flex" : "none"}
flexDirection="column"
p={2}
pb={4}
m={2}
bg={bg}
spacing={3}
rounded="sm"
shadow="sm"
>
<CloseButton
aria-label="Close menu"
onClick={mobileNav.onClose}
/>
<NavLink href="/other-downloads" passHref>
<Button w="full" as="a" colorScheme="gray" variant="ghost">
Downloads
</Button>
</NavLink>
<NavLink href="/blog" passHref>
<Button w="full" as="a" variant="ghost" colorScheme="gray">
Blog
</Button>
</NavLink>
<NavLink href="/about" passHref>
<Button w="full" as="a" variant="ghost" colorScheme="gray">
About
</Button>
</NavLink>
<Button
as={Link}
href="https://github.com/KRTirtho/spotube"
bgColor="black"
color="white"
target="_blank"
w="full"
_hover={{
textDecor: "none",
bgColor: "blackAlpha.800",
}}
_active={{
bgColor: "blackAlpha.700",
}}
rightIcon={<FiGithub />}
>
Give us a on
</Button>
</VStack>
</Box>
</HStack>
</Flex>
</chakra.header>
</React.Fragment>
);
};
export default Navbar;

View File

@ -1,153 +0,0 @@
import {
Flex,
Box,
Icon,
chakra,
Image,
HStack,
IconButton,
Link,
CircularProgress,
} from "@chakra-ui/react";
import { MdEmail, MdLocationOn } from "react-icons/md";
import { BsFillBriefcaseFill } from "react-icons/bs";
import { FC } from "react";
import { FaGithub } from "react-icons/fa";
import { FiTwitter } from "react-icons/fi";
import { octokit } from "configurations/ocotokit";
import useSWR from "swr";
interface UserDetailedCardProps {
username: string;
emoji: string;
}
const UserDetailedCard: FC<UserDetailedCardProps> = ({ username, emoji }) => {
const { data } = useSWR(`user-${username}}`, () =>
octokit.users.getByUsername({ username })
);
if (!data) {
return <CircularProgress />;
}
return (
<Box
w="xs"
bg="white"
_dark={{
bg: "#212121",
}}
shadow="xl"
rounded="lg"
overflow="hidden"
>
<Image
w="full"
h={56}
fit="cover"
objectPosition="center"
src={data.data.avatar_url}
alt="avatar"
/>
<Flex
alignItems="center"
px={6}
py={3}
bg="#1c1c1c"
_light={{ bg: "gray.50" }}
>
<span>{emoji}</span>
<chakra.h1 mx={3} fontWeight="bold" fontSize="lg">
{data.data.name ?? data.data.login}
</chakra.h1>
</Flex>
<Box py={4} px={6}>
<chakra.p
py={2}
color="gray.700"
_dark={{
color: "gray.400",
}}
>
{data.data.bio}
</chakra.p>
{data.data.company && (
<Flex
alignItems="center"
mt={4}
color="gray.700"
_dark={{
color: "gray.200",
}}
>
<Icon as={BsFillBriefcaseFill} h={6} w={6} mr={2} />
<chakra.h1 px={2} fontSize="sm">
{data.data.company}
</chakra.h1>
</Flex>
)}
{data.data.location && (
<Flex
alignItems="center"
mt={4}
color="gray.700"
_dark={{
color: "gray.200",
}}
>
<Icon as={MdLocationOn} h={6} w={6} mr={2} />
<chakra.h1 px={2} fontSize="sm">
{data.data.location}
</chakra.h1>
</Flex>
)}
{data.data.email && (
<Flex
alignItems="center"
mt={4}
color="gray.700"
_dark={{
color: "gray.200",
}}
>
<Icon as={MdEmail} h={6} w={6} mr={2} />
<chakra.h1 px={2} fontSize="sm">
{data.data.email}
</chakra.h1>
</Flex>
)}
<HStack justify="center" pt="4">
<IconButton
colorScheme="gray"
as={Link}
aria-label="Github Link"
href={data.data.html_url}
target="_blank"
icon={<FaGithub />}
variant="link"
/>
{data.data.twitter_username && (
<IconButton
colorScheme="gray"
as={Link}
aria-label="Twitter Link"
href={`https://twitter.com/${data.data.twitter_username}`}
target="_blank"
icon={<FiTwitter />}
variant="link"
/>
)}
</HStack>
</Box>
</Box>
);
};
export default UserDetailedCard;

View File

@ -1,72 +0,0 @@
import Script from "next/script";
import { FC, useId } from "react";
type AdComponent = FC<{
slot: string;
}>;
export const DisplayAd: AdComponent = ({ slot }) => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
data-ad-slot={slot}
data-ad-format="auto"
data-full-width-responsive="true"
></ins>
<Script
id={id + "#" + slot}
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};
export const GridMultiplexAd: AdComponent = ({ slot }) => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-format="autorelaxed"
data-ad-client={process.env.NEXT_PUBLIC_ADSENSE_ID}
data-ad-slot={slot}
></ins>
<Script
id={id + "#" + slot}
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};
export const InFeedAd = () => {
const id = useId();
return (
<>
<ins
className="adsbygoogle"
style={{ display: "block" }}
data-ad-format="fluid"
data-ad-layout-key="-e5+6n-34-bt+x0"
data-ad-client="ca-pub-6419300932495863"
data-ad-slot="6460144484"
></ins>
<Script
id={id}
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `(adsbygoogle = window.adsbygoogle || []).push({});`,
}}
></Script>
</>
);
};

View File

@ -1,17 +0,0 @@
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
// https://developers.google.com/analytics/devguides/collection/gtagjs/pages
export const pageview = (url: any) => {
(window as any).gtag("config", GA_TRACKING_ID, {
page_path: url,
});
};
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }: any) => {
(window as any).gtag("event", action, {
event_category: category,
event_label: label,
value: value,
});
};

View File

@ -1,3 +0,0 @@
import { Octokit } from "@octokit/rest";
export const octokit: Octokit = new Octokit();

View File

@ -1,26 +0,0 @@
import { useEffect, useState } from "react";
import { detectOS } from "detect-browser";
export enum Platform {
linux = "Linux",
windows = "Windows",
mac = "Mac",
android = "Android",
}
export function usePlatform(): Platform {
const [platform, setPlatform] = useState(Platform.linux);
useEffect(() => {
const detectedPlatform = detectOS(navigator.userAgent)?.toLowerCase();
if (!detectedPlatform) return;
if (detectedPlatform.includes("windows")) setPlatform(Platform.windows);
else if (detectedPlatform.includes("mac")) setPlatform(Platform.mac);
else if (detectedPlatform.includes("android"))
setPlatform(Platform.android);
}, []);
return platform;
}

View File

@ -1,43 +0,0 @@
import {
Link as Anchor,
Heading,
Text,
chakra,
Code,
HStack,
Divider,
Box,
} from "@chakra-ui/react";
import { Options } from "react-markdown";
export const MarkdownComponentDefs: Options["components"] = {
a: (props: any) => <Anchor {...props} color="blue.500" />,
h1: (props: any) => <Heading {...props} size="xl" mt="5" mb="1.5" />,
h2: (props: any) => <Heading {...props} size="lg" mt="5" mb="1.5" />,
h3: (props: any) => <Heading {...props} size="md" mt="5" mb="1.5" />,
h4: (props: any) => <Heading {...props} size="sm" />,
h5: (props: any) => <Heading {...props} size="xs" />,
h6: (props: any) => <Heading {...props} size="xs" />,
p: (props: any) => <Text {...props} />,
li: (props: any) => <chakra.li {...props} ml="4" />,
code: (props) => (
<Code
{...props}
p={!props.inline ? 5 : 0}
overflow="scroll"
colorScheme="gray"
maxW="full"
/>
),
blockquote: (props) => {
return (
<HStack bgColor="blackAlpha.300" py="3" px="2">
<Box borderLeft="2px solid gray" pl="2">
<Text as="span" fontSize="sm">
{props.children}
</Text>
</Box>
</HStack>
);
},
};

View File

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@ -1,7 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
}
module.exports = nextConfig

6391
website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +1,63 @@
{ {
"name": "website", "name": "website",
"version": "0.1.0", "version": "0.0.1",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "next dev", "dev": "vite dev",
"build": "next build", "build": "vite build",
"start": "next start", "preview": "vite preview",
"lint": "next lint" "test": "playwright test",
}, "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"dependencies": { "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"@babel/core": "^7.18.6", "lint": "prettier --check . && eslint .",
"@chakra-ui/react": "^2.2.4", "format": "prettier --write ."
"@chakra-ui/system": "^2.2.2",
"@chakra-ui/theme-tools": "^2.0.5",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@octokit/rest": "^19.0.3",
"@types/progress": "^2.0.5",
"detect-browser": "^5.3.0",
"framer-motion": "^6",
"gray-matter": "^4.0.3",
"next": "12.2.2",
"nextjs-progressbar": "^0.0.14",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.4.0",
"react-markdown": "^8.0.3",
"remark-gemoji": "^7.0.1",
"remark-gfm": "^3.0.1",
"swr": "^1.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.0.5", "@playwright/test": "^1.28.1",
"@types/react": "18.0.15", "@skeletonlabs/skeleton": "2.8.0",
"@types/react-dom": "18.0.6", "@skeletonlabs/tw-plugin": "0.3.1",
"@types/react-syntax-highlighter": "^15.5.3", "@sveltejs/adapter-cloudflare": "^4.1.0",
"eslint": "8.20.0", "@sveltejs/kit": "^2.0.0",
"eslint-config-next": "12.2.2", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"eslint-config-prettier": "^8.5.0", "@tailwindcss/typography": "0.5.10",
"typescript": "4.7.4" "@types/eslint": "8.56.0",
"@types/node": "^20.11.16",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.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.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.7",
"svelte-check": "^3.6.0",
"tailwindcss": "3.4.1",
"tslib": "^2.4.1",
"typescript": "^5.0.0",
"vite": "^5.0.3",
"vite-plugin-tailwind-purgecss": "0.2.0"
},
"dependencies": {
"@floating-ui/dom": "1.6.1",
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@octokit/openapi-types": "^19.1.0",
"@octokit/rest": "^20.0.2",
"date-fns": "^3.3.1",
"highlight.js": "11.9.0",
"lucide-svelte": "^0.323.0",
"mdsvex-relative-images": "^1.0.3",
"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"
} }
} }

View File

@ -1,138 +0,0 @@
import "../styles/globals.css";
import type { AppProps } from "next/app";
import {
ChakraProvider,
extendTheme,
withDefaultColorScheme,
} from "@chakra-ui/react";
import Navbar from "components/Navbar";
import { chakra } from "@chakra-ui/react";
import { mode } from "@chakra-ui/theme-tools";
import Head from "next/head";
import { useRouter } from "next/router";
// import Script from "next/script";
// import * as gtag from "configurations/gtag";
// import AdDetector from "components/AdDetector";
import Footer from "components/Footer";
import NextNProgress from "nextjs-progressbar";
const customTheme = extendTheme(
{
initialColorMode: 'system',
useSystemColorMode: true,
styles: {
global: (props: any) => ({
body: {
bg: mode("white", "#171717")(props),
},
}),
},
colors: {
blue: {
50: "#e6f2ff",
100: "#e6f2ff",
200: "#e6f2ff",
300: "#1681bd",
400: "#1681bd",
500: "#3a4da5",
600: "#2d3c7d",
700: "#1f2b55",
800: "#121c2e",
900: "#080e18",
},
components: {
Link: {
baseStyle: {
color: "blue",
},
},
},
},
},
withDefaultColorScheme({ colorScheme: "blue" })
);
function MyApp({ Component, pageProps }: AppProps) {
const router = useRouter();
// useEffect(() => {
// const handleRouteChange = (url: string) => {
// gtag.pageview(url);
// };
// router.events.on("routeChangeComplete", handleRouteChange);
// router.events.on("hashChangeComplete", handleRouteChange);
// return () => {
// router.events.off("routeChangeComplete", handleRouteChange);
// router.events.off("hashChangeComplete", handleRouteChange);
// };
// }, [router.events]);
return (
<>
{/* <Script
async
onError={(e) => {
console.error("Script failed to load", e);
}}
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${process.env.NEXT_PUBLIC_ADSENSE_ID}`}
crossOrigin="anonymous"
/> */}
{/* Global Site Tag (gtag.js) - Google Analytics */}
{/* <Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
/> */}
{/* <Script
id="gtag-init"
strategy="afterInteractive"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gtag.GA_TRACKING_ID}', {
page_path: window.location.pathname,
});
`,
}}
/> */}
<ChakraProvider theme={customTheme}>
<Head>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<title>Spotube</title>
</Head>
<NextNProgress color="#00a7a4" />
<chakra.div
minH="100vh"
display="flex"
flexDir="column"
justifyContent="space-between"
>
<div>
<Navbar />
<Component {...pageProps} />
</div>
<Footer />
</chakra.div>
</ChakraProvider>
</>
);
}
export default MyApp;

View File

@ -1,68 +0,0 @@
import {
Center,
CircularProgress,
Heading,
HStack,
VStack,
} from "@chakra-ui/react";
import UserDetailedCard from "components/UserDetailedCard";
import { octokit } from "configurations/ocotokit";
import useSwr from "swr";
const maintainers = ["KRTirtho", "RustyApple"];
const About = () => {
const { data } = useSwr("contributors", () =>
octokit.repos.listContributors({
owner: "KRTirtho",
repo: "spotube",
})
);
return (
<VStack my="20" mx="10">
<Heading>Maintainers</Heading>
<HStack pb="20" gap="40px" wrap="wrap" justify="center" align="start">
{data ? (
data.data.map((contributor) => {
if (!maintainers.includes(contributor.login!)) return;
return (
<UserDetailedCard
key={contributor.id}
emoji="⚡"
username={contributor.login!}
/>
);
})
) : (
<Center>
<CircularProgress />
</Center>
)}
</HStack>
<Heading>Valuable Code Contributors💝</Heading>
<HStack gap="40px" wrap="wrap" justify="center" align="start">
{data ? (
data.data.map((contributor) => {
if (maintainers.includes(contributor.login!)) return;
return (
<UserDetailedCard
key={contributor.id}
emoji="💪💝"
username={contributor.login!}
/>
);
})
) : (
<Center>
<CircularProgress />
</Center>
)}
</HStack>
</VStack>
);
};
export default About;

View File

@ -1,119 +0,0 @@
import fs from "fs";
import path from "path";
import { GetStaticPaths, GetStaticProps, NextPage } from "next";
import ReactMarkdown from "react-markdown";
import matter from "gray-matter";
import { BlogMetadata } from ".";
import gfm from "remark-gfm";
import gemoji from "remark-gemoji";
import { MarkdownComponentDefs } from "misc/MarkdownComponentDefs";
import {
Box,
chakra,
Flex,
Heading,
Image,
Text,
VStack,
} from "@chakra-ui/react";
import Head from "next/head";
interface Props {
metadata: BlogMetadata;
content: string;
}
const BlogPost: NextPage<Props> = ({
content,
metadata: { author, author_avatar_url, cover_image, date, tags, title },
}) => {
return (
<VStack>
<Head>
<title>{title}</title>
</Head>
<Box w="full" maxH="xl" overflow="hidden" mb="5">
<Image fit="cover" src={cover_image} alt={title} />
</Box>
<VStack
align="start"
spacing="4"
maxW={{
base: "full",
lg: "70%",
xl: "60%",
}}
py="5"
px="10"
w="full"
>
<Flex alignItems="center">
<Image
h="12"
fit="cover"
rounded="full"
src={author_avatar_url}
alt="Avatar"
/>
<VStack spacing="0">
<Text
mx={2}
fontWeight="bold"
color="gray.700"
_dark={{
color: "gray.200",
}}
>
{author}
</Text>
<chakra.span
mx={1}
fontSize="sm"
color="gray.600"
_dark={{
color: "gray.300",
}}
>
{date}
</chakra.span>
</VStack>
</Flex>
<Heading>{title}</Heading>
<ReactMarkdown
components={MarkdownComponentDefs}
remarkPlugins={[gfm, gemoji]}
>
{content}
</ReactMarkdown>
</VStack>
</VStack>
);
};
export default BlogPost;
export const getStaticPaths: GetStaticPaths = async () => {
const paths = fs.readdirSync("posts").map((file) => {
return {
params: {
slug: file.replace(".md", ""),
},
};
});
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps<Props> = async (ctx) => {
const { content, data } = matter(
fs.readFileSync(path.join("posts", `${ctx.params?.slug}.md`), "utf-8")
);
return {
props: {
content,
metadata: data as BlogMetadata,
},
};
};

View File

@ -1,66 +0,0 @@
import fs from "fs";
import path from "path";
import { GetStaticProps, NextPage } from "next";
import matter from "gray-matter";
import ArticleCard from "components/ArticleCard";
import { VStack } from "@chakra-ui/react";
import Head from "next/head";
export interface BlogMetadata {
title: string;
cover_image: string;
author: string;
date: string;
author_avatar_url: string;
tags: string[];
summary: string;
}
export interface BlogPost {
slug: string;
metadata: BlogMetadata;
}
interface Props {
posts: BlogPost[];
}
export const getStaticProps: GetStaticProps<Props> = async () => {
const posts = fs.readdirSync("posts").map((file) => {
return {
slug: file.replace(".md", ""),
metadata: matter(fs.readFileSync(path.join("posts", file)))
.data as BlogMetadata,
};
});
return {
props: {
posts: posts.sort(
// @ts-ignore
(a, b) => new Date(b.metadata.date) - new Date(a.metadata.date)
),
},
};
};
const Blog: NextPage<Props> = ({ posts }) => {
return (
<VStack mx="5" my="5" spacing="7">
<Head>
<title>Spotube - Blogs</title>
</Head>
{posts.map((post) => {
return (
<ArticleCard
key={post.slug}
metadata={post.metadata}
slug={post.slug}
/>
);
})}
</VStack>
);
};
export default Blog;

View File

@ -1,181 +0,0 @@
import {
Heading,
VStack,
chakra,
HStack,
Text,
useColorModeValue,
} from "@chakra-ui/react";
import DownloadButton from "components/DownloadButton";
import Image from 'next/image';
const Root = () => {
const textColor = useColorModeValue("#171717", "#f5f5f5");
return (
<>
<VStack spacing="$4" alignItems="stretch">
<chakra.section
h="60vh"
bgColor={useColorModeValue("#f5f5f5", "#171717")}
bgImage="url(/spotube-screenshot-web.jpg)"
bgRepeat="no-repeat"
bgSize="contain"
bgPos={useColorModeValue("right", "left")}
>
<VStack mt="10" mx="6" spacing="4" alignItems={useColorModeValue("flex-start", "flex-end")}>
<chakra.section
p={{ base: "5", md: "0" }}
borderRadius="2xl"
bgColor={{
base: "#f5f5f599",
md: "transparent",
}}
>
<Heading mb={4} color={textColor} size="2xl">
Spotube
</Heading>
<Heading color={textColor} size="lg" maxW="500px">
A fast, modern, lightweight & efficient Spotify Music Client for
every platform
</Heading>
</chakra.section>
<DownloadButton />
</VStack>
</chakra.section>
{/* <DisplayAd slot="9501208974" /> */}
<chakra.div bgGradient="linear-gradient(180deg, rgba(249,207,143,1) 0%, rgba(250,250,250,1) 65%)">
<VStack
p="5"
mt="10"
pos="relative"
_before={{
content: "''",
position: "absolute",
left: 0,
right: 0,
zIndex: -1,
display: "block",
height: "100%",
width: "100%",
background: "url('/headline-2b.png') rgba(0, 0, 0, 0.9)",
bgSize: "contain",
bgRepeat: "no-repeat",
bgPos: "center",
filter: "blur(2px)",
}}
flexDirection={{ base: "column", md: "row" }} // Stack content vertically on mobile, reverse order on desktop
alignItems="center" // Center align content vertically on mobile
textAlign="center" // Center align text horizontally on mobile
>
<chakra.div maxW="600px">
<Image
src="/headline-1.png"
m="10"
bgRepeat="no-repeat"
bgSize="contain"
h="60vh"
width="1020"
height="780"
layout="intrinsic"
alt="Headline 2"
/>
</chakra.div>
<chakra.div
margin="auto"
maxW="500px"
bgGradient={{
base: "radial-gradient(circle, #ffffff 23%, rgba(0,0,0,0) 82%)",
md: "radial-gradient(circle, #ffffff00 23%, rgba(0,0,0,0) 82%)",
}}
color="gray.800"
>
<Heading mb={2}>Access all your Spotify Music & Playlists</Heading>
<Text>
You can use all your Spotify tracks & playlists here. Everything
will be in Sync & some action taken from Spotube will also
reflect on your Spotify Account.
</Text>
</chakra.div>
</VStack>
</chakra.div>
<VStack
p="5"
mt="10"
pos="relative"
_before={{
content: "''",
position: "absolute",
left: 0,
right: 0,
zIndex: -1,
display: "block",
height: "100%",
width: "100%",
background: "url('/headline-2b.png') rgba(0, 0, 0, 0)",
bgSize: "contain",
bgRepeat: "no-repeat",
bgPos: "center",
filter: "blur(2px)",
}}
>
<chakra.div maxW="600px">
<Image
src="/headline-2a.svg"
width="1920"
height="1080"
layout="intrinsic"
alt="Headline 2"
/>
</chakra.div>
<chakra.div
maxW="600px"
color="gray.50"
bgColor="blackAlpha.600"
p="3"
borderRadius="3xl"
>
<Heading>On your Mobile, PC, Tablet everywhere</Heading>
<Text>
Spotube is available for all &quot;Major&quot; Platforms including
<b> Linux, Android, Windows & MacOS </b>natively unlike Spotify
Desktop App which uses Electron, that is basically the entire
Chrome
</Text>
</chakra.div>
</VStack>
{/* <DisplayAd slot="9501208974" /> */}
<HStack wrap="wrap-reverse" justify="center" px="8" align="center">
<chakra.div maxW="400px">
<Heading>Open Source, privacy respecting & No ads</Heading>
<Text>
Spotube is an Open Source App being developed & maintained by
Kingkor&nbsp;Roy&nbsp;Tirtho & is built by the Contributions of
the Community and Contributors
</Text>
</chakra.div>
<chakra.div maxW="500px">
<Image
src="/headline-3.svg"
width="1920"
height="1080"
layout="intrinsic"
alt="Headline 2"
/>
</chakra.div>
</HStack>
<VStack py="10" alignItems="center" textAlign="center">
<div>
<Heading size="lg">Download Now</Heading>
<Text>
Download Spotube for every platform you want. It's available everywhere.
</Text>
</div>
<DownloadButton />
</VStack>
</VStack>
</>
);
};
export default Root;

View File

@ -1,109 +0,0 @@
import {
Link as Anchor,
Heading,
VStack,
chakra,
Box,
Flex,
Stack,
HStack,
useColorModeValue,
} from "@chakra-ui/react";
import NavLink from "next/link";
// import { GridMultiplexAd } from "components/special";
import { GiBackwardTime } from "react-icons/gi";
import { FiPackage } from "react-icons/fi";
import { HiOutlineSparkles } from "react-icons/hi";
import { useRouter } from "next/router";
import { FC } from "react";
function OtherDownloads() {
const router = useRouter();
return (
<>
<Flex justify="center">
<VStack my="20" mx="5" maxW="3xl" spacing="28">
<VStack spacing="2" align="start">
<Heading size="2xl">Other ways to install?</Heading>
<Heading size="md">
Here&apos;s some alternative ways & versions of Spotube that you
can install try out
</Heading>
</VStack>
<Stack direction={["column", null, "row"]} spacing="4">
<OtherDownloadLinkItem
href={"/package-manager"}
icon={<FiPackage />}
>
Package Managers &amp; AppStores
</OtherDownloadLinkItem>
<OtherDownloadLinkItem
href="/nightly-downloads"
icon={<HiOutlineSparkles />}
color={useColorModeValue("red.500", "red.200")}
bgColor={useColorModeValue("red.100", "red.800")}
>
Nightly versions
</OtherDownloadLinkItem>
<OtherDownloadLinkItem
href={"/stable-downloads"}
icon={<GiBackwardTime />}
>
Previous versions
</OtherDownloadLinkItem>
&nbsp;(Nightly&nbsp;releases)
</Stack>
</VStack>
</Flex>
{/* <GridMultiplexAd slot="4575915852" /> */}
</>
);
}
export default OtherDownloads;
interface OtherDownloadLinkItemType {
href: string;
icon: React.ReactNode;
color?: string;
bgColor?: string;
children: React.ReactNode;
}
const OtherDownloadLinkItem: FC<OtherDownloadLinkItemType> = ({
href,
icon,
color,
bgColor,
children,
}) => {
const router = useRouter();
const dColor = useColorModeValue("blue.500", "blue.200");
const dBColor = useColorModeValue("blue.100", "blue.800");
return (
<NavLink href={router.pathname + href} passHref style={{ width: "100%" }}>
<Anchor color={color ?? dColor} w="100%">
<Box
w="100%"
h="40"
bgColor={bgColor ?? dBColor}
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
p="5"
borderRadius="lg"
fontSize="1.2rem"
textAlign="center"
>
<chakra.p mb="2" fontSize="4xl">
{icon}
</chakra.p>
{children}
</Box>
</Anchor>
</NavLink>
);
};

View File

@ -1,118 +0,0 @@
import {
Link as Anchor,
Button,
Heading,
Table,
Td,
Th,
VStack,
chakra,
Text,
Tr,
HStack,
} from "@chakra-ui/react";
import { octokit } from "configurations/ocotokit";
import { GetServerSideProps, NextPage } from "next";
// import { GridMultiplexAd } from "components/special";
import NavLink from "next/link";
import { ReleaseResponse } from "./stable-downloads";
type NightlyProps = ReleaseResponse;
export const getServerSideProps: GetServerSideProps<
NightlyProps
> = async () => {
const { data } = await octokit.repos.getReleaseByTag({
owner: "KRTirtho",
repo: "spotube",
tag: "nightly",
});
return {
props: {
tag_name: data.tag_name,
id: data.id,
body: data.body,
assets: data.assets.map((asset) => ({
id: asset.id,
name: asset.name,
browser_download_url: asset.browser_download_url,
})),
},
};
};
const NightlyDownloads: NextPage<NightlyProps> = (props) => {
return (
<>
<VStack>
<VStack
alignSelf="center"
alignItems="flex-start"
spacing="4"
maxW="500px"
overflow="auto"
m="5"
>
<Heading size="2xl">Nightly Release</Heading>
<Text>Download latest & most bleeding edge version of Spotube</Text>
<Text size="sm" color="red.500" textAlign="justify">
Disclaimer!: Nightly versions are untested and not the final version
of spotube. So it can consists of many hidden or unknown bugs but at
the same time will be opted with latest & greatest features too. So
use it at your own risk! If you don&apos;t know what you&apos;re
doing than we recommend you to download the{" "}
<NavLink href="/" passHref>
<Anchor color="blue.500">latest stable version of</Anchor>
</NavLink>{" "}
Spotube
</Text>
<VStack
p="2"
w="100%"
borderRadius="md"
spacing="4"
bgColor="gray.50"
_dark={{ bgColor: "gray.900" }}
>
{Object.entries(props.assets).map(
([_, { name, id, browser_download_url }], i) => {
const segments = name.split("-");
const platform = segments[1];
const executable = segments[segments.length - 1].split(".")[1];
return (
<HStack
key={id}
p="4"
w="100%"
borderRadius="md"
bgColor="gray.100"
_dark={{ bgColor: "gray.800" }}
>
<Text w="200px" textTransform="capitalize">
{platform}{" "}
<chakra.span color="gray.500">({executable})</chakra.span>
</Text>
<Anchor
overflowWrap="break-word"
wordBreak="break-word"
w="full"
href={browser_download_url}
color="blue.500"
>
{name}
</Anchor>
</HStack>
);
}
)}
</VStack>
</VStack>
<chakra.div w="full">
{/* <GridMultiplexAd slot="3192619797" /> */}
</chakra.div>
</VStack>
</>
);
};
export default NightlyDownloads;

View File

@ -1,117 +0,0 @@
import {
Box,
Code,
Heading,
HStack,
Icon,
Image,
Link,
Text,
VStack,
} from "@chakra-ui/react";
import { CodeBlock } from "components/CodeBlock";
import { FcLinux } from "react-icons/fc";
import { BsWindows } from "react-icons/bs";
// import { DisplayAd } from "components/special";
export default function PackageManagerPage() {
return (
<VStack p="5" spacing="5">
<VStack align="start" w="full" maxW="2xl">
<Heading>Installation using Package Managers</Heading>
<Text>
If you don&apos;t want to download the binary of Spotube, you can use
various package managers to install Spotube too (only Windows & Linux
now)
</Text>
<Heading size="lg" pt="5">
Linux
<Icon>
<FcLinux />
</Icon>
</Heading>
<Heading size="md" pt="3">
Flatpak
</Heading>
<Text>
Make sure Flatpak is installed in your Linux device & Run the
following command in the terminal:
</Text>
<CodeBlock>$ flatpak install com.github.KRTirtho.Spotube</CodeBlock>
<Heading size="md" pt="3">
AUR
</Heading>
<Text>
If you&apos;re an Arch Linux user, you can also install Spotube from
AUR. Make sure you have <Code>yay</Code> or <Code>pamac</Code> or{" "}
<Code>paru</Code> installed in your system. And Run the Following
command in the Terminal:
</Text>
<CodeBlock>
# for pamac user
<br />$ pamac install spotube-bin
</CodeBlock>
<CodeBlock>
# for paru user
<br />$ paru -Sy spotube-bin
</CodeBlock>
<CodeBlock>
# for yay user
<br />$ yay -Sy spotube-bin
</CodeBlock>
<Box w="full">
{/* <DisplayAd slot="9501208974" /> */}
</Box>
<HStack align="center" pt="5">
<Heading size="lg">Windows</Heading>
<BsWindows fontSize="25px" color="#00A4EF" />
</HStack>
<Heading size="md" pt="3">
Chocolatey
</Heading>
<Text>
Spotube is available in{" "}
<Link
color="blue.500"
target="_blank"
href="community.chocolatey.org"
>
community.chocolatey.org
</Link>{" "}
repo. If you have chocolatey install in your system just run following
command in an Elevated Command Prompt or PowerShell:
</Text>
<CodeBlock>$ choco install spotube</CodeBlock>
<Heading size="md" pt="3">
WinGet
</Heading>
<Text>
Yes, 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:
</Text>
<CodeBlock>$ winget install --id KRTirtho.Spotube</CodeBlock>
<Box w="full">
{/* <DisplayAd slot="9501208974" /> */}
</Box>
<Heading pt="5">Install from App Stores</Heading>
<Heading size="md">Android (F-Droid)</Heading>
<Text>
Spotube for Android is available in the F-Droid repositories too. So
you can install it directly from F-Droid Android application too
</Text>
<Link
href="https://f-droid.org/packages/oss.krtirtho.spotube/"
target="_blank"
>
<Image
src="https://user-images.githubusercontent.com/61944859/174589876-bace24c0-b3fd-4c4a-bdb4-6fa82b5853ec.png"
alt="F-Droid Download"
height="70"
width="240"
/>
</Link>
</VStack>
</VStack>
);
}

View File

@ -1,195 +0,0 @@
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Link as Anchor,
Heading,
HStack,
Text,
VStack,
chakra,
} from "@chakra-ui/react";
import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
import ReactMarkdown from "react-markdown";
import { Platform } from "hooks/usePlatform";
import gfm from "remark-gfm";
// import { DisplayAd, InFeedAd } from "components/special";
import { GetServerSideProps, NextPage } from "next";
import { MarkdownComponentDefs } from "misc/MarkdownComponentDefs";
import { octokit } from "configurations/ocotokit";
import gemoji from "remark-gemoji";
enum AssetTypes {
sums = "sums",
linux = "linux",
mac = "mac",
windows = "windows",
android = "android",
}
export type ReleaseResponse = {
id: number;
body: string | null | undefined;
tag_name: string;
assets: {
id: number;
name: string;
browser_download_url: string;
}[];
};
type Props = {
data: ReleaseResponse[];
};
export const getServerSideProps: GetServerSideProps<Props> = async ({
res,
}) => {
res.setHeader(
"Cache-Control",
"public, s-maxage=10, stale-while-revalidate=59"
);
const { data } = await octokit.repos.listReleases({
owner: "KRTirtho",
repo: "spotube",
});
const releaseResponse: ReleaseResponse[] = data
.map((data) => {
return {
tag_name: data.tag_name,
id: data.id,
body: data.body,
assets: data.assets.map((asset) => ({
id: asset.id,
name: asset.name,
browser_download_url: asset.browser_download_url,
})),
};
})
.filter((release) => release.tag_name !== "nightly");
return {
props: {
data: releaseResponse,
},
};
};
const StableDownloads: NextPage<Props> = ({ data }) => {
return (
<VStack alignItems="center">
<VStack alignItems="stretch" m="3">
<Heading size="xl">Previous Versions</Heading>
<Text my="5">
If any of your version is not working correctly than you can download
& use previous versions of Spotube too
</Text>
<VStack
alignItems="flex-start"
spacing="3"
mr="1"
>
{data.map((release, i) => {
const releaseSome = release.assets
.map((asset) => {
const platform =
Object.keys(Platform).find((platform) =>
asset.name.toLowerCase().includes(platform)
) ?? "sums";
return {
type: AssetTypes[platform as keyof typeof AssetTypes],
asset,
};
})
.reduce((acc, val) => {
acc[val.type] = [...(acc[val.type] ?? []), val.asset];
return acc;
}, {} as any) as {
[key in AssetTypes]: RestEndpointMethodTypes["repos"]["listReleases"]["response"]["data"][0]["assets"];
};
return (
<VStack key={release.id} py="3" w="full">
<Heading size="md">
Version{" "}
<Text as="span" color="blue.500">
{release.tag_name}
</Text>{" "}
{i == 0 && "(Latest)"}
</Heading>
{Object.entries(releaseSome).map(([type, assets], i) => {
return (
<HStack
key={i}
spacing={0}
p="2"
alignItems="flex-start"
bgColor="gray.50"
_dark={{ bgColor: "gray.900" }}
>
<Heading
w={90}
p="2"
colorScheme="blue"
borderRadius="5px 0 0 5px"
borderRight="none"
size="sm"
>
{type[0].toUpperCase() + type.slice(1)}
</Heading>
<VStack
alignItems="flex-start"
w={{
base: "full",
sm: "72",
}}
spacing={2}
>
{assets.map((asset) => {
return (
<Anchor
key={asset.id}
color="blue.500"
width="full"
p="1.5"
href={asset.browser_download_url}
target="_blank"
referrerPolicy="no-referrer"
bgColor="gray.100"
_dark={{ bgColor: "gray.800" }}
borderRadius="md"
>
{asset.name}
</Anchor>
);
})}
</VStack>
</HStack>
);
})}
<Accordion defaultIndex={i} allowToggle>
<AccordionItem>
<AccordionButton>
Release Notes <AccordionIcon />
</AccordionButton>
<AccordionPanel>
<ReactMarkdown
components={MarkdownComponentDefs}
remarkPlugins={[gfm, gemoji]}
>
{release.body ?? ""}
</ReactMarkdown>
</AccordionPanel>
</AccordionItem>
</Accordion>
</VStack>
);
})}
</VStack>
</VStack>
</VStack>
);
};
export default StableDownloads;

View File

@ -0,0 +1,12 @@
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;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,84 +0,0 @@
---
title: Getting Started With Spotube
cover_image: https://github.com/KRTirtho/spotube/raw/master/assets/spotube-screenshot.png
date: "July 16, 2022"
author: Kingkor Roy Tirtho
author_avatar_url: https://avatars.githubusercontent.com/u/61944859?v=4
tags:
- getting-started
- spotube
summary: You installed Spotube, don't know what to do now? Then don't worry we Gotchu covered here. We'll guide you through the basics of using Spotube & how you can use it to enrich your daily life with music.
---
So installing Spotube is done. Its a new app & although most of the things can be understood just by using the app as the UI is descriptive but still sometimes newcomers become overwhelmed by its UI & functionalities. In this article, were going to guide you through the problems that you can face while using Spotube by misunderstanding
## Login with Spotify
The most common issue with Spotube when someone uses it for the first is with its Login mechanism. Spotube is a Spotify client. And Spotify clients usually require *Premium Accounts* as the Spotify Server only allows premium accounts to download music. But **in Spotube you dont need a Premium Account**
Since Spotube doesnt require premium accounts it needs to special workarounds to supply the Music for Free. Thus, the Login mechanism in Spotube is a bit lengthy, but its actually more secure than any other Spotify Clients as the data stays in your Account & doesnt need to go through a middleware unlike other Spotify Clients
Now, lets get through the login Part. For Login, youll need two special things
- Client ID
- Client Secret
**What is a Client ID?**
Client ID is kind of a Public key (ID) that is a unique identifier for the Spotify API client & is usually used to pair with Client Secret
**What is a Client Secret?**
A client secret is **a secret known only to the application instance and the authorization server**. It protects Spotify Data by only granting tokens to authorized requestors
Now In Spotube, tap on the Settings Icon in the Sidebar/Bottom Bar & click on the **Login With Spotify** button.
Now youll have to open a browser & have to go to [developers.spotify.com/dashboard](https://developers.spotify.com/dashboard) & press the Login button
![step-1.png](https://rawcdn.githack.com/KRTirtho/spotube/0e10ddfa54113eb559308be1eb976b707dd7410c/assets/tutorial/step-1.png)
After Login, create a Spotify Developer App by clicking on the CREATE AN APP button. Give the App a name & description & hit CREATE. Finally, the view will look somewhat similar to below
![step-2.png](https://rawcdn.githack.com/KRTirtho/spotube/0e10ddfa54113eb559308be1eb976b707dd7410c/assets/tutorial/step-2.png)
Now comes the **Most Important Part…**
First Tap on the App to enter the dashboard page. In there, tap EDIT SETTINGS & a Dialog with multiple configuration will Open
![step-3a.jpg](https://rawcdn.githack.com/KRTirtho/spotube/0e10ddfa54113eb559308be1eb976b707dd7410c/assets/tutorial/step-3a.jpg)
Now find the **Redirect Uris** and type/paste `http://localhost:4304/auth/spotify/callback` in the field and press Add. Finally scroll down to the bottom section of the Dialog and press the SAVE button to save the changes
![step-3b.jpg](https://rawcdn.githack.com/KRTirtho/spotube/0e10ddfa54113eb559308be1eb976b707dd7410c/assets/tutorial/step-3b.jpg)
Now close the Dialog & see in the Left/Below of the EDIT SETTINGS button, there youll find **Client ID** and a SHOW CLIENT SECRET button. Copy the *Client ID* & paste it in the Spotubes Text Field. Then tap/click on the SHOW CLIENT SECRET button to reveal the **Client Secret.** Finally, copy the **Client Secret** & paste it in the Spotubes corresponding Text Field
![step-4.jpg](https://rawcdn.githack.com/KRTirtho/spotube/0e10ddfa54113eb559308be1eb976b707dd7410c/assets/tutorial/step-4.jpg)
Finally, press on the *Submit Button* which will open a Browser Window/Tab (desktop) or a Browser Application (android). Press/Click ALLOW button in that page & now youre successfully Logged In with your Spotify Account in Spotube
Close the Browser Tab (optional) & Go back to Spotube and Enjoy your lifetime (probably) free Music
## Playing Playlists & Tracks
You can play any playlists in the Home Screen of Spotube just by pressing the Play button of the playlist. But this is not just it. You can also play any playlist from starting from any track of the playlist and this available in all platform unlike Spotify which doesn't allow this kind stupid simple stuff in the Mobile App
Just tap on any Playlist's Cover Image & from the track list, tap the little Play button next to the track from which you want the playlist to start playing.
![Bpfau.png](https://s6.imgcdn.dev/Bpfau.png)
## Sing along with any Song with **Synced Lyrics** just like Karaoke
Yes, Spotube support Synced Lyrics too (most of the popular English songs). So, if you wanna sing along with your favorite Music Track but don't know the Lyrics Spotube got you covered & now even you can sing along. Here's how, just play any playlist/album/track & press on the Lyrics Tab in the Sidebar (desktop/tablet) or Bottom Bar (mobile) & the Synced Lyrics will automatically start playing with the Audio Track
![BpttL.png](https://s6.imgcdn.dev/BpttL.png)
## Discover Weekly Playlist
Spotube categorizes playlists by Genre & always shows the most trending playlists in the home screen. In Spotube the Music isn't personalized & it shows you the playlists with best music. So the experience is more vast & what you experience is genuinely what you love & want to listen to not forced or manipulated to
But there's one question, **where the heck is my Discover Weekly playlist**?
It's there. But doesn't show up in the home screen because it's a special kind of playlist that Spotify doesn't allow third-party clients to access unless the User specifically searches for it. You can find your Discover Weekly playlist by simply searching on Spotube's search page like below
![BpVcB.png](https://s6.imgcdn.dev/BpVcB.png)

View File

View File

@ -0,0 +1,41 @@
---
title: Spotube Basics
author: Kingkor Roy Tirtho
date: 2024-02-10
published: true
---
Spotube is an open-source Spotify client that allows users to stream music from Spotify. To use Spotube, you need to sign in with your Spotify account. Here's a step-by-step guide on how to sign in to Spotube.
## Prerequisites
Before you begin, make sure you have the following:
- A Spotify account
- The Spotube application installed on your device
## Steps
1. Open the Spotube application on your device.
2. Click on the "Sign in with Spotify" button.
3. You will be redirected to the Spotify login page. Enter your Spotify username and password and click on the "Log In" button.
4. You will be asked to grant Spotube permission to access your Spotify account. Click on the "Agree" button to grant permission.
5. You will be redirected back to the Spotube application, and you should now be signed in to your Spotify account.
That's it! You are now signed in to Spotube and can start streaming music from Spotify.
| Title | Author | Date | Published |
| -------------- | ------------------ | ---------- | --------- |
| Spotube Basics | Kingkor Roy Tirtho | 2024-02-10 | true |
```bash
$ git clone
```
```javascript
const a = 1;
```
## Conclusion
Signing in to Spotube is a simple process that requires you to have a Spotify account and the Spotube application installed on your device. By following the steps outlined in this tutorial, you should be able to sign in to Spotube and start streaming music from Spotify.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1 +0,0 @@
google-site-verification: google7d445d7199e703dc.html

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1 +0,0 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

22
website/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
// 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 module '@fortawesome/pro-solid-svg-icons/index.es' {
export * from '@fortawesome/pro-solid-svg-icons';
}

23
website/src/app.html Normal file
View File

@ -0,0 +1,23 @@
<!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" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" data-theme="wintry">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

11
website/src/app.postcss Normal file
View File

@ -0,0 +1,11 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind variants;
/* vintage theme */
@font-face {
font-family: 'Abril Fatface';
src: url('/fonts/AbrilFatface.ttf');
font-display: swap;
}

View File

@ -0,0 +1,25 @@
<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>

View File

@ -0,0 +1,3 @@
<article class="prose lg:prose-lg dark:prose-invert max-w-3xl">
<slot />
</article>

View File

@ -0,0 +1,32 @@
<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">
{#if label}
<label class="ps-4">{label}</label>
{/if}
<SlideToggle
active="bg-primary-backdrop-token"
size="sm"
name="dark-mode"
checked={$isDark}
on:change={() => {
isDark.update((prev) => !prev);
}}
/>
</div>

View File

@ -0,0 +1,57 @@
<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>

View File

@ -0,0 +1,37 @@
<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>

61
website/src/lib/index.ts Normal file
View File

@ -0,0 +1,61 @@
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';
export const routes: Record<string, [string, any]> = {
'/': ['Home', Home],
'/blog': ['Blog', Newspaper],
'/downloads': ['Downloads', Download],
'/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]],
'Windows Executable': [`${releasesUrl}/Spotube-windows-x86_64-setup.exe`, [faWindows]],
'macOS Dmg': [`${releasesUrl}/Spotube-macos-universal.dmg`, [faApple]],
'Ubuntu, Debian': [`${releasesUrl}/Spotube-linux-x86_64.deb`, [faUbuntu, faDebian]],
'Fedora, Redhat, Opensuse': [
`${releasesUrl}/Spotube-linux-x86_64.rpm`,
[faFedora, faRedhat, faOpensuse]
],
'iPhone Ipa': [`${releasesUrl}/Spotube-iOS.ipa`, [faApple]]
};
export const extendedDownloadLinks: Record<string, [string, IconDefinition[], string]> = {
Android: [`${releasesUrl}/Spotube-android-all-arch.apk`, [faAndroid], 'apk'],
Windows: [`${releasesUrl}/Spotube-windows-x86_64-setup.exe`, [faWindows], 'exe'],
macOS: [`${releasesUrl}/Spotube-macos-universal.dmg`, [faApple], 'dmg'],
'Ubuntu, Debian': [`${releasesUrl}/Spotube-linux-x86_64.deb`, [faUbuntu, faDebian], 'deb'],
'Fedora, Redhat, Opensuse': [
`${releasesUrl}/Spotube-linux-x86_64.rpm`,
[faFedora, faRedhat, faOpensuse],
'rpm'
],
iPhone: [`${releasesUrl}/Spotube-iOS.ipa`, [faApple], 'ipa']
};
const nightlyReleaseUrl = 'https://github.com/KRTirtho/Spotube/releases/download/nightly';
export const extendedNightlyDownloadLinks: Record<string, [string, IconDefinition[], string]> = {
Android: [`${nightlyReleaseUrl}/Spotube-android-all-arch.apk`, [faAndroid], 'apk'],
Windows: [`${nightlyReleaseUrl}/Spotube-windows-x86_64-setup.exe`, [faWindows], 'exe'],
macOS: [`${nightlyReleaseUrl}/Spotube-macos-universal.dmg`, [faApple], 'dmg'],
'Ubuntu, Debian': [`${nightlyReleaseUrl}/Spotube-linux-x86_64.deb`, [faUbuntu, faDebian], 'deb'],
'Fedora, Redhat, Opensuse': [
`${nightlyReleaseUrl}/Spotube-linux-x86_64.rpm`,
[faFedora, faRedhat, faOpensuse],
'rpm'
],
iPhone: [`${nightlyReleaseUrl}/Spotube-iOS.ipa`, [faApple], 'ipa']
};

View File

@ -0,0 +1,106 @@
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];
}

41
website/src/lib/posts.ts Normal file
View File

@ -0,0 +1,41 @@
export interface Post {
date: string;
title: string;
tags: string[];
published: boolean;
author: string;
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 };
})
);
let sortedPosts = posts.sort((a, b) => +new Date(b.date) - +new Date(a.date));
sortedPosts = sortedPosts.map((post) => ({
...post
}));
return {
posts: sortedPosts
};
};

View File

@ -0,0 +1,71 @@
<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, storeHighlightJs } 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';
initializeStores();
const drawerStore = getDrawerStore();
</script>
<main class="p-2 md:p-4 flex flex-col min-h-[90vh]">
<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>

View File

@ -0,0 +1,110 @@
<script lang="ts">
import {
faSpotify,
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';
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 Spotify Client for every platform" />
<meta name="keywords" content="spotify, 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="flex flex-col gap-4 ps-4 pt-16 md:ps-24 md:pt-24">
<div>
<h1 class="h1">Spotube</h1>
<br />
<h3 class="h3">
An Open Source <Fa class="inline text-[#1DB954]" icon={faSpotify} /> Spotify Client for every 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">
<a href="https://play.google.com/store/apps/details?id=oss.krtirtho.spotube" target="_blank">
<img
class="-m-2"
src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png"
alt="Google PlayStore"
width="200"
/>
</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>
<br /><br class="hidden md:block" /><br class="hidden md:block" />
<div class="flex justify-center">
<a href="/downloads" class="flex gap-2 btn variant-filled-primary">
Download
<Download />
</a>
</div>
<br /><br />
<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>
</section>

View File

@ -0,0 +1,34 @@
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)
}
};
};

View File

@ -0,0 +1,22 @@
<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>

View File

@ -0,0 +1,9 @@
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);
};

View File

@ -0,0 +1,32 @@
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
const formatter = Intl.DateTimeFormat('en-US', {
dateStyle: 'medium'
});
</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 xl:grid-cols-4">
{#each data.posts as post}
<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"
>
<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>
{/each}
</article>
</section>

View File

@ -0,0 +1,11 @@
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 };
} else {
return { posts: [] };
}
};

View File

@ -0,0 +1,26 @@
<script lang="ts">
import Layout from '$lib/components/markdown/layout.svelte';
import type { PageData } from './$types';
export let data: PageData;
let {
Content,
meta: { date, title, readingTime }
} = data as Required<PageData>;
</script>
<svelte:head>
<title>Blog | {title}</title>
</svelte:head>
<article class="p-4 md:p-16 flex flex-grow flex-col">
<h1 class="h1">{title}</h1>
<br />
<div class="">
<p>{new Date(date).toDateString()}</p>
<p class="mb-16">{readingTime?.text ?? ''}</p>
<Layout>
<svelte:component this={Content} />
</Layout>
</div>
</article>

View File

@ -0,0 +1,23 @@
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}`
};
}
};

View File

@ -0,0 +1,39 @@
<script lang="ts">
import { extendedDownloadLinks } from '$lib';
import { Download } from 'lucide-svelte';
import { History, Sparkles, Package } from 'lucide-svelte';
import DownloadItems from '$lib/components/downloads/download-items.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">
<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} />
<br /><br /><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>
</section>

View File

@ -0,0 +1,33 @@
<script>
import { AlertTriangle, Bug, Sparkles } from 'lucide-svelte';
import DownloadItems from '$lib/components/downloads/download-items.svelte';
import { extendedNightlyDownloadLinks } from '$lib';
</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 />
<DownloadItems links={extendedNightlyDownloadLinks} />
</section>

View File

@ -0,0 +1,138 @@
<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 />
<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={release.assets_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>

View File

@ -0,0 +1,14 @@
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
};
};

View File

@ -0,0 +1,70 @@
---
title: CLI Packages Managers
author: Kingkor Roy Tirtho
---
<script lang="ts">
import { faLinux, faWindows } from '@fortawesome/free-brands-svg-icons';
import Fa from 'svelte-fa';
</script>
<div class="p-4 md:ps-24">
<h2 class="h2">Package Managers</h2>
Spotube is available in various Package Managers supported by Platform
## <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
```
## <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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

BIN
website/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,19 @@
{
"name": "Spotube",
"short_name": "spotube",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1 @@
User-agent: *

View File

@ -1,16 +0,0 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

55
website/svelte.config.js Normal file
View File

@ -0,0 +1,55 @@
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';
/** @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'
}
]
]
})
],
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;

View File

@ -0,0 +1,28 @@
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;

6
website/tests/test.ts Normal file
View File

@ -0,0 +1,6 @@
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();
});

29
website/tsconfig.json Executable file → Normal file
View File

@ -1,23 +1,18 @@
{ {
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "checkJs": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"declaration": true,
"sourceMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "skipLibCheck": true,
"jsx": "preserve", "sourceMap": true,
"incremental": true, "strict": true,
"baseUrl": "." "moduleResolution": "bundler"
}, }
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
"exclude": ["node_modules"] //
// 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
} }

22
website/vite.config.ts Normal file
View File

@ -0,0 +1,22 @@
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: ['..']
}
}
});