Public Access
feat(import): add the import page, multi-select add and the favorite platform
This commit is contained in:
1 parent
aa9e83e0cb
commit
14dd59f9e0
21 files changed
+1436
-48
No files matched your search
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user