feat(import): add the import page, multi-select add and the favorite platform

This commit is contained in:
lobo committed 2026-09-29 10:20:54 +02:00
1 parent aa9e83e0cb
commit 14dd59f9e0
21 files changed
+1436 -48

No files matched your search

+114
View File
@@ -0,0 +1,114 @@
import {apiFetch} from './api.js';
import {parseStatus} from './backlog.js';
import {hoursToMinutes, MAX_PLATFORM_LENGTH} from './manualGames.js';
const IMPORTS_URL = () => `${import.meta.env.VITE_CORE_API_URL}/api/library/imports`;
const JSON_HEADERS = {'Content-Type': 'application/json'};
// Core's limits: lines per import, title length of the game search, playtime.
export const MAX_IMPORT_ROWS = 1000;
const MAX_TITLE = 100;
const MAX_MINUTES = 6_000_000;
// The calls return the raw response; the caller handles the status.
export const createImport = (source, rows) =>
apiFetch(IMPORTS_URL(), {method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({source, rows})});
export const listImports = () => apiFetch(IMPORTS_URL());
export const getImport = (id) => apiFetch(`${IMPORTS_URL()}/${id}`);
export const saveImport = (id, rows) =>
apiFetch(`${IMPORTS_URL()}/${id}/save`, {method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({rows})});
export const retryImport = (id) => apiFetch(`${IMPORTS_URL()}/${id}/retry`, {method: 'POST'});
export const checkUndo = (id) => apiFetch(`${IMPORTS_URL()}/${id}/undo-check`);
export const undoImport = (id) => apiFetch(`${IMPORTS_URL()}/${id}/games`, {method: 'DELETE'});
/**
* One title per line; empty lines are left out.
*/
export const parseTextList = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
.map((line) => ({title: line.slice(0, MAX_TITLE)}));
// Header names, lower case, per field.
const COLUMNS = {
title: ['title', 'name', 'game'],
platform: ['platform'],
hours: ['hours', 'playtime', 'playtime (hours)'],
minutes: ['minutes', 'playtime (minutes)'],
status: ['status'],
};
/**
* A CSV with a header row; the delimiter (comma, semicolon or tab) is the one the header has most of. A field that
* starts with a double quote may hold the delimiter, line breaks and "" for a quote.
*
* @return {rows} with title, platform, playtimeMinutes and status where the file has them, or {error} as an i18n key
*/
export const parseCsv = (text) => {
const clean = text.replace(/^/, '');
const firstLine = clean.split(/\r?\n/, 1)[0];
const delimiter = [',', ';', '\t'].reduce((best, next) =>
firstLine.split(next).length > firstLine.split(best).length ? next : best);
const [header, ...records] = csvRecords(clean, delimiter);
const names = (header ?? []).map((name) => name.trim().toLowerCase());
const at = Object.fromEntries(Object.entries(COLUMNS)
.map(([field, headers]) => [field, names.findIndex((name) => headers.includes(name))]));
if (at.title < 0) return {error: 'IMPORT.CSV_NO_TITLE'};
const value = (record, field) => (at[field] >= 0 ? (record[at[field]] ?? '').trim() : '');
const rows = records.filter((record) => value(record, 'title')).map((record) => {
const platform = value(record, 'platform');
const minutes = at.minutes >= 0 ? Math.round(Number(value(record, 'minutes'))) || 0
: hoursToMinutes(value(record, 'hours'));
return {
title: value(record, 'title').slice(0, MAX_TITLE),
platform: platform && platform.length <= MAX_PLATFORM_LENGTH ? platform : null,
playtimeMinutes: minutes > 0 ? Math.min(minutes, MAX_MINUTES) : null,
status: parseStatus(value(record, 'status').toUpperCase()),
};
});
return {rows};
};
function csvRecords(text, delimiter) {
const records = [];
let record = [];
let field = '';
let quoted = false;
for (let i = 0; i < text.length; i++) {
const c = text[i];
if (quoted) {
if (c === '"' && text[i + 1] === '"') {
field += '"';
i++;
} else if (c === '"') {
quoted = false;
} else {
field += c;
}
} else if (c === '"' && field === '') {
// Only at the start of a field; a quote inside one (12" Barbarian) is a character.
quoted = true;
} else if (c === delimiter) {
record.push(field);
field = '';
} else if (c === '\n' || c === '\r') {
if (c === '\r' && text[i + 1] === '\n') i++;
record.push(field);
records.push(record);
record = [];
field = '';
} else {
field += c;
}
}
if (field || record.length) {
record.push(field);
records.push(record);
}
return records;
}