From 61d5f2f5f09ba53e3cfa2d168c5f4a851d263fd6 Mon Sep 17 00:00:00 2001 From: joffrey-b Date: Tue, 21 Apr 2026 11:17:47 +0200 Subject: [PATCH] Added KOITO_DATE_FORMAT option to choose how the dates are displayed --- client/api/api.ts | 1 + client/app/components/ActivityGrid.tsx | 6 +++++- client/app/providers/AppProvider.tsx | 5 +++++ client/app/routes/MediaItems/Album.tsx | 7 +++++-- client/app/routes/MediaItems/Artist.tsx | 7 +++++-- client/app/routes/MediaItems/Track.tsx | 7 +++++-- client/app/utils/utils.ts | 11 ++++++++++- engine/handlers/server_cfg.go | 7 ++++++- internal/cfg/cfg.go | 4 ++++ internal/cfg/getters.go | 7 +++++++ 10 files changed, 53 insertions(+), 9 deletions(-) diff --git a/client/api/api.ts b/client/api/api.ts index bd2430b..836dbf7 100644 --- a/client/api/api.ts +++ b/client/api/api.ts @@ -455,6 +455,7 @@ type ApiError = { }; type Config = { default_theme: string; + date_format: string; }; type NowPlaying = { currently_playing: boolean; diff --git a/client/app/components/ActivityGrid.tsx b/client/app/components/ActivityGrid.tsx index 0d39e2c..3562a67 100644 --- a/client/app/components/ActivityGrid.tsx +++ b/client/app/components/ActivityGrid.tsx @@ -9,6 +9,8 @@ import { useState } from "react"; import { useTheme } from "~/hooks/useTheme"; import ActivityOptsSelector from "./ActivityOptsSelector"; import type { Theme } from "~/styles/themes.css"; +import { useAppContext } from "~/providers/AppProvider"; +import { formatDate } from "~/utils/utils"; function getPrimaryColor(theme: Theme): string { const value = theme.primary; @@ -65,6 +67,7 @@ export default function ActivityGrid({ const { theme } = useTheme(); const color = getPrimaryColor(theme); + const { dateFormat } = useAppContext(); if (isPending) { return ( @@ -165,7 +168,7 @@ export default function ActivityGrid({ position="top" space={12} extraClasses="left-2" - inner={`${new Date(item.start_time).toLocaleDateString()} ${ + inner={`${formatDate(new Date(item.start_time), dateFormat)} ${ item.listens } plays`} > @@ -194,3 +197,4 @@ export default function ActivityGrid({ ); } + diff --git a/client/app/providers/AppProvider.tsx b/client/app/providers/AppProvider.tsx index 4b8290d..c9e3a86 100644 --- a/client/app/providers/AppProvider.tsx +++ b/client/app/providers/AppProvider.tsx @@ -6,6 +6,7 @@ interface AppContextType { configurableHomeActivity: boolean; homeItems: number; defaultTheme: string; + dateFormat: string; setConfigurableHomeActivity: (value: boolean) => void; setHomeItems: (value: number) => void; setUsername: (value: string) => void; @@ -26,6 +27,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { const [defaultTheme, setDefaultTheme] = useState( undefined ); + const [dateFormat, setDateFormat] = useState(""); const [configurableHomeActivity, setConfigurableHomeActivity] = useState(false); const [homeItems, setHomeItems] = useState(0); @@ -55,6 +57,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { } else { setDefaultTheme("yuu"); } + setDateFormat(cfg.date_format ?? ""); }); }, []); @@ -68,6 +71,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { configurableHomeActivity, homeItems, defaultTheme, + dateFormat, setConfigurableHomeActivity, setHomeItems, setUsername, @@ -77,3 +81,4 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => { {children} ); }; + diff --git a/client/app/routes/MediaItems/Album.tsx b/client/app/routes/MediaItems/Album.tsx index e6f413e..765c7aa 100644 --- a/client/app/routes/MediaItems/Album.tsx +++ b/client/app/routes/MediaItems/Album.tsx @@ -6,8 +6,9 @@ import LastPlays from "~/components/LastPlays"; import PeriodSelector from "~/components/PeriodSelector"; import MediaLayout from "./MediaLayout"; import ActivityGrid from "~/components/ActivityGrid"; -import { timeListenedString } from "~/utils/utils"; +import { timeListenedString, formatDate } from "~/utils/utils"; import InterestGraph from "~/components/InterestGraph"; +import { useAppContext } from "~/providers/AppProvider"; export async function clientLoader({ params }: LoaderFunctionArgs) { const res = await fetch(`/apis/web/v1/album?id=${params.id}`); @@ -21,6 +22,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { export default function Album() { const album = useLoaderData() as Album; const [period, setPeriod] = useState("week"); + const { dateFormat } = useAppContext(); console.log(album); @@ -59,7 +61,7 @@ export default function Album() { {album.first_listen > 0 && (

Listening since{" "} - {new Date(album.first_listen * 1000).toLocaleDateString()} + {formatDate(new Date(album.first_listen * 1000), dateFormat)}

)} @@ -79,3 +81,4 @@ export default function Album() { ); } + diff --git a/client/app/routes/MediaItems/Artist.tsx b/client/app/routes/MediaItems/Artist.tsx index a23e4cd..4b73a45 100644 --- a/client/app/routes/MediaItems/Artist.tsx +++ b/client/app/routes/MediaItems/Artist.tsx @@ -7,8 +7,9 @@ import PeriodSelector from "~/components/PeriodSelector"; import MediaLayout from "./MediaLayout"; import ArtistAlbums from "~/components/ArtistAlbums"; import ActivityGrid from "~/components/ActivityGrid"; -import { timeListenedString } from "~/utils/utils"; +import { timeListenedString, formatDate } from "~/utils/utils"; import InterestGraph from "~/components/InterestGraph"; +import { useAppContext } from "~/providers/AppProvider"; export async function clientLoader({ params }: LoaderFunctionArgs) { const res = await fetch(`/apis/web/v1/artist?id=${params.id}`); @@ -22,6 +23,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { export default function Artist() { const artist = useLoaderData() as Artist; const [period, setPeriod] = useState("week"); + const { dateFormat } = useAppContext(); // remove canonical name from alias list console.log(artist.aliases); @@ -65,7 +67,7 @@ export default function Artist() { {artist.first_listen > 0 && (

Listening since{" "} - {new Date(artist.first_listen * 1000).toLocaleDateString()} + {formatDate(new Date(artist.first_listen * 1000), dateFormat)}

)} @@ -88,3 +90,4 @@ export default function Artist() { ); } + diff --git a/client/app/routes/MediaItems/Track.tsx b/client/app/routes/MediaItems/Track.tsx index 6b6690e..0155118 100644 --- a/client/app/routes/MediaItems/Track.tsx +++ b/client/app/routes/MediaItems/Track.tsx @@ -5,8 +5,9 @@ import LastPlays from "~/components/LastPlays"; import PeriodSelector from "~/components/PeriodSelector"; import MediaLayout from "./MediaLayout"; import ActivityGrid from "~/components/ActivityGrid"; -import { timeListenedString } from "~/utils/utils"; +import { timeListenedString, formatDate } from "~/utils/utils"; import InterestGraph from "~/components/InterestGraph"; +import { useAppContext } from "~/providers/AppProvider"; export async function clientLoader({ params }: LoaderFunctionArgs) { let res = await fetch(`/apis/web/v1/track?id=${params.id}`); @@ -27,6 +28,7 @@ export async function clientLoader({ params }: LoaderFunctionArgs) { export default function Track() { const { track, album } = useLoaderData(); const [period, setPeriod] = useState("week"); + const { dateFormat } = useAppContext(); return ( 0 && (

Listening since{" "} - {new Date(track.first_listen * 1000).toLocaleDateString()} + {formatDate(new Date(track.first_listen * 1000), dateFormat)}

)} @@ -88,3 +90,4 @@ export default function Track() {
); } + diff --git a/client/app/utils/utils.ts b/client/app/utils/utils.ts index 4acbad5..312509b 100644 --- a/client/app/utils/utils.ts +++ b/client/app/utils/utils.ts @@ -52,7 +52,15 @@ function timeSince(date: Date) { return "just now"; } -export { timeSince }; +function formatDate(date: Date, format: string): string { + if (!format) return date.toLocaleDateString(); + const dd = String(date.getDate()).padStart(2, "0"); + const mm = String(date.getMonth() + 1).padStart(2, "0"); + const yyyy = String(date.getFullYear()); + return format.replace("DD", dd).replace("MM", mm).replace("YYYY", yyyy); +} + +export { timeSince, formatDate }; type hsl = { h: number; @@ -119,3 +127,4 @@ const timeListenedString = (seconds: number) => { export { hexToHSL, timeListenedString, getRewindYear, getRewindParams }; export type { hsl }; + diff --git a/engine/handlers/server_cfg.go b/engine/handlers/server_cfg.go index ebc7b9f..9b3f625 100644 --- a/engine/handlers/server_cfg.go +++ b/engine/handlers/server_cfg.go @@ -9,10 +9,15 @@ import ( type ServerConfig struct { DefaultTheme string `json:"default_theme"` + DateFormat string `json:"date_format"` } func GetCfgHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - utils.WriteJSON(w, http.StatusOK, ServerConfig{DefaultTheme: cfg.DefaultTheme()}) + utils.WriteJSON(w, http.StatusOK, ServerConfig{ + DefaultTheme: cfg.DefaultTheme(), + DateFormat: cfg.DateFormat(), + }) } } + diff --git a/internal/cfg/cfg.go b/internal/cfg/cfg.go index 0cfc7bb..ced0770 100644 --- a/internal/cfg/cfg.go +++ b/internal/cfg/cfg.go @@ -33,6 +33,7 @@ const ( DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME" DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD" DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME" + DATE_FORMAT_ENV = "KOITO_DATE_FORMAT" DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER" DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE" DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ" @@ -69,6 +70,7 @@ type config struct { defaultPw string defaultUsername string defaultTheme string + dateFormat string disableDeezer bool disableCAA bool disableMusicBrainz bool @@ -186,6 +188,7 @@ func loadConfig(getenv func(string) string, version string) (*config, error) { } cfg.defaultTheme = getenv(DEFAULT_THEME_ENV) + cfg.dateFormat = getenv(DATE_FORMAT_ENV) cfg.configDir = getenv(CONFIG_DIR_ENV) if cfg.configDir == "" { @@ -244,3 +247,4 @@ func parseBool(s string) bool { return false } } + diff --git a/internal/cfg/getters.go b/internal/cfg/getters.go index 596ca9d..8b353eb 100644 --- a/internal/cfg/getters.go +++ b/internal/cfg/getters.go @@ -90,6 +90,12 @@ func DefaultTheme() string { return globalConfig.defaultTheme } +func DateFormat() string { + lock.RLock() + defer lock.RUnlock() + return globalConfig.dateFormat +} + func FullImageCacheEnabled() bool { lock.RLock() defer lock.RUnlock() @@ -204,3 +210,4 @@ func ForceTZ() *time.Location { defer lock.RUnlock() return globalConfig.forceTZ } +