Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement new settings option to show played time in hours (30h 26m) or in days (1d 6h 26m) #10

Merged
merged 8 commits into from
Jan 8, 2025
9 changes: 0 additions & 9 deletions jest.config.js

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "PlayTime",
"version": "2.0.7",
"version": "2.0.8",
"description": "PlayTime",
"type": "module",
"scripts": {
Expand Down
116 changes: 88 additions & 28 deletions src/app/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,123 @@ import { type Duration, intervalToDuration } from "date-fns";
import type { Interval } from "./reports";

export {
humanReadableTime,
toIsoDateOnly,
formatMonthInterval,
formatWeekInterval,
getDurationInDays,
getDurationInHours,
humanReadableTime,
toIsoDateOnly,
};

function humanReadableTime(
seconds: number,
function reduceDuration(
accumulator: Array<string>,
key: string,
duration: Duration,
withSeconds: boolean,
short: boolean,
) {
if (
key === "seconds" &&
!withSeconds &&
// NOTE(ynhhoJ): If `seconds` key is only one in object then we should display it
Object.keys(duration).length !== 1
) {
return accumulator;
}

const durationValue = duration[key as keyof Duration];

if (isNil(durationValue)) {
return accumulator;
}

if (short) {
accumulator.push(`${durationValue}${key[0]}`);

return accumulator;
}

const durationTime = durationValue === 1 ? key.slice(0, key.length - 1) : key;

accumulator.push(`${durationValue} ${durationTime}`);

return accumulator;
}

function getDurationInDays(
durationInSeconds: number,
short = true,
withSeconds = false,
): string {
let duration: Duration = {};

if (seconds === 0) {
if (durationInSeconds === 0) {
duration = {
seconds: 0,
};
} else {
duration = intervalToDuration({
start: new Date(),
end: new Date().getTime() + seconds * 1000,
end: new Date().getTime() + durationInSeconds * 1000,
});
}

return Object.keys(duration)
.reduce<Array<string>>((accumulator, key) => {
if (
key === "seconds" &&
!withSeconds &&
// NOTE(ynhhoJ): If `seconds` key is only one in object then we should display it
Object.keys(duration).length !== 1
) {
return accumulator;
}
.reduce<Array<string>>(
(accumulator, key) =>
reduceDuration(accumulator, key, duration, withSeconds, short),
[],
)
.join(" ");
}

const durationValue = duration[key as keyof Duration];
function getDurationInHours(
durationInSeconds: number,
short = true,
withSeconds = false,
): string {
const duration: Duration = {};

if (isNil(durationValue)) {
return accumulator;
}
const hours = Math.floor(durationInSeconds / 3600);

if (short) {
accumulator.push(`${durationValue}${key[0]}`);
if (hours !== 0) {
duration.hours = hours;
}

const minutes = Math.floor((durationInSeconds % 3600) / 60);

return accumulator;
}
if (minutes !== 0) {
duration.minutes = minutes;
}

const durationTime =
durationValue === 1 ? key.slice(0, key.length - 1) : key;
const seconds = Math.floor((durationInSeconds % 3600) % 60);

accumulator.push(`${durationValue} ${durationTime}`);
if ((withSeconds && seconds !== 0) || Object.keys(duration).length === 0) {
duration.seconds = seconds;
}

return accumulator;
}, [])
return Object.keys(duration)
.reduce<Array<string>>(
(accumulator, key) =>
reduceDuration(accumulator, key, duration, withSeconds, short),
[],
)
.join(" ");
}

function humanReadableTime(
showTimeInHours: boolean,
durationInSeconds: number,
short = true,
withSeconds = false,
) {
if (showTimeInHours) {
return getDurationInHours(durationInSeconds, short, withSeconds);
}

return getDurationInDays(durationInSeconds, short, withSeconds);
}

function toIsoDateOnly(date: Date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
Expand Down
80 changes: 68 additions & 12 deletions src/app/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import logger from "../utils";
export interface PlayTimeSettings {
gameChartStyle: ChartStyle;
reminderToTakeBreaksInterval: number;
displayTime: {
showSeconds: boolean;
/**
* When `false` time will be shown as `2d 2h`
* when `true` time will be shown as `50h` (`48h` + `2h`)
*/
showTimeInHours: boolean;
};
}

export enum ChartStyle {
Expand All @@ -12,36 +20,84 @@ export enum ChartStyle {
}

const PLAY_TIME_SETTINGS_KEY = "decky-loader-SDH-Playtime";

export const DEFAULTS: PlayTimeSettings = {
gameChartStyle: ChartStyle.BAR,
reminderToTakeBreaksInterval: -1,
displayTime: {
showTimeInHours: true,
showSeconds: false,
},
};

export class Settings {
constructor() {
SteamClient.Storage.GetJSON(PLAY_TIME_SETTINGS_KEY).catch((e: Error) => {
if (e.message === "Not found") {
logger.error("Unable to get settings, saving defaults", e);
SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, DEFAULTS);
} else {
SteamClient.Storage.GetJSON(PLAY_TIME_SETTINGS_KEY)
.then(async (json) => {
const parsedJson = JSON.parse(json) as PlayTimeSettings;

this.setDefaultDisplayTimeIfNeeded(parsedJson);
})
.catch((e: Error) => {
if (e.message === "Not found") {
logger.error("Unable to get settings, saving defaults", e);

SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, DEFAULTS);

return;
}

logger.error("Unable to get settings", e);
}
});
});
}

async get(): Promise<PlayTimeSettings> {
const settings = await SteamClient.Storage.GetJSON<string>(
PLAY_TIME_SETTINGS_KEY,
);
const settings = await SteamClient.Storage.GetJSON(PLAY_TIME_SETTINGS_KEY);

if (isNil(settings)) {
return DEFAULTS;
}

return JSON.parse(settings);
let data = JSON.parse(settings);

data = {
...data,
displayTime: {
showTimeInHours: !!data.displayTime.showTimeInHours,
showSeconds: !!data.displayTime.showSeconds,
},
};

return data;
}

async save(data: PlayTimeSettings) {
await SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, data);
await SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, {
...data,
displayTime: {
showTimeInHours: +data.displayTime.showTimeInHours,
showSeconds: +data.displayTime.showSeconds,
},
});
}

private async setDefaultDisplayTimeIfNeeded(settings: PlayTimeSettings) {
// NOTE(ynhhoJ): If fore some reason `settings` is `null` or `undefined` we should set it
if (isNil(settings)) {
SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, DEFAULTS);

return;
}

const { displayTime } = settings;

if (!isNil(displayTime)) {
return;
}

await SteamClient.Storage.SetObject(PLAY_TIME_SETTINGS_KEY, {
...settings,
displayTime: DEFAULTS.displayTime,
});
}
}
10 changes: 9 additions & 1 deletion src/components/Timebar.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useLocator } from "@src/locator";
import { humanReadableTime } from "../app/formatters";
import { TimeBarCSS } from "../styles";
import { VerticalContainer } from "./VerticalContainer";

export const Timebar: React.FC<{ time: number; allTime: number }> = (props) => {
const { currentSettings: settings } = useLocator();

const barWidth =
props.allTime !== 0 ? `${(props.time / props.allTime) * 100}%` : "0%";

Expand All @@ -17,7 +20,12 @@ export const Timebar: React.FC<{ time: number; allTime: number }> = (props) => {
/>
</div>
<div style={TimeBarCSS.time_bar__time_text}>
{humanReadableTime(props.time, true)}
{humanReadableTime(
settings.displayTime.showTimeInHours,
props.time,
true,
settings.displayTime.showSeconds,
)}
</div>
</VerticalContainer>
);
Expand Down
16 changes: 14 additions & 2 deletions src/components/statistics/AverageAndOverall.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Field, PanelSection, PanelSectionRow } from "@decky/ui";
import { useLocator } from "@src/locator";
import moment from "moment";
import type { FC } from "react";
import { humanReadableTime } from "../../app/formatters";
Expand All @@ -8,6 +9,7 @@ import { FocusableExt } from "../FocusableExt";
export const AverageAndOverall: FC<{ statistics: DailyStatistics[] }> = (
props,
) => {
const { currentSettings: settings } = useLocator();
const overall = props.statistics
.map((it) => it.total)
.reduce((a, c) => a + c, 0);
Expand All @@ -22,13 +24,23 @@ export const AverageAndOverall: FC<{ statistics: DailyStatistics[] }> = (
<PanelSection title="Average and overall">
<PanelSectionRow>
<Field label="Daily average" bottomSeparator="none">
{humanReadableTime(average)}
{humanReadableTime(
settings.displayTime.showTimeInHours,
average,
true,
settings.displayTime.showSeconds,
)}
</Field>
</PanelSectionRow>

<PanelSectionRow>
<Field label="Overall" bottomSeparator="none">
{humanReadableTime(overall)}
{humanReadableTime(
settings.displayTime.showTimeInHours,
overall,
true,
settings.displayTime.showSeconds,
)}
</Field>
</PanelSectionRow>
</PanelSection>
Expand Down
10 changes: 9 additions & 1 deletion src/components/statistics/MonthView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useLocator } from "@src/locator";
import moment from "moment";
import type { FC } from "react";
import {
Expand All @@ -18,6 +19,7 @@ interface DayTime {
}

export const MonthView: FC<{ statistics: DailyStatistics[] }> = (props) => {
const { currentSettings: settings } = useLocator();
const dayTimes = props.statistics.map((it) => {
return {
time: it.total,
Expand Down Expand Up @@ -57,7 +59,13 @@ export const MonthView: FC<{ statistics: DailyStatistics[] }> = (props) => {
<YAxis
axisLine={false}
domain={["auto", (dataMax: number) => dataMax * 1.05]}
tickFormatter={(e: number) => humanReadableTime(e, true)}
tickFormatter={(e: number) =>
humanReadableTime(
settings.displayTime.showTimeInHours,
e,
true,
)
}
width={75}
/>
<Bar dataKey="time" fill="#008ADA" />
Expand Down
Loading