Skip to content

Commit

Permalink
Merge pull request #10 from 0u73r-h34v3n/features/better-time-display
Browse files Browse the repository at this point in the history
Implement new settings option to show played time in hours (`30h 26m`) or in days (`1d 6h 26m`)
  • Loading branch information
ynhhoJ authored Jan 8, 2025
2 parents 7361c2c + d27ae54 commit 05fcee6
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 81 deletions.
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

0 comments on commit 05fcee6

Please sign in to comment.