mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
Merge f042e041eb into 0ec7b458cc
This commit is contained in:
commit
568cbb83db
11 changed files with 63 additions and 9 deletions
|
|
@ -455,6 +455,7 @@ type ApiError = {
|
||||||
};
|
};
|
||||||
type Config = {
|
type Config = {
|
||||||
default_theme: string;
|
default_theme: string;
|
||||||
|
date_format: string;
|
||||||
};
|
};
|
||||||
type NowPlaying = {
|
type NowPlaying = {
|
||||||
currently_playing: boolean;
|
currently_playing: boolean;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import { useState } from "react";
|
||||||
import { useTheme } from "~/hooks/useTheme";
|
import { useTheme } from "~/hooks/useTheme";
|
||||||
import ActivityOptsSelector from "./ActivityOptsSelector";
|
import ActivityOptsSelector from "./ActivityOptsSelector";
|
||||||
import type { Theme } from "~/styles/themes.css";
|
import type { Theme } from "~/styles/themes.css";
|
||||||
|
import { useAppContext } from "~/providers/AppProvider";
|
||||||
|
import { formatDate } from "~/utils/utils";
|
||||||
|
|
||||||
function getPrimaryColor(theme: Theme): string {
|
function getPrimaryColor(theme: Theme): string {
|
||||||
const value = theme.primary;
|
const value = theme.primary;
|
||||||
|
|
@ -65,6 +67,7 @@ export default function ActivityGrid({
|
||||||
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const color = getPrimaryColor(theme);
|
const color = getPrimaryColor(theme);
|
||||||
|
const { dateFormat } = useAppContext();
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -165,7 +168,7 @@ export default function ActivityGrid({
|
||||||
position="top"
|
position="top"
|
||||||
space={12}
|
space={12}
|
||||||
extraClasses="left-2"
|
extraClasses="left-2"
|
||||||
inner={`${new Date(item.start_time).toLocaleDateString()} ${
|
inner={`${formatDate(new Date(item.start_time), dateFormat)} ${
|
||||||
item.listens
|
item.listens
|
||||||
} plays`}
|
} plays`}
|
||||||
>
|
>
|
||||||
|
|
@ -194,3 +197,4 @@ export default function ActivityGrid({
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ interface AppContextType {
|
||||||
configurableHomeActivity: boolean;
|
configurableHomeActivity: boolean;
|
||||||
homeItems: number;
|
homeItems: number;
|
||||||
defaultTheme: string;
|
defaultTheme: string;
|
||||||
|
dateFormat: string;
|
||||||
setConfigurableHomeActivity: (value: boolean) => void;
|
setConfigurableHomeActivity: (value: boolean) => void;
|
||||||
setHomeItems: (value: number) => void;
|
setHomeItems: (value: number) => void;
|
||||||
setUsername: (value: string) => void;
|
setUsername: (value: string) => void;
|
||||||
|
|
@ -26,6 +27,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(
|
const [defaultTheme, setDefaultTheme] = useState<string | undefined>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [dateFormat, setDateFormat] = useState<string>("");
|
||||||
const [configurableHomeActivity, setConfigurableHomeActivity] =
|
const [configurableHomeActivity, setConfigurableHomeActivity] =
|
||||||
useState<boolean>(false);
|
useState<boolean>(false);
|
||||||
const [homeItems, setHomeItems] = useState<number>(0);
|
const [homeItems, setHomeItems] = useState<number>(0);
|
||||||
|
|
@ -55,6 +57,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
} else {
|
} else {
|
||||||
setDefaultTheme("yuu");
|
setDefaultTheme("yuu");
|
||||||
}
|
}
|
||||||
|
setDateFormat(cfg.date_format ?? "");
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -68,6 +71,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
configurableHomeActivity,
|
configurableHomeActivity,
|
||||||
homeItems,
|
homeItems,
|
||||||
defaultTheme,
|
defaultTheme,
|
||||||
|
dateFormat,
|
||||||
setConfigurableHomeActivity,
|
setConfigurableHomeActivity,
|
||||||
setHomeItems,
|
setHomeItems,
|
||||||
setUsername,
|
setUsername,
|
||||||
|
|
@ -77,3 +81,4 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
<AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
|
<AppContext.Provider value={contextValue}>{children}</AppContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,9 @@ import LastPlays from "~/components/LastPlays";
|
||||||
import PeriodSelector from "~/components/PeriodSelector";
|
import PeriodSelector from "~/components/PeriodSelector";
|
||||||
import MediaLayout from "./MediaLayout";
|
import MediaLayout from "./MediaLayout";
|
||||||
import ActivityGrid from "~/components/ActivityGrid";
|
import ActivityGrid from "~/components/ActivityGrid";
|
||||||
import { timeListenedString } from "~/utils/utils";
|
import { timeListenedString, formatDate } from "~/utils/utils";
|
||||||
import InterestGraph from "~/components/InterestGraph";
|
import InterestGraph from "~/components/InterestGraph";
|
||||||
|
import { useAppContext } from "~/providers/AppProvider";
|
||||||
|
|
||||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||||
const res = await fetch(`/apis/web/v1/album?id=${params.id}`);
|
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() {
|
export default function Album() {
|
||||||
const album = useLoaderData() as Album;
|
const album = useLoaderData() as Album;
|
||||||
const [period, setPeriod] = useState("week");
|
const [period, setPeriod] = useState("week");
|
||||||
|
const { dateFormat } = useAppContext();
|
||||||
|
|
||||||
console.log(album);
|
console.log(album);
|
||||||
|
|
||||||
|
|
@ -59,7 +61,7 @@ export default function Album() {
|
||||||
{album.first_listen > 0 && (
|
{album.first_listen > 0 && (
|
||||||
<p title={new Date(album.first_listen * 1000).toLocaleString()}>
|
<p title={new Date(album.first_listen * 1000).toLocaleString()}>
|
||||||
Listening since{" "}
|
Listening since{" "}
|
||||||
{new Date(album.first_listen * 1000).toLocaleDateString()}
|
{formatDate(new Date(album.first_listen * 1000), dateFormat)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -79,3 +81,4 @@ export default function Album() {
|
||||||
</MediaLayout>
|
</MediaLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import PeriodSelector from "~/components/PeriodSelector";
|
||||||
import MediaLayout from "./MediaLayout";
|
import MediaLayout from "./MediaLayout";
|
||||||
import ArtistAlbums from "~/components/ArtistAlbums";
|
import ArtistAlbums from "~/components/ArtistAlbums";
|
||||||
import ActivityGrid from "~/components/ActivityGrid";
|
import ActivityGrid from "~/components/ActivityGrid";
|
||||||
import { timeListenedString } from "~/utils/utils";
|
import { timeListenedString, formatDate } from "~/utils/utils";
|
||||||
import InterestGraph from "~/components/InterestGraph";
|
import InterestGraph from "~/components/InterestGraph";
|
||||||
|
import { useAppContext } from "~/providers/AppProvider";
|
||||||
|
|
||||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||||
const res = await fetch(`/apis/web/v1/artist?id=${params.id}`);
|
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() {
|
export default function Artist() {
|
||||||
const artist = useLoaderData() as Artist;
|
const artist = useLoaderData() as Artist;
|
||||||
const [period, setPeriod] = useState("week");
|
const [period, setPeriod] = useState("week");
|
||||||
|
const { dateFormat } = useAppContext();
|
||||||
|
|
||||||
// remove canonical name from alias list
|
// remove canonical name from alias list
|
||||||
console.log(artist.aliases);
|
console.log(artist.aliases);
|
||||||
|
|
@ -65,7 +67,7 @@ export default function Artist() {
|
||||||
{artist.first_listen > 0 && (
|
{artist.first_listen > 0 && (
|
||||||
<p title={new Date(artist.first_listen * 1000).toLocaleString()}>
|
<p title={new Date(artist.first_listen * 1000).toLocaleString()}>
|
||||||
Listening since{" "}
|
Listening since{" "}
|
||||||
{new Date(artist.first_listen * 1000).toLocaleDateString()}
|
{formatDate(new Date(artist.first_listen * 1000), dateFormat)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,3 +90,4 @@ export default function Artist() {
|
||||||
</MediaLayout>
|
</MediaLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ import LastPlays from "~/components/LastPlays";
|
||||||
import PeriodSelector from "~/components/PeriodSelector";
|
import PeriodSelector from "~/components/PeriodSelector";
|
||||||
import MediaLayout from "./MediaLayout";
|
import MediaLayout from "./MediaLayout";
|
||||||
import ActivityGrid from "~/components/ActivityGrid";
|
import ActivityGrid from "~/components/ActivityGrid";
|
||||||
import { timeListenedString } from "~/utils/utils";
|
import { timeListenedString, formatDate } from "~/utils/utils";
|
||||||
import InterestGraph from "~/components/InterestGraph";
|
import InterestGraph from "~/components/InterestGraph";
|
||||||
|
import { useAppContext } from "~/providers/AppProvider";
|
||||||
|
|
||||||
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
export async function clientLoader({ params }: LoaderFunctionArgs) {
|
||||||
let res = await fetch(`/apis/web/v1/track?id=${params.id}`);
|
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() {
|
export default function Track() {
|
||||||
const { track, album } = useLoaderData();
|
const { track, album } = useLoaderData();
|
||||||
const [period, setPeriod] = useState("week");
|
const [period, setPeriod] = useState("week");
|
||||||
|
const { dateFormat } = useAppContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MediaLayout
|
<MediaLayout
|
||||||
|
|
@ -69,7 +71,7 @@ export default function Track() {
|
||||||
{track.first_listen > 0 && (
|
{track.first_listen > 0 && (
|
||||||
<p title={new Date(track.first_listen * 1000).toLocaleString()}>
|
<p title={new Date(track.first_listen * 1000).toLocaleString()}>
|
||||||
Listening since{" "}
|
Listening since{" "}
|
||||||
{new Date(track.first_listen * 1000).toLocaleDateString()}
|
{formatDate(new Date(track.first_listen * 1000), dateFormat)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -88,3 +90,4 @@ export default function Track() {
|
||||||
</MediaLayout>
|
</MediaLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,15 @@ function timeSince(date: Date) {
|
||||||
return "just now";
|
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 = {
|
type hsl = {
|
||||||
h: number;
|
h: number;
|
||||||
|
|
@ -119,3 +127,4 @@ const timeListenedString = (seconds: number) => {
|
||||||
|
|
||||||
export { hexToHSL, timeListenedString, getRewindYear, getRewindParams };
|
export { hexToHSL, timeListenedString, getRewindYear, getRewindParams };
|
||||||
export type { hsl };
|
export type { hsl };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ If the environment variable is defined without **and** with the suffix at the sa
|
||||||
##### KOITO_DEFAULT_THEME
|
##### KOITO_DEFAULT_THEME
|
||||||
- Default: `yuu`
|
- Default: `yuu`
|
||||||
- Description: The lowercase name of the default theme to be used by the client. Overridden if a user picks a theme in the theme switcher.
|
- Description: The lowercase name of the default theme to be used by the client. Overridden if a user picks a theme in the theme switcher.
|
||||||
|
##### KOITO_DATE_FORMAT
|
||||||
|
- Default: `Browser locale`
|
||||||
|
- Description: The format to use for dates in the client. Supports the tokens `DD`, `MM`, and `YYYY` with `/`, `-`, and `.` as separators. For example, `DD/MM/YYYY` produces `21/04/2026`. When not set, dates are formatted using the browser's locale.
|
||||||
##### KOITO_LOGIN_GATE
|
##### KOITO_LOGIN_GATE
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Description: When `true`, Koito will not show any statistics unless the user is logged in.
|
- Description: When `true`, Koito will not show any statistics unless the user is logged in.
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,15 @@ import (
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
DefaultTheme string `json:"default_theme"`
|
DefaultTheme string `json:"default_theme"`
|
||||||
|
DateFormat string `json:"date_format"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCfgHandler() http.HandlerFunc {
|
func GetCfgHandler() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ const (
|
||||||
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
DEFAULT_USERNAME_ENV = "KOITO_DEFAULT_USERNAME"
|
||||||
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
DEFAULT_PASSWORD_ENV = "KOITO_DEFAULT_PASSWORD"
|
||||||
DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME"
|
DEFAULT_THEME_ENV = "KOITO_DEFAULT_THEME"
|
||||||
|
DATE_FORMAT_ENV = "KOITO_DATE_FORMAT"
|
||||||
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
DISABLE_DEEZER_ENV = "KOITO_DISABLE_DEEZER"
|
||||||
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
DISABLE_COVER_ART_ARCHIVE_ENV = "KOITO_DISABLE_COVER_ART_ARCHIVE"
|
||||||
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
||||||
|
|
@ -69,6 +70,7 @@ type config struct {
|
||||||
defaultPw string
|
defaultPw string
|
||||||
defaultUsername string
|
defaultUsername string
|
||||||
defaultTheme string
|
defaultTheme string
|
||||||
|
dateFormat string
|
||||||
disableDeezer bool
|
disableDeezer bool
|
||||||
disableCAA bool
|
disableCAA bool
|
||||||
disableMusicBrainz bool
|
disableMusicBrainz bool
|
||||||
|
|
@ -186,6 +188,14 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.defaultTheme = getenv(DEFAULT_THEME_ENV)
|
cfg.defaultTheme = getenv(DEFAULT_THEME_ENV)
|
||||||
|
rawDateFormat := getenv(DATE_FORMAT_ENV)
|
||||||
|
if rawDateFormat != "" {
|
||||||
|
validFormat := regexp.MustCompile(`^(DD|MM|YYYY)([-/.](DD|MM|YYYY)){2}$`)
|
||||||
|
if !validFormat.MatchString(rawDateFormat) {
|
||||||
|
return nil, fmt.Errorf("loadConfig: %s must use DD, MM, and YYYY tokens with a single / - or . separator (e.g. DD/MM/YYYY)", DATE_FORMAT_ENV)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.dateFormat = rawDateFormat
|
||||||
|
|
||||||
cfg.configDir = getenv(CONFIG_DIR_ENV)
|
cfg.configDir = getenv(CONFIG_DIR_ENV)
|
||||||
if cfg.configDir == "" {
|
if cfg.configDir == "" {
|
||||||
|
|
@ -244,3 +254,4 @@ func parseBool(s string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,12 @@ func DefaultTheme() string {
|
||||||
return globalConfig.defaultTheme
|
return globalConfig.defaultTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DateFormat() string {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
return globalConfig.dateFormat
|
||||||
|
}
|
||||||
|
|
||||||
func FullImageCacheEnabled() bool {
|
func FullImageCacheEnabled() bool {
|
||||||
lock.RLock()
|
lock.RLock()
|
||||||
defer lock.RUnlock()
|
defer lock.RUnlock()
|
||||||
|
|
@ -204,3 +210,4 @@ func ForceTZ() *time.Location {
|
||||||
defer lock.RUnlock()
|
defer lock.RUnlock()
|
||||||
return globalConfig.forceTZ
|
return globalConfig.forceTZ
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue