mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 13:38:15 -08:00
feat: version v0.0.2
This commit is contained in:
parent
0dceaf017a
commit
7ff317756f
36 changed files with 336 additions and 160 deletions
|
|
@ -55,7 +55,7 @@
|
|||
|
||||
|
||||
:root {
|
||||
--header-xl: 78px;
|
||||
--header-xl: 36px;
|
||||
--header-lg: 28px;
|
||||
--header-md: 22px;
|
||||
--header-sm: 16px;
|
||||
|
|
@ -63,6 +63,18 @@
|
|||
--header-weight: 600;
|
||||
}
|
||||
|
||||
@media (min-width: 60rem) {
|
||||
:root {
|
||||
--header-xl: 78px;
|
||||
--header-lg: 28px;
|
||||
--header-md: 22px;
|
||||
--header-sm: 16px;
|
||||
--header-xl-weight: 600;
|
||||
--header-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
html,
|
||||
body {
|
||||
background-color: var(--color-bg);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getActivity, type getActivityArgs } from "api/api"
|
||||
import { getActivity, type getActivityArgs, type ListenActivityItem } from "api/api"
|
||||
import Popup from "./Popup"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTheme } from "~/hooks/useTheme"
|
||||
|
|
@ -142,44 +142,55 @@ export default function ActivityGrid({
|
|||
}
|
||||
}
|
||||
|
||||
const dotSize = 12;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-start">
|
||||
<h2>Activity</h2>
|
||||
{configurable ?
|
||||
<ActivityOptsSelector rangeSetter={setRange} currentRange={rangeState} stepSetter={setStep} currentStep={stepState} />
|
||||
:
|
||||
const mobileDotSize = 10
|
||||
const normalDotSize = 12
|
||||
|
||||
let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
|
||||
|
||||
let dotSize = vw > 768 ? normalDotSize : mobileDotSize
|
||||
|
||||
return (<div className="flex flex-col items-start">
|
||||
<h2>Activity</h2>
|
||||
{configurable ? (
|
||||
<ActivityOptsSelector
|
||||
rangeSetter={setRange}
|
||||
currentRange={rangeState}
|
||||
stepSetter={setStep}
|
||||
currentStep={stepState}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
}
|
||||
<div className="grid grid-flow-col grid-rows-7 gap-[5px]">
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={new Date(item.start_time).toString()}
|
||||
style={{ width: dotSize, height: dotSize }}
|
||||
)}
|
||||
<div className="flex flex-row flex-wrap w-[94px] md:w-auto md:grid md:grid-flow-col md:grid-cols-7 md:grid-rows-7 gap-[4px] md:gap-[5px]">
|
||||
{data.map((item) => (
|
||||
<div
|
||||
key={new Date(item.start_time).toString()}
|
||||
style={{ width: dotSize, height: dotSize }}
|
||||
>
|
||||
<Popup
|
||||
position="top"
|
||||
space={dotSize}
|
||||
extraClasses="left-2"
|
||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
||||
>
|
||||
<Popup
|
||||
position="top"
|
||||
space={dotSize}
|
||||
extraClasses="left-2"
|
||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${item.listens} plays`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
display: 'inline-block',
|
||||
background:
|
||||
item.listens > 0
|
||||
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
|
||||
: 'var(--color-bg-secondary)',
|
||||
}}
|
||||
className={`rounded-[3px] ${item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'}`}
|
||||
></div>
|
||||
</Popup>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
background:
|
||||
item.listens > 0
|
||||
? LightenDarkenColor(color, getDarkenAmount(item.listens, 100))
|
||||
: 'var(--color-bg-secondary)',
|
||||
}}
|
||||
className={`rounded-[2px] md:rounded-[3px] ${item.listens > 0 ? '' : 'border-[0.5px] border-(--color-bg-tertiary)'}`}
|
||||
></div>
|
||||
</Popup>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export default function Footer() {
|
|||
return (
|
||||
<div className="mx-auto py-10 pt-20 color-fg-tertiary text-sm">
|
||||
<ul className="flex flex-col items-center w-sm justify-around">
|
||||
<li>Koito {pkg.version}</li>
|
||||
<li>Koito {import.meta.env.VITE_KOITO_VERSION || pkg.version}</li>
|
||||
<li><a href="https://github.com/gabehf/koito" target="_blank" className="link-underline">View the source on GitHub <ExternalLinkIcon className='inline mb-1' size={14}/></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function LastPlays(props: Props) {
|
|||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="w-[500px]">
|
||||
<div className="w-[400px] sm:w-[500px]">
|
||||
<h2>Last Played</h2>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
|
@ -43,8 +43,8 @@ export default function LastPlays(props: Props) {
|
|||
<tbody>
|
||||
{data.items.map((item) => (
|
||||
<tr key={`last_listen_${item.time}`}>
|
||||
<td className="color-fg-tertiary pr-4 text-sm" title={new Date(item.time).toString()}>{timeSince(new Date(item.time))}</td>
|
||||
<td className="text-ellipsis overflow-hidden max-w-[600px]">
|
||||
<td className="color-fg-tertiary pr-2 sm:pr-4 text-sm whitespace-nowrap w-0" title={new Date(item.time).toString()}>{timeSince(new Date(item.time))}</td>
|
||||
<td className="text-ellipsis overflow-hidden max-w-[400px] sm:max-w-[600px]">
|
||||
{props.hideArtists ? <></> : <><ArtistLinks artists={item.track.artists} /> - </>}
|
||||
<Link className="hover:text-(--color-fg-secondary)" to={`/track/${item.track.id}`}>{item.track.title}</Link>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ export default function Account() {
|
|||
setLoading(false)
|
||||
}
|
||||
const updateHandler = () => {
|
||||
setError('')
|
||||
setSuccess('')
|
||||
if (password != "" && confirmPw === "") {
|
||||
setError("confirm your password before submitting")
|
||||
setError("confirm your new password before submitting")
|
||||
return
|
||||
}
|
||||
setError('')
|
||||
|
|
@ -58,37 +60,44 @@ export default function Account() {
|
|||
<AsyncButton loading={loading} onClick={logoutHandler}>Logout</AsyncButton>
|
||||
</div>
|
||||
<h2>Update User</h2>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-username"
|
||||
type="text"
|
||||
placeholder="Update username"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-password"
|
||||
type="password"
|
||||
placeholder="Update password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
name="koito-confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={confirmPw}
|
||||
onChange={(e) => setConfirmPw(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-sm">
|
||||
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
|
||||
</div>
|
||||
<form action="#" onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4">
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-username"
|
||||
type="text"
|
||||
placeholder="Update username"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-sm">
|
||||
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
|
||||
</div>
|
||||
</form>
|
||||
<form action="#" onSubmit={(e) => e.preventDefault()} className="flex flex-col gap-4">
|
||||
<div className="flex flex gap-4">
|
||||
<input
|
||||
name="koito-update-password"
|
||||
type="password"
|
||||
placeholder="Update password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
name="koito-confirm-password"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
className="w-full mx-auto fg bg rounded p-2"
|
||||
value={confirmPw}
|
||||
onChange={(e) => setConfirmPw(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-sm">
|
||||
<AsyncButton loading={loading} onClick={updateHandler}>Submit</AsyncButton>
|
||||
</div>
|
||||
</form>
|
||||
{success != "" && <p className="success">{success}</p>}
|
||||
{error != "" && <p className="error">{error}</p>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,16 +16,25 @@ export default function SettingsModal({ open, setOpen } : Props) {
|
|||
const { user } = useAppContext()
|
||||
|
||||
const triggerClasses = "px-4 py-2 w-full hover-bg-secondary rounded-md text-start data-[state=active]:bg-[var(--color-bg-secondary)]"
|
||||
const contentClasses = "w-full px-10 overflow-y-auto"
|
||||
const contentClasses = "w-full px-2 mt-8 sm:mt-0 sm:px-10 overflow-y-auto"
|
||||
|
||||
return (
|
||||
<Modal h={600} isOpen={open} onClose={() => setOpen(false)} maxW={900}>
|
||||
<Tabs defaultValue="Appearance" orientation="vertical" className="flex justify-between h-full">
|
||||
<TabsList className="w-full flex flex-col gap-1 items-start max-w-1/4 rounded-md bg p-2 grow-0">
|
||||
<Tabs
|
||||
defaultValue="Appearance"
|
||||
orientation="vertical" // still vertical, but layout is responsive via Tailwind
|
||||
className="flex flex-col sm:flex-row h-full"
|
||||
>
|
||||
<TabsList className="flex flex-row sm:flex-col gap-1 w-full sm:max-w-1/4 rounded-md bg p-2">
|
||||
<TabsTrigger className={triggerClasses} value="Appearance">Appearance</TabsTrigger>
|
||||
<TabsTrigger className={triggerClasses} value="Account">Account</TabsTrigger>
|
||||
{ user && <TabsTrigger className={triggerClasses} value="API Keys">API Keys</TabsTrigger>}
|
||||
{user && (
|
||||
<TabsTrigger className={triggerClasses} value="API Keys">
|
||||
API Keys
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="Account" className={contentClasses}>
|
||||
<AccountPage />
|
||||
</TabsContent>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,36 @@
|
|||
import { ExternalLink, Home, Info } from "lucide-react";
|
||||
import { ExternalLink, Home, Info } from "lucide-react";
|
||||
import SidebarSearch from "./SidebarSearch";
|
||||
import SidebarItem from "./SidebarItem";
|
||||
import SidebarSettings from "./SidebarSettings";
|
||||
|
||||
export default function Sidebar() {
|
||||
|
||||
const iconSize = 20;
|
||||
|
||||
return (
|
||||
<div className="z-50 flex flex-col justify-between h-screen border-r-1 border-(--color-bg-tertiary) p-1 py-10 sticky left-0 top-0 bg-(--color-bg)">
|
||||
<div className="flex flex-col gap-4">
|
||||
<SidebarItem space={10} to="/" name="Home" onClick={() => {}} modal={<></>}><Home size={iconSize} /></SidebarItem>
|
||||
<SidebarSearch size={iconSize} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<SidebarItem icon keyHint={<ExternalLink size={14} />} space={22} externalLink to="https://koito.io" name="About" onClick={() => {}} modal={<></>}><Info size={iconSize} /></SidebarItem>
|
||||
<SidebarSettings size={iconSize} />
|
||||
<div className="overflow-x-hidden w-full sm:w-auto">
|
||||
<div className="z-50 flex sm:flex-col justify-between sm:h-screen h-auto sm:w-auto w-full border-b sm:border-b-0 sm:border-r border-(--color-bg-tertiary) pt-2 sm:py-10 sm:px-1 px-4 sticky top-0 sm:left-0 bg-(--color-bg)">
|
||||
<div className="flex gap-4 sm:flex-col">
|
||||
<SidebarItem space={10} to="/" name="Home" onClick={() => {}} modal={<></>}>
|
||||
<Home size={iconSize} />
|
||||
</SidebarItem>
|
||||
<SidebarSearch size={iconSize} />
|
||||
</div>
|
||||
<div className="flex gap-4 sm:flex-col">
|
||||
<SidebarItem
|
||||
icon
|
||||
keyHint={<ExternalLink size={14} />}
|
||||
space={22}
|
||||
externalLink
|
||||
to="https://koito.io"
|
||||
name="About"
|
||||
onClick={() => {}}
|
||||
modal={<></>}
|
||||
>
|
||||
<Info size={iconSize} />
|
||||
</SidebarItem>
|
||||
<SidebarSettings size={iconSize} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export default function ThemeOption({ theme, setTheme }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div onClick={() => setTheme(theme.name)} className="rounded-md p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
|
||||
{capitalizeFirstLetter(theme.name)}
|
||||
<div onClick={() => setTheme(theme.name)} className="rounded-md p-3 sm:p-5 hover:cursor-pointer flex gap-4 items-center border-2" style={{background: theme.bg, color: theme.fg, borderColor: theme.bgSecondary}}>
|
||||
<div className="text-xs sm:text-sm">{capitalizeFirstLetter(theme.name)}</div>
|
||||
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.bgSecondary}}></div>
|
||||
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.fgSecondary}}></div>
|
||||
<div className="w-[50px] h-[30px] rounded-md" style={{background: theme.primary}}></div>
|
||||
|
|
|
|||
|
|
@ -65,12 +65,12 @@ export default function App() {
|
|||
<AppProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="flex">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col items-center mx-auto w-full">
|
||||
<Outlet />
|
||||
<Footer />
|
||||
</div>
|
||||
<div className="flex-col flex sm:flex-row">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col items-center mx-auto w-full">
|
||||
<Outlet />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
|
|
@ -117,7 +117,7 @@ export function ErrorBoundary() {
|
|||
<div className="w-full flex flex-col">
|
||||
<main className="pt-16 p-4 container mx-auto flex-grow">
|
||||
<div className="flex gap-4 items-end">
|
||||
<img className="w-[200px] rounded" src="../public/yuu.jpg" />
|
||||
<img className="w-[200px] rounded" src="../yuu.jpg" />
|
||||
<div>
|
||||
<h1>{message}</h1>
|
||||
<p>{details}</p>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function Home() {
|
|||
<ActivityGrid />
|
||||
</div>
|
||||
<PeriodSelector setter={setPeriod} current={period} />
|
||||
<div className="flex flex-wrap 2xl:gap-20 xl:gap-10 justify-around gap-5">
|
||||
<div className="flex flex-wrap 2xl:gap-20 xl:gap-10 justify-between mx-5 gap-5">
|
||||
<TopArtists period={period} limit={homeItems} />
|
||||
<TopAlbums period={period} limit={homeItems} />
|
||||
<TopTracks period={period} limit={homeItems} />
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export default function Album() {
|
|||
<div className="mt-10">
|
||||
<PeriodSelector setter={setPeriod} current={period} />
|
||||
</div>
|
||||
<div className="flex gap-20 mt-10">
|
||||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={30} albumId={album.id} />
|
||||
<TopTracks limit={12} period={period} albumId={album.id} />
|
||||
<ActivityGrid autoAdjust configurable albumId={album.id} />
|
||||
|
|
|
|||
|
|
@ -45,6 +45,13 @@ export default function MediaLayout(props: Props) {
|
|||
|
||||
const title = `${props.title} - Koito`
|
||||
|
||||
const mobileIconSize = 22
|
||||
const normalIconSize = 30
|
||||
|
||||
let vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
|
||||
|
||||
let iconSize = vw > 768 ? normalIconSize : mobileIconSize
|
||||
|
||||
return (
|
||||
<main
|
||||
className="w-full flex flex-col flex-grow"
|
||||
|
|
@ -61,19 +68,21 @@ export default function MediaLayout(props: Props) {
|
|||
content={title}
|
||||
/>
|
||||
<div className="w-19/20 mx-auto pt-12">
|
||||
<div className="flex gap-8 relative">
|
||||
<img style={{zIndex: 5}} src={imageUrl(props.img, "large")} alt={props.title} className="w-sm shadow-(--color-shadow) shadow-lg" />
|
||||
<div className="flex gap-8 flex-wrap relative">
|
||||
<div className="flex flex-col justify-around">
|
||||
<img style={{zIndex: 5}} src={imageUrl(props.img, "large")} alt={props.title} className="md:w-sm w-[220px] h-auto shadow-(--color-shadow) shadow-lg" />
|
||||
</div>
|
||||
<div className="flex flex-col items-start">
|
||||
<h3>{props.type}</h3>
|
||||
<h1>{props.title}</h1>
|
||||
{props.subContent}
|
||||
</div>
|
||||
{ user &&
|
||||
<div className="absolute right-1 flex gap-3 items-center">
|
||||
<button title="Rename Item" className="hover:cursor-pointer" onClick={() => setRenameModalOpen(true)}><Edit size={30} /></button>
|
||||
<button title="Replace Image" className="hover:cursor-pointer" onClick={() => setImageModalOpen(true)}><ImageIcon size={30} /></button>
|
||||
<button title="Merge Items" className="hover:cursor-pointer" onClick={() => setMergeModalOpen(true)}><Merge size={30} /></button>
|
||||
<button title="Delete Item" className="hover:cursor-pointer" onClick={() => setDeleteModalOpen(true)}><Trash size={30} /></button>
|
||||
<div className="absolute left-1 sm:right-1 sm:left-auto -top-9 sm:top-1 flex gap-3 items-center">
|
||||
<button title="Rename Item" className="hover:cursor-pointer" onClick={() => setRenameModalOpen(true)}><Edit size={iconSize} /></button>
|
||||
<button title="Replace Image" className="hover:cursor-pointer" onClick={() => setImageModalOpen(true)}><ImageIcon size={iconSize} /></button>
|
||||
<button title="Merge Items" className="hover:cursor-pointer" onClick={() => setMergeModalOpen(true)}><Merge size={iconSize} /></button>
|
||||
<button title="Delete Item" className="hover:cursor-pointer" onClick={() => setDeleteModalOpen(true)}><Trash size={iconSize} /></button>
|
||||
<RenameModal open={renameModalOpen} setOpen={setRenameModalOpen} type={props.type.toLowerCase()} id={props.id}/>
|
||||
<ImageReplaceModal open={imageModalOpen} setOpen={setImageModalOpen} id={props.imgItemId} musicbrainzId={props.musicbrainzId} type={props.type === "Track" ? "Album" : props.type} />
|
||||
<MergeModal currentTitle={props.title} mergeFunc={props.mergeFunc} mergeCleanerFunc={props.mergeCleanerFunc} type={props.type} currentId={props.id} open={mergeModalOpen} setOpen={setMergeModalOpen} />
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ export default function Track() {
|
|||
<div className="mt-10">
|
||||
<PeriodSelector setter={setPeriod} current={period} />
|
||||
</div>
|
||||
<div className="flex gap-20 mt-10">
|
||||
<LastPlays limit={20} trackId={track.id}/>
|
||||
<ActivityGrid trackId={track.id} configurable autoAdjust />
|
||||
<div className="flex flex-wrap gap-20 mt-10">
|
||||
<LastPlays limit={20} trackId={track.id}/>
|
||||
<ActivityGrid trackId={track.id} configurable autoAdjust />
|
||||
</div>
|
||||
</MediaLayout>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "koito",
|
||||
"version": "v0.0.1",
|
||||
"version": "dev",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default defineConfig({
|
|||
changeOrigin: true,
|
||||
},
|
||||
'/images': {
|
||||
target: 'http://192.168.0.153:4110',
|
||||
target: 'http://localhost:4110',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue