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

sync main #3

Merged
merged 1 commit into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@
"@types/react-router-dom": "^4.3.1",
"@types/react-transition-group": "^2.0.9",
"@types/webpack-env": "^1.13.6",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.1.0",
Expand Down
12 changes: 12 additions & 0 deletions apps/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import ResetScroll from "./components/ResetScroll";
import commonEN from "./locales/en/common.json";
import commonES from "./locales/es/common.json";
import commonFR from "./locales/fr/common.json";
import commonHR from "./locales/hr/common.json";
import commonRO from "./locales/ro/common.json";
import commonID from "./locales/id/common.json";
import { About } from "./pages/about";
import { CreateProjectPage } from "./pages/create";
import { HomePage } from "./pages/home";
Expand Down Expand Up @@ -78,6 +81,15 @@ i18next
es_ES: {
translations: commonES,
},
hr_HR: {
translations: commonHR,
},
ro_RO: {
translations: commonRO,
},
id_ID: {
translations: commonID,
},
},
ns: ["translations"],
defaultNS: "translations",
Expand Down
9 changes: 9 additions & 0 deletions apps/frontend/src/components/LanguageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ export const LanguageMenu = () => {
<MenuItem onClick={() => handleLangChange("es_ES")}>
{t("language-menu.spanish")}
</MenuItem>
<MenuItem onClick={() => handleLangChange("hr_HR")}>
{t("language.croatian")}
</MenuItem>
<MenuItem onClick={() => handleLangChange("ro_RO")}>
{t("language.romanian")}
</MenuItem>
<MenuItem onClick={() => handleLangChange("id_ID")}>
{t("language.indonesian")}
</MenuItem>
</Menu>
</React.Fragment>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/frontend/src/components/annotation/AnnotationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ function AdvancedControls({
user?: UserMe;
}) {
const { mode, setMode } = usePlayerModeStore();

const { t } = useTranslation();
return (
<Stack
direction="row"
Expand All @@ -328,7 +328,7 @@ function AdvancedControls({
inputProps={{ "aria-label": "ant design" }}
/>
<Typography sx={{ color: "text.secondary", fontSize: "12px" }}>
Show Only Mine
{t("annotation.show-only-mine")}
</Typography>
</Stack>
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
Expand All @@ -340,7 +340,7 @@ function AdvancedControls({
inputProps={{ "aria-label": "ant design" }}
/>
<Typography sx={{ color: "text.secondary", fontSize: "12px" }}>
Performance Mode
{t("annotation.performance-mode")}
</Typography>
</Stack>
</Stack>
Expand Down
52 changes: 33 additions & 19 deletions apps/frontend/src/components/annotation/concept-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,55 @@ import FormControl from "@mui/material/FormControl";
import Select, { type SelectChangeEvent } from "@mui/material/Select";
import { useConceptsQuery } from "~utils/concepts";
import { grey } from "@mui/material/colors";
import { InputLabel, OutlinedInput } from "@mui/material";
import { useTranslation } from "react-i18next";

const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};

export function ConceptSelector() {
const { data } = useConceptsQuery();
const [concept, setConcept] = React.useState("");

const { t } = useTranslation();
const handleChange = (event: SelectChangeEvent) => {
setConcept(event.target.value);
};

return (
<FormControl sx={{ m: 1, minWidth: 120 }} size="small">
<FormControl sx={{ m: 1, maxWidth: 150 }} size="small">
<Select
labelId="demo-select-small-label"
id="demo-select-small"
labelId="concept-select-label"
id="concept-select"
value={concept}
label={t("annotation.concept.label")}
hiddenLabel
onChange={handleChange}
sx={{
color: "white",
".MuiOutlinedInput-notchedOutline": {
borderColor: grey[800],
},
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: "white",
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: "white",
},
".MuiSvgIcon-root": {
color: "white",
},
displayEmpty
input={<OutlinedInput />}
renderValue={(selected) => {
if (selected === "") {
return (
<span style={{ color: "white" }}>
{t("annotation.concept.label")}
</span>
);
}
return <span style={{ color: "white" }}>{selected}</span>;
}}
MenuProps={MenuProps}
inputProps={{ "aria-label": "Without label" }}
>
<MenuItem value="">
<em>Sélectionner un concept</em>
<MenuItem disabled value="">
<em>{t("annotation.select-concept")}</em>
</MenuItem>
{data?.map((concept) => (
<MenuItem key={concept.concept} value={concept.concept}>
Expand Down
215 changes: 215 additions & 0 deletions apps/frontend/src/components/commun/button-duplin-export.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import * as React from "react";
import { styled, alpha } from "@mui/material/styles";
import Button from "@mui/material/Button";
import Menu, { type MenuProps } from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import type { ProjectById } from "~/utils/trpc";
import { parseString, Builder } from "xml2js";
import { useTranslation } from "react-i18next";
import saveAs from "file-saver";
import { useSnackbar } from "notistack";
import type { DublinMetadata } from "../project/dubin-panel";

const StyledMenu = styled((props: MenuProps) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
transformOrigin={{
vertical: "top",
horizontal: "right",
}}
{...props}
/>
))(({ theme }) => ({
"& .MuiPaper-root": {
borderRadius: 6,
marginTop: theme.spacing(1),
minWidth: 180,
color: "rgb(55, 65, 81)",
boxShadow:
"rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px",
"& .MuiMenu-list": {
padding: "4px 0",
},
"& .MuiMenuItem-root": {
"& .MuiSvgIcon-root": {
fontSize: 18,
color: theme.palette.text.secondary,
marginRight: theme.spacing(1.5),
},
"&:active": {
backgroundColor: alpha(
theme.palette.primary.main,
theme.palette.action.selectedOpacity
),
},
},
...theme.applyStyles("dark", {
color: theme.palette.grey[300],
}),
},
}));

export default function DuplinExportButton({
project,
onImport,
}: {
project: ProjectById;
onImport: (metadata: DublinMetadata) => void;
}) {
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};

const { enqueueSnackbar } = useSnackbar();

const handleClose = () => {
setAnchorEl(null);
};

// Save metadata to XML
const handleExport = () => {
handleClose();

const builder = new Builder({
xmldec: { version: "1.0", encoding: "UTF-8" },
});

const dublin = project.dublin;

const xmlObject = {
"rdf:RDF": {
$: {
"xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"xmlns:dcterms": "http://purl.org/dc/terms/",
},

"dcterms:title": [dublin?.title ?? ""],
"dcterms:creator": [dublin?.creator ?? ""],
"dcterms:subject": [dublin?.subject ?? ""],
"dcterms:description": [dublin?.description ?? ""],
"dcterms:publisher": [dublin?.publisher ?? ""],
"dcterms:contributor": [dublin?.contributor ?? ""],
"dcterms:date": [dublin?.date ?? ""],
"dcterms:type": [dublin?.type ?? ""],
"dcterms:format": [dublin?.format ?? ""],
"dcterms:identifier": [dublin?.identifier ?? ""],
"dcterms:source": [dublin?.source ?? ""],
"dcterms:language": [dublin?.language ?? ""],
"dcterms:relation": [dublin?.relation ?? ""],
"dcterms:coverage": [dublin?.coverage ?? ""],
"dcterms:rights": [dublin?.rights ?? ""],
},
};

const data = builder.buildObject(xmlObject);
// Simulate file save (in a real app, you'd use file system API)

const blob = new Blob([data], {
type: "text/xml;charset=utf-8",
});
saveAs(blob, "dublin.xml");

enqueueSnackbar("Exported successfully", {
variant: "success",
key: "metadata.dublin.export.success",
});
};

// Load XML file on component mount
const handleImport = async () => {
handleClose();
const [fileHandle] = await window.showOpenFilePicker({
types: [
{
description: "XML Files",
accept: {
"text/xml": [".xml"],
},
},
],
multiple: false,
});

// Get the file
const file = await fileHandle.getFile();
const text = await file.text();

// Parse XML to set initial metadata
parseString(text, (err, result) => {
if (err) {
console.error("Error parsing XML", err);
return;
}

const rdfData = result?.["rdf:RDF"] || {};
const newMetadata = {
title: rdfData["dcterms:title"]?.[0] ?? undefined,
creator: rdfData["dcterms:creator"]?.[0] ?? undefined,
subject: rdfData["dcterms:subject"]?.[0] ?? undefined,
description: rdfData["dcterms:description"]?.[0] ?? undefined,
publisher: rdfData["dcterms:publisher"]?.[0] ?? undefined,
contributor: rdfData["dcterms:contributor"]?.[0] ?? undefined,
date: rdfData["dcterms:date"]?.[0] ?? undefined,
type: rdfData["dcterms:type"]?.[0] ?? undefined,
format: rdfData["dcterms:format"]?.[0] ?? undefined,
identifier: rdfData["dcterms:identifier"]?.[0] ?? undefined,
source: rdfData["dcterms:source"]?.[0] ?? undefined,
language: rdfData["dcterms:language"]?.[0] ?? undefined,
relation: rdfData["dcterms:relation"]?.[0] ?? undefined,
coverage: rdfData["dcterms:coverage"]?.[0] ?? undefined,
rights: rdfData["dcterms:rights"]?.[0] ?? undefined,
};

const cleanMetadata = Object.fromEntries(
Object.entries(newMetadata).filter(
([_, value]) => value !== undefined && value !== null && value !== ""
)
);

onImport(cleanMetadata as DublinMetadata);
});
};

return (
<div>
<Button
id="demo-customized-button"
aria-controls={open ? "demo-customized-menu" : undefined}
aria-haspopup="true"
aria-expanded={open ? "true" : undefined}
variant="contained"
disableElevation
onClick={handleClick}
endIcon={<KeyboardArrowDownIcon />}
>
{t("metadata.dublin.option")}
</Button>
<StyledMenu
id="demo-customized-menu"
MenuListProps={{
"aria-labelledby": "demo-customized-button",
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem onClick={handleExport} disableRipple>
{t("metadata.dublin.menu.export")}
</MenuItem>
{project.editable && (
<MenuItem onClick={handleImport} disableRipple>
{t("metadata.dublin.menu.import")}
</MenuItem>
)}
</StyledMenu>
</div>
);
}
Loading
Loading