Skip to content

Commit bccbf61

Browse files
committed
feat(cards): show source sort values on covers
1 parent 04d2a1d commit bccbf61

14 files changed

Lines changed: 236 additions & 53 deletions

File tree

src-tauri/src/database/repository/game_stats_repository.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ use serde::{Deserialize, Serialize};
55

66
/// 每日统计数据结构
77
#[derive(Debug, Clone, Serialize, Deserialize)]
8-
pub struct DailyStats {
9-
pub date: String,
10-
pub playtime: i32,
11-
}
12-
13-
/// 游戏统计仓库
14-
pub struct GameStatsRepository;
8+
pub struct DailyStats {
9+
pub date: String,
10+
pub playtime: i32,
11+
}
12+
13+
#[derive(Debug, Clone, Serialize, FromQueryResult)]
14+
pub struct GameLastPlayed {
15+
pub game_id: i32,
16+
pub last_played: Option<i32>,
17+
}
18+
19+
/// 游戏统计仓库
20+
pub struct GameStatsRepository;
1521

1622
impl GameStatsRepository {
1723
// ==================== 游戏会话操作 ====================
@@ -191,13 +197,26 @@ impl GameStatsRepository {
191197
}
192198

193199
/// 获取所有游戏统计数据
194-
pub async fn get_all_statistics(
195-
db: &DatabaseConnection,
196-
) -> Result<Vec<game_statistics::Model>, DbErr> {
197-
GameStatistics::find().all(db).await
198-
}
199-
200-
/// 初始化游戏统计记录(游戏启动时调用)
200+
pub async fn get_all_statistics(
201+
db: &DatabaseConnection,
202+
) -> Result<Vec<game_statistics::Model>, DbErr> {
203+
GameStatistics::find().all(db).await
204+
}
205+
206+
/// 获取所有游戏的最近游玩时间,不包含 daily_stats 大字段。
207+
pub async fn get_all_last_played(
208+
db: &DatabaseConnection,
209+
) -> Result<Vec<GameLastPlayed>, DbErr> {
210+
GameStatistics::find()
211+
.select_only()
212+
.column(game_statistics::Column::GameId)
213+
.column(game_statistics::Column::LastPlayed)
214+
.into_model::<GameLastPlayed>()
215+
.all(db)
216+
.await
217+
}
218+
219+
/// 初始化游戏统计记录(游戏启动时调用)
201220
pub async fn init_statistics_if_not_exists(
202221
db: &DatabaseConnection,
203222
game_id: i32,

src-tauri/src/database/service.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::database::dto::{
77
};
88
use crate::database::repository::{
99
collections_repository::{CategoryWithCount, CollectionsRepository},
10-
game_stats_repository::{DailyStats, GameStatsRepository},
10+
game_stats_repository::{DailyStats, GameLastPlayed, GameStatsRepository},
1111
games_repository::{GameType, GamesRepository, SortOption, SortOrder},
1212
settings_repository::SettingsRepository,
1313
};
@@ -366,6 +366,16 @@ pub async fn get_all_game_statistics(
366366
.map_err(|e| format!("获取所有游戏统计失败: {}", e))
367367
}
368368

369+
/// 获取所有游戏的最近游玩时间
370+
#[tauri::command]
371+
pub async fn get_all_game_last_played(
372+
db: State<'_, DatabaseConnection>,
373+
) -> Result<Vec<GameLastPlayed>, String> {
374+
GameStatsRepository::get_all_last_played(&db)
375+
.await
376+
.map_err(|e| format!("获取所有游戏最近游玩时间失败: {}", e))
377+
}
378+
369379
/// 删除游戏统计信息
370380
#[tauri::command]
371381
pub async fn delete_game_statistics(

src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ pub fn run() {
101101
get_game_statistics,
102102
get_multiple_game_statistics,
103103
get_all_game_statistics,
104+
get_all_game_last_played,
104105
delete_game_statistics,
105106
get_today_playtime,
106107
init_game_statistics,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { TFunction } from "i18next";
2+
import type { SortOption } from "@/services/invoke/types";
3+
import type { GameData } from "@/types";
4+
import { getLocalDateString } from "@/utils/dateTime";
5+
import type { CardSortFieldOverlay } from "./types";
6+
7+
interface CardSortFieldOverlayParams {
8+
game: GameData;
9+
sortOption: SortOption;
10+
lastPlayed?: number | null;
11+
language: string;
12+
t: TFunction;
13+
}
14+
15+
const DAY_MS = 24 * 60 * 60 * 1000;
16+
17+
function startOfLocalDay(date: Date) {
18+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
19+
}
20+
21+
function formatTimeHM(date: Date) {
22+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
23+
}
24+
25+
function formatScore(value: number | null | undefined) {
26+
return value ? value.toFixed(1) : null;
27+
}
28+
29+
function formatRank(value: number | null | undefined) {
30+
return value && value > 0 ? `#${value}` : null;
31+
}
32+
33+
function formatBgmOverlay(
34+
score: number | null | undefined,
35+
rank: number | null | undefined,
36+
) {
37+
const parts = [formatScore(score), formatRank(rank)].filter(Boolean);
38+
return parts.length > 0 ? parts.join(" ") : null;
39+
}
40+
41+
function formatLastPlayedOverlay(
42+
timestamp: number | null | undefined,
43+
language: string,
44+
t: TFunction,
45+
) {
46+
if (!timestamp) return null;
47+
48+
const date = new Date(timestamp * 1000);
49+
const today = startOfLocalDay(new Date());
50+
const targetDay = startOfLocalDay(date);
51+
const diffDays = Math.floor((today.getTime() - targetDay.getTime()) / DAY_MS);
52+
const time = formatTimeHM(date);
53+
54+
if (diffDays === 0) return time;
55+
if (diffDays === 1) {
56+
return `${t("components.Cards.sortOverlay.yesterday", "昨天")} ${time}`;
57+
}
58+
if (diffDays > 1 && diffDays < 7) {
59+
const weekday = new Intl.DateTimeFormat(language, {
60+
weekday: "short",
61+
}).format(date);
62+
return `${weekday} ${time}`;
63+
}
64+
65+
return getLocalDateString(timestamp);
66+
}
67+
68+
export function getCardSortFieldOverlay({
69+
game,
70+
sortOption,
71+
lastPlayed,
72+
language,
73+
t,
74+
}: CardSortFieldOverlayParams): CardSortFieldOverlay | undefined {
75+
let value: string | null;
76+
77+
switch (sortOption) {
78+
case "addtime":
79+
value = game.created_at ? getLocalDateString(game.created_at) : null;
80+
break;
81+
case "datetime":
82+
value = game.date || null;
83+
break;
84+
case "userratingrank":
85+
value = formatScore(game.custom_data?.user_rating);
86+
break;
87+
case "bgmrank":
88+
value = formatBgmOverlay(game.sourceScores?.bgm, game.rank);
89+
break;
90+
case "vndbrank":
91+
value = formatScore(game.sourceScores?.vndb);
92+
break;
93+
case "lastplayed":
94+
value = formatLastPlayedOverlay(lastPlayed, language, t);
95+
break;
96+
case "namesort":
97+
value = null;
98+
break;
99+
}
100+
101+
return value ? { value } : undefined;
102+
}

src/components/Cards/useCardsController.tsx

Lines changed: 21 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,23 @@ import { useLocation, useNavigate } from "react-router-dom";
44
import { useShallow } from "zustand/react/shallow";
55
import { saveScrollPosition } from "@/hooks/common/useScrollRestore";
66
import { useRemoveGamesFromCategory } from "@/hooks/queries/useCollections";
7+
import { useAllGameLastPlayedMap } from "@/hooks/queries/useStats";
78
import { snackbar } from "@/providers/snackBar";
8-
import type { SortOption } from "@/services/invoke/types";
99
import { useStore } from "@/store/appStore";
1010
import { useGamePlayStore } from "@/store/gamePlayStore";
1111
import type { GameData } from "@/types";
12-
import { getLocalDateString } from "@/utils/dateTime";
1312
import { getUserErrorMessage } from "@/utils/errors";
1413
import { getGameDisplayName } from "@/utils/game";
1514
import { CardsBatchBar } from "./CardsBatchBar";
15+
import { getCardSortFieldOverlay } from "./cardSortFieldOverlay";
1616
import { RightMenuHost } from "./RightMenuHost";
17-
import type {
18-
CardSortFieldOverlay,
19-
RightMenuHostHandle,
20-
SortableCardItemProps,
21-
} from "./types";
17+
import type { RightMenuHostHandle, SortableCardItemProps } from "./types";
2218

2319
interface UseCardsControllerOptions {
2420
gameIds: number[];
2521
categoryId?: number;
2622
}
2723

28-
function getCardSortFieldOverlay(
29-
game: GameData,
30-
sortOption: SortOption,
31-
): CardSortFieldOverlay | undefined {
32-
switch (sortOption) {
33-
case "addtime":
34-
return game.created_at
35-
? { value: getLocalDateString(game.created_at) }
36-
: undefined;
37-
case "datetime":
38-
return game.date ? { value: game.date } : undefined;
39-
case "userratingrank": {
40-
const userRating = game.custom_data?.user_rating;
41-
return userRating ? { value: userRating.toFixed(1) } : undefined;
42-
}
43-
case "bgmrank":
44-
case "vndbrank":
45-
case "lastplayed":
46-
case "namesort":
47-
return undefined;
48-
}
49-
}
50-
5124
export function useCardsController({
5225
gameIds,
5326
categoryId,
@@ -74,6 +47,13 @@ export function useCardsController({
7447
})),
7548
);
7649
const launchGame = useGamePlayStore((s) => s.launchGame);
50+
const shouldShowCardSortFieldOverlay =
51+
isLibraries && showCardSortFieldOverlay;
52+
const shouldLoadLastPlayed =
53+
shouldShowCardSortFieldOverlay && sortOption === "lastplayed";
54+
const lastPlayedQuery = useAllGameLastPlayedMap({
55+
enabled: shouldLoadLastPlayed,
56+
});
7757
const [batchMode, setBatchMode] = useState(false);
7858
const [selectedBatchGameIds, setSelectedBatchGameIds] = useState<number[]>(
7959
[],
@@ -192,8 +172,14 @@ export function useCardsController({
192172
return {
193173
game,
194174
displayName: getGameDisplayName(game),
195-
sortFieldOverlay: showCardSortFieldOverlay
196-
? getCardSortFieldOverlay(game, sortOption)
175+
sortFieldOverlay: shouldShowCardSortFieldOverlay
176+
? getCardSortFieldOverlay({
177+
game,
178+
sortOption,
179+
lastPlayed: lastPlayedQuery.data?.get(gameId),
180+
language: i18n.language,
181+
t,
182+
})
197183
: undefined,
198184
batch: showBatchControls
199185
? { selected: selectedBatchGameIdSet.has(gameId) }
@@ -220,8 +206,10 @@ export function useCardsController({
220206
handleCardDoubleClick,
221207
handleRemoveSingleFromCategory,
222208
isCollectionCategory,
209+
i18n.language,
210+
lastPlayedQuery.data,
211+
shouldShowCardSortFieldOverlay,
223212
selectedBatchGameIdSet,
224-
showCardSortFieldOverlay,
225213
showBatchControls,
226214
sortOption,
227215
t,

src/hooks/queries/useStats.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useQuery } from "@tanstack/react-query";
22
import { useMemo } from "react";
33
import {
4+
getAllGameLastPlayed,
45
getAllGameStatistics,
56
getFormattedGameStats,
67
getRecentSessionsForGames,
@@ -13,6 +14,7 @@ export const statsKeys = {
1314
gameStats: (gameId: number) => [...statsKeys.all, "game", gameId] as const,
1415
sessions: (gameId: number, limit: number) =>
1516
[...statsKeys.all, "sessions", gameId, limit] as const,
17+
allGameLastPlayed: () => [...statsKeys.all, "allGameLastPlayed"] as const,
1618
recentSessionsForGames: (gameIds: number[], limit: number) =>
1719
[...statsKeys.all, "recentSessionsForGames", gameIds, limit] as const,
1820
playTimeSummary: () => [...statsKeys.all, "playTimeSummary"] as const,
@@ -120,6 +122,14 @@ function useGameSessions(gameId: number | null, limit = 10) {
120122
});
121123
}
122124

125+
function useAllGameLastPlayedMap({ enabled }: { enabled: boolean }) {
126+
return useQuery({
127+
queryKey: statsKeys.allGameLastPlayed(),
128+
queryFn: getAllGameLastPlayed,
129+
enabled,
130+
});
131+
}
132+
123133
function useRecentSessionsForGames(gameIds: number[], limit = 10) {
124134
return useQuery({
125135
queryKey: statsKeys.recentSessionsForGames(gameIds, limit),
@@ -182,6 +192,7 @@ function usePlayTimeSummary() {
182192
}
183193

184194
export {
195+
useAllGameLastPlayedMap,
185196
useGameSessions,
186197
useGameStats,
187198
usePlayTimeSummary,

src/locales/en-US.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,10 @@
108108
"Cards": {
109109
"removeFromCategory": "Remove from Current Category",
110110
"removeFromCategoryFailed": "Failed to remove from category",
111-
"removeFromCategorySuccess": "Removed from current category"
111+
"removeFromCategorySuccess": "Removed from current category",
112+
"sortOverlay": {
113+
"yesterday": "Yesterday"
114+
}
112115
},
113116
"Collection": {
114117
"errors": {

src/locales/ja-JP.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@
104104
"Cards": {
105105
"removeFromCategory": "現在のカテゴリから削除",
106106
"removeFromCategoryFailed": "カテゴリからの削除に失敗しました",
107-
"removeFromCategorySuccess": "現在のカテゴリから削除しました"
107+
"removeFromCategorySuccess": "現在のカテゴリから削除しました",
108+
"sortOverlay": {
109+
"yesterday": "昨日"
110+
}
108111
},
109112
"Collection": {
110113
"errors": {

src/locales/zh-CN.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@
104104
"Cards": {
105105
"removeFromCategory": "移出当前分类",
106106
"removeFromCategoryFailed": "移出分类失败",
107-
"removeFromCategorySuccess": "已从当前分类移除"
107+
"removeFromCategorySuccess": "已从当前分类移除",
108+
"sortOverlay": {
109+
"yesterday": "昨天"
110+
}
108111
},
109112
"Collection": {
110113
"errors": {

src/locales/zh-TW.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,10 @@
104104
"Cards": {
105105
"removeFromCategory": "移出目前分類",
106106
"removeFromCategoryFailed": "移出分類失敗",
107-
"removeFromCategorySuccess": "已從目前分類移除"
107+
"removeFromCategorySuccess": "已從目前分類移除",
108+
"sortOverlay": {
109+
"yesterday": "昨天"
110+
}
108111
},
109112
"Collection": {
110113
"errors": {

0 commit comments

Comments
 (0)