mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-11-02 13:07:03 +00:00
added translations
This commit is contained in:
83
package-lock.json
generated
83
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.37",
|
||||
"version": "1.0.46",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "qapp-core",
|
||||
"version": "1.0.37",
|
||||
"version": "1.0.46",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/react-virtual": "^3.13.2",
|
||||
@@ -16,10 +16,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"dompurify": "^3.2.4",
|
||||
"i18next": "^25.3.2",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-idle-timer": "^5.7.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
@@ -2395,6 +2397,46 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.3.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz",
|
||||
"integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
|
||||
@@ -3217,6 +3259,32 @@
|
||||
"react-dom": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.1.tgz",
|
||||
"integrity": "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-idle-timer": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz",
|
||||
@@ -3947,7 +4015,7 @@
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -4024,6 +4092,15 @@
|
||||
"global": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"generate-i18n": "./scripts/generate_locales.js",
|
||||
"build": "npm run generate-i18n && tsup",
|
||||
"prepare": "npm run build",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
@@ -30,10 +31,12 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dexie": "^4.0.11",
|
||||
"dompurify": "^3.2.4",
|
||||
"i18next": "^25.3.2",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"iso-639-1": "^3.1.5",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hot-toast": "^2.5.2",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-idle-timer": "^5.7.2",
|
||||
"react-intersection-observer": "^9.16.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
|
||||
36
scripts/generate_locales.js
Executable file
36
scripts/generate_locales.js
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fg = require('fast-glob');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOCALES_DIR = path.resolve(__dirname, '../src/i18n/locales');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../src/i18n/compiled-i18n.json');
|
||||
|
||||
(async () => {
|
||||
const files = await fg('**/*.json', { cwd: LOCALES_DIR, absolute: true });
|
||||
|
||||
const resources = {};
|
||||
const supportedLanguages = new Set();
|
||||
|
||||
for (const filePath of files) {
|
||||
const parts = filePath.split(path.sep);
|
||||
const lang = parts[parts.length - 2];
|
||||
const ns = path.basename(filePath, '.json');
|
||||
|
||||
supportedLanguages.add(lang);
|
||||
|
||||
const json = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
||||
|
||||
if (!resources[lang]) resources[lang] = {};
|
||||
resources[lang][ns] = json;
|
||||
}
|
||||
|
||||
// Save compiled resources and languages
|
||||
fs.writeFileSync(
|
||||
OUTPUT_FILE,
|
||||
JSON.stringify({ resources, supportedLanguages: Array.from(supportedLanguages) }, null, 2)
|
||||
);
|
||||
|
||||
console.log('✅ i18n resources generated.');
|
||||
})();
|
||||
@@ -39,6 +39,8 @@ import { useModal } from "../useModal";
|
||||
import { createAvatarLink } from "../../utils/qortal";
|
||||
import { extractComponents } from "../../utils/text";
|
||||
import NavigateNextIcon from "@mui/icons-material/NavigateNext";
|
||||
import { useLibTranslation } from "../../hooks/useLibTranslation";
|
||||
import { t } from "i18next";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 10, dictionary: "alphanum" });
|
||||
|
||||
@@ -57,6 +59,8 @@ interface PropsIndexManager {
|
||||
const cleanString = (str: string) => str.replace(/\s{2,}/g, ' ').trim().toLocaleLowerCase();
|
||||
|
||||
export const IndexManager = ({ username }: PropsIndexManager) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const open = useIndexStore((state) => state.open);
|
||||
const setOpen = useIndexStore((state) => state.setOpen);
|
||||
const [title, setTitle] = useState("");
|
||||
@@ -123,7 +127,7 @@ export const IndexManager = ({ username }: PropsIndexManager) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>Index manager</DialogTitle>
|
||||
<DialogTitle>{t("index.title")}</DialogTitle>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
@@ -196,6 +200,8 @@ const EntryMode = ({
|
||||
username,
|
||||
hasMetadata,
|
||||
}: PropsEntryMode) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
@@ -222,7 +228,7 @@ const EntryMode = ({
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography>Create new index</Typography>
|
||||
<Typography>{t("index.create_new_index")}</Typography>
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
{/* <ButtonBase
|
||||
@@ -260,7 +266,7 @@ const EntryMode = ({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>Add metadata</Typography>
|
||||
<Typography>{t("index.add_metadata")}</Typography>
|
||||
{hasMetadata && <CheckCircleIcon color="success" />}
|
||||
</Box>
|
||||
</ButtonBase>
|
||||
@@ -288,12 +294,14 @@ const AddMetadata = ({
|
||||
setDescription,
|
||||
setTitle,
|
||||
}: PropsAddMetadata) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const publish = usePublish();
|
||||
|
||||
const disableButton = !title.trim() || !description.trim() || !name || !link;
|
||||
|
||||
const createMetadata = async () => {
|
||||
const loadId = showLoading("Publishing metadata...");
|
||||
const loadId = showLoading(t("index.publishing_metadata"));
|
||||
try {
|
||||
const identifierWithoutHash = name + link;
|
||||
const identifier = await hashWordWithoutPublicSalt(
|
||||
@@ -313,7 +321,7 @@ const AddMetadata = ({
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
showSuccess("Successfully published metadata");
|
||||
showSuccess(t("index.published_metadata"));
|
||||
publish.updatePublish(
|
||||
{
|
||||
identifier,
|
||||
@@ -325,7 +333,7 @@ const AddMetadata = ({
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to publish metadata";
|
||||
error instanceof Error ? error.message : t("index.failed_metadata");
|
||||
showError(message);
|
||||
} finally {
|
||||
dismissToast(loadId);
|
||||
@@ -350,7 +358,7 @@ const AddMetadata = ({
|
||||
<IconButton disabled={mode === 1} onClick={() => setMode(1)}>
|
||||
<ArrowBackIosIcon />
|
||||
</IconButton>
|
||||
<Typography>Example of how it could look like:</Typography>
|
||||
<Typography>{t("index.example")}</Typography>
|
||||
<Card sx={{
|
||||
width: '100%',
|
||||
padding: '5px'
|
||||
@@ -437,12 +445,12 @@ const AddMetadata = ({
|
||||
</Box>
|
||||
</Card>
|
||||
<Box>
|
||||
<Typography>Title</Typography>
|
||||
<Typography>{t("index.metadata.title")}</Typography>
|
||||
<TextField
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
size="small"
|
||||
placeholder="Add a title for the link"
|
||||
placeholder={t("index.metadata_title_placeholder")}
|
||||
slotProps={{
|
||||
htmlInput: { maxLength: 50 },
|
||||
}}
|
||||
@@ -452,19 +460,19 @@ const AddMetadata = ({
|
||||
variant="caption"
|
||||
color={title.length >= 50 ? "error" : "text.secondary"}
|
||||
>
|
||||
{title.length}/{50} characters
|
||||
{title.length}/{50} {` ${t("index.characters")}`}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography>Description</Typography>
|
||||
<Typography>{t("index.metadata.description")}</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
size="small"
|
||||
placeholder="Add a description for the link"
|
||||
placeholder={t("index.metadata_description_placeholder")}
|
||||
slotProps={{
|
||||
htmlInput: { maxLength: 120 },
|
||||
}}
|
||||
@@ -473,7 +481,7 @@ const AddMetadata = ({
|
||||
variant="caption"
|
||||
color={description.length >= 120 ? "error" : "text.secondary"}
|
||||
>
|
||||
{description.length}/{120} characters
|
||||
{description.length}/{120} {` ${t("index.characters")}`}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
@@ -486,7 +494,7 @@ const AddMetadata = ({
|
||||
disabled={disableButton}
|
||||
variant="contained"
|
||||
>
|
||||
Publish metadata
|
||||
{t("actions.publish_metadata")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
@@ -507,6 +515,8 @@ const CreateIndex = ({
|
||||
category,
|
||||
rootName,
|
||||
}: PropsCreateIndex) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const [terms, setTerms] = useState<string[]>([]);
|
||||
const publish = usePublish();
|
||||
const [size, setSize] = useState(0);
|
||||
@@ -588,7 +598,7 @@ const CreateIndex = ({
|
||||
const disableButton = (terms.length === 0 && !recommendedSelection) || !name || !link;
|
||||
|
||||
const createIndex = async () => {
|
||||
const loadId = showLoading("Publishing index...");
|
||||
const loadId = showLoading(t("index.publishing_index"));
|
||||
try {
|
||||
const hashedRootName = await hashWordWithoutPublicSalt(rootName, 20);
|
||||
const hashedLink = await hashWordWithoutPublicSalt(link, 20);
|
||||
@@ -603,7 +613,7 @@ const CreateIndex = ({
|
||||
});
|
||||
|
||||
if (res?.signature) {
|
||||
showSuccess("Successfully published index");
|
||||
showSuccess(t("index.published_index"));
|
||||
publish.updatePublish(
|
||||
{
|
||||
identifier,
|
||||
@@ -616,7 +626,7 @@ const CreateIndex = ({
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to publish index";
|
||||
error instanceof Error ? error.message : t("index.failed_index");
|
||||
showError(message);
|
||||
} finally {
|
||||
dismissToast(loadId);
|
||||
@@ -645,7 +655,7 @@ const CreateIndex = ({
|
||||
</IconButton>
|
||||
{recommendedIndices?.length > 0 && (
|
||||
<>
|
||||
<Typography>Recommended Indices</Typography>
|
||||
<Typography>{t("index.recommended_indices")}</Typography>
|
||||
<RadioGroup
|
||||
aria-labelledby="demo-controlled-radio-buttons-group"
|
||||
name="controlled-radio-buttons-group"
|
||||
@@ -673,7 +683,7 @@ const CreateIndex = ({
|
||||
value={recommendedSelection}
|
||||
onChange={handleChange}
|
||||
>
|
||||
<FormControlLabel value="" control={<Radio />} label="Add search term" />
|
||||
<FormControlLabel value="" control={<Radio />} label={t("index.add_search_term")} />
|
||||
</RadioGroup>
|
||||
<Spacer height="10px" />
|
||||
{!recommendedSelection && (
|
||||
@@ -681,7 +691,7 @@ const CreateIndex = ({
|
||||
maxLength={17}
|
||||
items={terms}
|
||||
onlyStrings
|
||||
label="search terms"
|
||||
label={t("index.search_terms")}
|
||||
setItems={async (termsNew: string[]) => {
|
||||
try {
|
||||
if (terms?.length === 1 && termsNew?.length === 2) {
|
||||
@@ -704,8 +714,10 @@ const CreateIndex = ({
|
||||
shouldRecommendMax && fullSize > 230 ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
It is recommended to keep your term character count below{" "}
|
||||
{recommendedSize} characters
|
||||
|
||||
{t("index.recommendation_size", {
|
||||
recommendedSize
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -716,7 +728,7 @@ const CreateIndex = ({
|
||||
disabled={disableButton}
|
||||
variant="contained"
|
||||
>
|
||||
Publish index
|
||||
{t("actions.publish_index")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<Dialog
|
||||
@@ -733,18 +745,17 @@ const CreateIndex = ({
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
Adding multiple indices
|
||||
{t("index.multiple_title")}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Subsequent indices will keep your publish fees lower, but they will
|
||||
have less strength in future search results.
|
||||
{t("index.multiple_description")}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={onCancel}>Cancel</Button>
|
||||
<Button variant="contained" onClick={onCancel}>{t("actions.cancel")}</Button>
|
||||
<Button variant="contained" onClick={onOk}>
|
||||
Continue
|
||||
{t("actions.continue")}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -762,6 +773,8 @@ const YourIndices = ({
|
||||
category,
|
||||
rootName,
|
||||
}: PropsCreateIndex) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const [terms, setTerms] = useState<string[]>([]);
|
||||
const publish = usePublish();
|
||||
const [size, setSize] = useState(0);
|
||||
|
||||
@@ -22,6 +22,8 @@ import { ResourceToPublish } from "../../types/qortalRequests/types";
|
||||
import { QortalGetMetadata } from "../../types/interfaces/resources";
|
||||
import ErrorIcon from '@mui/icons-material/Error';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import { useLibTranslation } from "../../hooks/useLibTranslation";
|
||||
import { t } from "i18next";
|
||||
|
||||
export interface MultiplePublishError {
|
||||
error: {
|
||||
@@ -31,6 +33,8 @@ export interface MultiplePublishError {
|
||||
|
||||
|
||||
export const MultiPublishDialogComponent = () => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const {
|
||||
resources,
|
||||
isPublishing,
|
||||
@@ -177,7 +181,7 @@ export const MultiPublishDialogComponent = () => {
|
||||
disableAutoFocus
|
||||
disableRestoreFocus
|
||||
>
|
||||
<DialogTitle>Publishing Status</DialogTitle>
|
||||
<DialogTitle>{t("multi_publish.title")}</DialogTitle>
|
||||
<DialogContent>
|
||||
{publishError && (
|
||||
<Stack spacing={3}>
|
||||
@@ -215,7 +219,7 @@ export const MultiPublishDialogComponent = () => {
|
||||
gap: '10px'
|
||||
}}>
|
||||
<ErrorIcon color="error" />
|
||||
<Typography variant="body2">Publish failed</Typography>
|
||||
<Typography variant="body2">{t("multi_publish.publish_failed")}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -229,7 +233,7 @@ export const MultiPublishDialogComponent = () => {
|
||||
reject(new Error('Canceled Publish'));
|
||||
reset();
|
||||
}}>
|
||||
Close
|
||||
{t("actions.close")}
|
||||
</Button>
|
||||
{failedResources?.length > 0 && (
|
||||
<Button
|
||||
@@ -238,7 +242,7 @@ export const MultiPublishDialogComponent = () => {
|
||||
variant="contained"
|
||||
onClick={publishMultipleResources}
|
||||
>
|
||||
Retry
|
||||
{t("actions.retry")}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
@@ -306,14 +310,14 @@ useEffect(() => {
|
||||
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
File Chunk {publishStatus?.chunks || 0}/{publishStatus?.totalChunks || 0} ({chunkPercent.toFixed(0)}%)
|
||||
{t("multi_publish.file_chunk")} {publishStatus?.chunks || 0}/{publishStatus?.totalChunks || 0} ({chunkPercent.toFixed(0)}%)
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={chunkPercent} />
|
||||
</Box>
|
||||
|
||||
<Box mt={2}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
File Processing ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%)
|
||||
{t("multi_publish.file_processing")} ({publishStatus?.processed ? 100 : processingStart ? processingPercent.toFixed(0) : '0'}%)
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={publishStatus?.processed ? 100 : processingPercent} />
|
||||
</Box>
|
||||
@@ -321,14 +325,14 @@ useEffect(() => {
|
||||
{publishStatus?.processed && (
|
||||
<Box mt={2} display="flex" gap={1} alignItems="center">
|
||||
<CheckCircleIcon color="success" />
|
||||
<Typography variant="body2">Published successfully</Typography>
|
||||
<Typography variant="body2">{t("multi_publish.success")}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{publishStatus?.retry && !publishStatus?.error && !publishStatus?.processed && (
|
||||
<Box mt={2} display="flex" gap={1} alignItems="center">
|
||||
<ErrorIcon color="error" />
|
||||
<Typography variant="body2">Publish failed. Attempting retry...</Typography>
|
||||
<Typography variant="body2">{t("multi_publish.attempt_retry")}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -336,7 +340,7 @@ useEffect(() => {
|
||||
<Box mt={2} display="flex" gap={1} alignItems="center">
|
||||
<ErrorIcon color="error" />
|
||||
<Typography variant="body2">
|
||||
Publish failed. {publishStatus?.error?.reason || 'Unknown error'}
|
||||
{t("multi_publish.publish_failed")} - {publishStatus?.error?.reason || 'Unknown error'}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -373,7 +373,6 @@ removeFromList(listName, displayLimit)
|
||||
getResourceMoreList(displayLimit)
|
||||
}, [getResourceMoreList])
|
||||
|
||||
console.log('listToDisplay', listToDisplay)
|
||||
|
||||
return (
|
||||
<div ref={elementRef} style={{
|
||||
|
||||
@@ -292,7 +292,6 @@ removeFromList(listName, displayLimit)
|
||||
getResourceMoreList(displayLimit)
|
||||
}, [getResourceMoreList])
|
||||
|
||||
console.log('isLoading', isLoading, listToDisplay?.length)
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
showSuccess,
|
||||
} from "../../utils/toast";
|
||||
import { RequestQueueWithPromise } from "../../utils/queue";
|
||||
import { useLibTranslation } from "../../hooks/useLibTranslation";
|
||||
|
||||
export const requestQueueGetStatus = new RequestQueueWithPromise(1);
|
||||
|
||||
@@ -114,6 +115,8 @@ const SubtitleManagerComponent = ({
|
||||
isFromDrawer = false,
|
||||
exitFullscreen,
|
||||
}: SubtitleManagerProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const [mode, setMode] = useState(1);
|
||||
const [isOpenPublish, setIsOpenPublish] = useState(false);
|
||||
const { lists, identifierOperations, auth } = useGlobal();
|
||||
@@ -308,7 +311,7 @@ const SubtitleManagerComponent = ({
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
Subtitles
|
||||
{t("subtitle.subtitles")}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
<ButtonBase
|
||||
@@ -365,7 +368,7 @@ const SubtitleManagerComponent = ({
|
||||
marginTop: "20px",
|
||||
}}
|
||||
>
|
||||
No subtitles
|
||||
{t("subtitle.no_subtitles")}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -393,7 +396,7 @@ const SubtitleManagerComponent = ({
|
||||
disabled={showAll}
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
Load community subs
|
||||
{t("subtitle.load_community_subs")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -424,6 +427,8 @@ const PublisherSubtitles = ({
|
||||
onBack,
|
||||
currentSubTrack,
|
||||
}: PublisherSubtitlesProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonBase
|
||||
@@ -439,7 +444,7 @@ const PublisherSubtitles = ({
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography>Off</Typography>
|
||||
<Typography>{t("subtitle.off")}</Typography>
|
||||
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
|
||||
</ButtonBase>
|
||||
|
||||
@@ -470,6 +475,8 @@ const PublishSubtitles = ({
|
||||
setIsOpen,
|
||||
mySubtitles,
|
||||
}: PublishSubtitlesProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const [language, setLanguage] = useState<null | string>(null);
|
||||
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
|
||||
const [isPublishing, setIsPublishing] = useState(false);
|
||||
@@ -488,7 +495,7 @@ const PublishSubtitles = ({
|
||||
};
|
||||
newSubtitles.push(newSubtitle);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse audio file:", error);
|
||||
console.error("Failed to convert to base64:", error);
|
||||
}
|
||||
}
|
||||
setSubtitles((prev) => [...newSubtitles, ...prev]);
|
||||
@@ -538,11 +545,13 @@ const PublishSubtitles = ({
|
||||
let loadId;
|
||||
try {
|
||||
setIsPublishing(true);
|
||||
loadId = showLoading("Deleting subtitle...");
|
||||
loadId = showLoading(t("subtitle.deleting_subtitle"));
|
||||
await lists.deleteResource([sub]);
|
||||
showSuccess("Deleted subtitle");
|
||||
showSuccess(t("subtitle.deleted"));
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : "Unable to delete");
|
||||
showError(
|
||||
error instanceof Error ? error.message : t("subtitle.unable_delete")
|
||||
);
|
||||
} finally {
|
||||
setIsPublishing(false);
|
||||
dismissToast(loadId);
|
||||
@@ -553,12 +562,14 @@ const PublishSubtitles = ({
|
||||
let loadId;
|
||||
try {
|
||||
setIsPublishing(true);
|
||||
loadId = showLoading("Publishing subtitles...");
|
||||
loadId = showLoading(t("subtitle.publishing"));
|
||||
await publishHandler(subtitles);
|
||||
showSuccess("Subtitles published");
|
||||
showSuccess(t("subtitle.published"));
|
||||
setSubtitles([]);
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : "Unable to publish");
|
||||
showError(
|
||||
error instanceof Error ? error.message : t("subtitle.unable_publish")
|
||||
);
|
||||
} finally {
|
||||
dismissToast(loadId);
|
||||
setIsPublishing(false);
|
||||
@@ -587,7 +598,7 @@ const PublishSubtitles = ({
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>My Subtitles</DialogTitle>
|
||||
<DialogTitle>{t("subtitle.my_subtitles")}</DialogTitle>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={handleClose}
|
||||
@@ -626,8 +637,8 @@ const PublishSubtitles = ({
|
||||
onChange={handleChange}
|
||||
aria-label="basic tabs example"
|
||||
>
|
||||
<Tab label="New" {...a11yProps(0)} />
|
||||
<Tab label="Existing" {...a11yProps(1)} />
|
||||
<Tab label={t("subtitle.new")} {...a11yProps(0)} />
|
||||
<Tab label={t("subtitle.existing")} {...a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -651,7 +662,7 @@ const PublishSubtitles = ({
|
||||
variant="contained"
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
Import subtitles
|
||||
{t("subtitle.import_subtitles")}
|
||||
</Button>
|
||||
</Box>
|
||||
{subtitles?.map((sub, i) => {
|
||||
@@ -697,7 +708,7 @@ const PublishSubtitles = ({
|
||||
size="small"
|
||||
color="secondary"
|
||||
>
|
||||
remove
|
||||
{t("actions.remove")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
@@ -739,7 +750,7 @@ const PublishSubtitles = ({
|
||||
disabled={disableButton}
|
||||
variant="contained"
|
||||
>
|
||||
Publish
|
||||
{t("actions.publish")}
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
@@ -849,6 +860,8 @@ interface MySubtitleProps {
|
||||
onDelete: (subtitle: QortalGetMetadata) => void;
|
||||
}
|
||||
const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const { resource, isLoading, error } = usePublish(2, "JSON", sub);
|
||||
return (
|
||||
<Card
|
||||
@@ -887,7 +900,7 @@ const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
|
||||
size="small"
|
||||
color="secondary"
|
||||
>
|
||||
delete
|
||||
{t("actions.delete")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
@@ -3,9 +3,7 @@ import {
|
||||
Box,
|
||||
ButtonBase,
|
||||
Divider,
|
||||
Fade,
|
||||
IconButton,
|
||||
Popover,
|
||||
Popper,
|
||||
Slider,
|
||||
Typography,
|
||||
@@ -13,11 +11,9 @@ import {
|
||||
} from "@mui/material";
|
||||
export const fontSizeExSmall = "60%";
|
||||
export const fontSizeSmall = "80%";
|
||||
import AspectRatioIcon from "@mui/icons-material/AspectRatio";
|
||||
import {
|
||||
Fullscreen,
|
||||
Pause,
|
||||
PictureInPicture,
|
||||
PlayArrow,
|
||||
Refresh,
|
||||
VolumeOff,
|
||||
@@ -25,17 +21,20 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { formatTime } from "../../utils/time.js";
|
||||
import { CustomFontTooltip } from "./CustomFontTooltip.js";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo";
|
||||
const buttonPaddingBig = "6px";
|
||||
const buttonPaddingSmall = "4px";
|
||||
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
|
||||
import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import { useLibTranslation } from "../../hooks/useLibTranslation.js";
|
||||
|
||||
export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow>
|
||||
<CustomFontTooltip title={t("video.play_pause")} placement="bottom" arrow>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
@@ -50,8 +49,10 @@ export const PlayButton = ({ togglePlay, isPlaying, isScreenSmall }: any) => {
|
||||
};
|
||||
|
||||
export const ReloadButton = ({ reloadVideo, isScreenSmall }: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow>
|
||||
<CustomFontTooltip title={t("video.reload_video")} placement="bottom" arrow>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
@@ -72,7 +73,7 @@ export const ProgressSlider = ({
|
||||
playerRef,
|
||||
resetHideTimeout,
|
||||
isVideoPlayerSmall,
|
||||
isOnTimeline
|
||||
isOnTimeline,
|
||||
}: any) => {
|
||||
const sliderRef = useRef(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
@@ -117,26 +118,25 @@ export const ProgressSlider = ({
|
||||
const debounceTimeoutRef = useRef<any>(null);
|
||||
const previousBlobUrlRef = useRef<string | null>(null);
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
const slider = sliderRef.current;
|
||||
if (!slider) return;
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
const slider = sliderRef.current;
|
||||
if (!slider) return;
|
||||
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percent = x / rect.width;
|
||||
const time = Math.min(Math.max(0, percent * duration), duration);
|
||||
const rect = slider.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const percent = x / rect.width;
|
||||
const time = Math.min(Math.max(0, percent * duration), duration);
|
||||
|
||||
// Position anchor element at the correct spot
|
||||
if (hoverAnchorRef.current) {
|
||||
hoverAnchorRef.current.style.left = `${x}px`;
|
||||
}
|
||||
// Position anchor element at the correct spot
|
||||
if (hoverAnchorRef.current) {
|
||||
hoverAnchorRef.current.style.left = `${x}px`;
|
||||
}
|
||||
|
||||
setHoverX(e.clientX); // optional – can be removed unless used elsewhere
|
||||
setShowDuration(time);
|
||||
|
||||
setHoverX(e.clientX); // optional – can be removed unless used elsewhere
|
||||
setShowDuration(time);
|
||||
|
||||
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
||||
};
|
||||
if (debounceTimeoutRef.current) clearTimeout(debounceTimeoutRef.current);
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
lastRequestedTimeRef.current = null;
|
||||
setThumbnailUrl(null);
|
||||
@@ -166,14 +166,14 @@ const handleMouseMove = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if(!isOnTimeline) return
|
||||
if(hoverX){
|
||||
isOnTimeline.current = true
|
||||
useEffect(() => {
|
||||
if (!isOnTimeline) return;
|
||||
if (hoverX) {
|
||||
isOnTimeline.current = true;
|
||||
} else {
|
||||
isOnTimeline.current = false
|
||||
isOnTimeline.current = false;
|
||||
}
|
||||
}, [hoverX])
|
||||
}, [hoverX]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -183,17 +183,17 @@ const handleMouseMove = (e: React.MouseEvent) => {
|
||||
padding: isVideoPlayerSmall ? "0px" : "0px 10px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
ref={hoverAnchorRef}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
width: "1px",
|
||||
height: "1px",
|
||||
pointerEvents: "none",
|
||||
transform: "translateX(-50%)", // center popper on the anchor
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
ref={hoverAnchorRef}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
width: "1px",
|
||||
height: "1px",
|
||||
pointerEvents: "none",
|
||||
transform: "translateX(-50%)", // center popper on the anchor
|
||||
}}
|
||||
/>
|
||||
<Slider
|
||||
ref={sliderRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
@@ -268,9 +268,11 @@ const handleMouseMove = (e: React.MouseEvent) => {
|
||||
};
|
||||
|
||||
export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<CustomFontTooltip
|
||||
title="Seek video in 10% increments (0-9)"
|
||||
title={t("video.seek_video")}
|
||||
placement="bottom"
|
||||
arrow
|
||||
disableHoverListener={isScreenSmall}
|
||||
@@ -295,12 +297,10 @@ export const VideoTime = ({ progress, isScreenSmall, duration }: any) => {
|
||||
};
|
||||
|
||||
const VolumeButton = ({ isMuted, toggleMute }: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<CustomFontTooltip
|
||||
title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
|
||||
placement="bottom"
|
||||
arrow
|
||||
>
|
||||
<CustomFontTooltip title={t("video.toggle_mute")} placement="bottom" arrow>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
@@ -373,6 +373,8 @@ export const PlaybackRate = ({
|
||||
onSelect,
|
||||
openPlaybackMenu,
|
||||
}: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const btnRef = useRef(null);
|
||||
const theme = useTheme();
|
||||
@@ -383,7 +385,7 @@ export const PlaybackRate = ({
|
||||
return (
|
||||
<>
|
||||
<CustomFontTooltip
|
||||
title="Video Speed. Increase (+ or >), Decrease (- or <)"
|
||||
title={t("video.video_speed")}
|
||||
placement="bottom"
|
||||
arrow
|
||||
>
|
||||
@@ -403,55 +405,15 @@ export const PlaybackRate = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ObjectFitButton = ({ toggleObjectFit, isScreenSmall }: any) => {
|
||||
return (
|
||||
<CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||
}}
|
||||
onClick={() => toggleObjectFit()}
|
||||
>
|
||||
<AspectRatioIcon />
|
||||
</IconButton>
|
||||
</CustomFontTooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const PictureInPictureButton = ({
|
||||
isFullscreen,
|
||||
toggleRef,
|
||||
togglePictureInPicture,
|
||||
isScreenSmall,
|
||||
}: any) => {
|
||||
return (
|
||||
<>
|
||||
{!isFullscreen && (
|
||||
<CustomFontTooltip
|
||||
title="Picture in Picture (P)"
|
||||
placement="bottom"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
|
||||
}}
|
||||
ref={toggleRef}
|
||||
onClick={togglePictureInPicture}
|
||||
>
|
||||
<PictureInPicture />
|
||||
</IconButton>
|
||||
</CustomFontTooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
return (
|
||||
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow>
|
||||
<CustomFontTooltip
|
||||
title={t("video.toggle_fullscreen")}
|
||||
placement="bottom"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
sx={{
|
||||
color: "white",
|
||||
@@ -479,6 +441,8 @@ export const PlayBackMenu = ({
|
||||
playbackRate,
|
||||
isFromDrawer,
|
||||
}: PlayBackMenuProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const theme = useTheme();
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
@@ -539,7 +503,7 @@ export const PlayBackMenu = ({
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
Playback speed
|
||||
{t("video.playback_speed")}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import SubtitlesIcon from "@mui/icons-material/Subtitles";
|
||||
import { CustomFontTooltip } from "./CustomFontTooltip";
|
||||
import { RefObject } from "react";
|
||||
import { useLibTranslation } from "../../hooks/useLibTranslation";
|
||||
import i18n from "../../i18n/i18n";
|
||||
interface VideoControlsBarProps {
|
||||
canPlay: boolean;
|
||||
isScreenSmall: boolean;
|
||||
@@ -71,8 +73,10 @@ export const VideoControlsBar = ({
|
||||
openPlaybackMenu,
|
||||
togglePictureInPicture,
|
||||
isVideoPlayerSmall,
|
||||
isOnTimeline
|
||||
isOnTimeline,
|
||||
}: VideoControlsBarProps) => {
|
||||
const { t } = useLibTranslation();
|
||||
|
||||
const showMobileControls = isScreenSmall && canPlay;
|
||||
|
||||
const controlGroupSX = {
|
||||
@@ -145,8 +149,11 @@ export const VideoControlsBar = ({
|
||||
increaseSpeed={increaseSpeed}
|
||||
decreaseSpeed={decreaseSpeed}
|
||||
/>
|
||||
{/* <ObjectFitButton /> */}
|
||||
<CustomFontTooltip title="Subtitles" placement="bottom" arrow>
|
||||
<CustomFontTooltip
|
||||
title={t("subtitle.subtitles")}
|
||||
placement="bottom"
|
||||
arrow
|
||||
>
|
||||
<IconButton
|
||||
ref={subtitleBtnRef}
|
||||
onClick={openSubtitleManager}
|
||||
@@ -158,7 +165,7 @@ export const VideoControlsBar = ({
|
||||
/>
|
||||
</IconButton>
|
||||
</CustomFontTooltip>
|
||||
{/* <PictureInPictureButton togglePictureInPicture={togglePictureInPicture} /> */}
|
||||
|
||||
<FullscreenButton toggleFullscreen={toggleFullscreen} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -631,10 +631,8 @@ export const VideoPlayer = ({
|
||||
playerRef.current?.playbackRate(playbackRate);
|
||||
playerRef.current?.volume(volume);
|
||||
const key = `${resource.service}-${resource.name}-${resource.identifier}`
|
||||
console.log('key', key)
|
||||
if (key) {
|
||||
const savedProgress = getProgress(key);
|
||||
console.log('savedProgress', savedProgress)
|
||||
if (typeof savedProgress === "number") {
|
||||
playerRef.current?.currentTime(savedProgress);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useProgressStore } from "../state/video";
|
||||
import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
|
||||
import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog";
|
||||
import { useMultiplePublishStore } from "../state/multiplePublish";
|
||||
import { useIframe } from "../hooks/useIframe";
|
||||
|
||||
// ✅ Define Global Context Type
|
||||
interface GlobalContextType {
|
||||
@@ -52,6 +53,8 @@ export const GlobalProvider = ({
|
||||
config,
|
||||
toastStyle = {},
|
||||
}: GlobalProviderProps) => {
|
||||
|
||||
useIframe()
|
||||
// ✅ Call hooks and pass in options dynamically
|
||||
const auth = useAuth(config?.auth || {});
|
||||
const isPublishing = useMultiplePublishStore((s) => s.isPublishing);
|
||||
@@ -94,6 +97,7 @@ export const GlobalProvider = ({
|
||||
|
||||
return (
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
|
||||
{config?.enableGlobalVideoFeature && <GlobalPipPlayer />}
|
||||
|
||||
{isPublishing && <MultiPublishDialog />}
|
||||
@@ -108,6 +112,7 @@ export const GlobalProvider = ({
|
||||
<IndexManager username={auth?.name} />
|
||||
|
||||
{children}
|
||||
|
||||
</GlobalContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
44
src/hooks/useIframe.tsx
Normal file
44
src/hooks/useIframe.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useEffect } from "react";
|
||||
import { supportedLanguages } from "../i18n/i18n";
|
||||
import i18n from '../i18n/i18n'
|
||||
type Language = "de" | "en" | "es" | "fr" | "it" | "ja" | "ru" | "zh";
|
||||
type Theme = "dark" | "light";
|
||||
|
||||
interface CustomWindow extends Window {
|
||||
_qdnTheme: Theme;
|
||||
_qdnLang: Language;
|
||||
}
|
||||
const customWindow = window as unknown as CustomWindow;
|
||||
|
||||
|
||||
export const useIframe = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const languageDefault = customWindow?._qdnLang;
|
||||
|
||||
if (supportedLanguages?.includes(languageDefault)) {
|
||||
i18n.changeLanguage(languageDefault);
|
||||
}
|
||||
|
||||
function handleNavigation(event: {
|
||||
data: {
|
||||
action: string;
|
||||
language: Language;
|
||||
};
|
||||
}) {
|
||||
|
||||
if (event.data?.action === "LANGUAGE_CHANGED" && event.data.language) {
|
||||
if (!supportedLanguages?.includes(event.data.language)) return;
|
||||
|
||||
i18n.changeLanguage(event.data.language);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleNavigation);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("message", handleNavigation);
|
||||
};
|
||||
}, []);
|
||||
return;
|
||||
};
|
||||
7
src/hooks/useLibTranslation.tsx
Normal file
7
src/hooks/useLibTranslation.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
// src/hooks/useLibTranslation.ts
|
||||
import { useTranslation as useTranslationOriginal } from 'react-i18next';
|
||||
import libI18n from '../i18n/i18n'
|
||||
|
||||
export function useLibTranslation(ns?: string | string[]) {
|
||||
return useTranslationOriginal(ns || 'lib-core', { i18n: libI18n });
|
||||
}
|
||||
86
src/i18n/compiled-i18n.json
Normal file
86
src/i18n/compiled-i18n.json
Normal file
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"resources": {
|
||||
"en": {
|
||||
"lib-core": {
|
||||
"subtitle": {
|
||||
"subtitles": "Subtitles",
|
||||
"no_subtitles": "No subtitles",
|
||||
"load_community_subs": "Load community subtitles",
|
||||
"off": "Off",
|
||||
"deleting_subtitle": "Deleting subtitle...",
|
||||
"deleted": "Deleted subtitle",
|
||||
"unable_delete": "unable to delete",
|
||||
"publishing": "Publishing subtitles...",
|
||||
"published": "Subtitles published",
|
||||
"unable_publish": "Unable to publish",
|
||||
"my_subtitles": "My Subtitles",
|
||||
"new": "New",
|
||||
"existing": "Existing",
|
||||
"import_subtitles": "Import subtitles"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "remove",
|
||||
"publish": "publish",
|
||||
"delete": "delete",
|
||||
"publish_metadata": "publish metadata",
|
||||
"publish_index": "publish index",
|
||||
"cancel": "cancel",
|
||||
"continue": "continue",
|
||||
"close": "close",
|
||||
"retry": "retry"
|
||||
},
|
||||
"video": {
|
||||
"playback_speed": "Playback speed",
|
||||
"toggle_fullscreen": "Toggle Fullscreen (F)",
|
||||
"video_speed": "Video Speed. Increase (+ or >), Decrease (- or <)",
|
||||
"toggle_mute": "Toggle Mute (M), Raise (UP), Lower (DOWN)",
|
||||
"seek_video": "Seek video in 10% increments (0-9)",
|
||||
"reload_video": "Reload Video (R)",
|
||||
"play_pause": "Pause/Play (Spacebar)"
|
||||
},
|
||||
"index": {
|
||||
"title": "Index Manager",
|
||||
"create_new_index": "Create new index",
|
||||
"add_metadata": "Add metadata",
|
||||
"publishing_metadata": "Publishing metadata...",
|
||||
"published_metadata": "Successfully published metadata",
|
||||
"failed_metadata": "Failed to publish metadata",
|
||||
"example": "Example of how it could look like:",
|
||||
"metadata_title": "Title",
|
||||
"metadata_description": "Description",
|
||||
"metadata_title_placeholder": "Add a title for the link",
|
||||
"metadata_description_placeholder": "Add a description for the link",
|
||||
"characters": "characters",
|
||||
"publishing_index": "Publishing index...",
|
||||
"published_index": "Successfully published index",
|
||||
"failed_index": "Failed to publish index",
|
||||
"recommended_indices": "Recommended Indices",
|
||||
"add_search_term": "Add search term",
|
||||
"search_terms": "search terms",
|
||||
"recommendation_size": "It is recommended to keep your term character count below {{recommendedSize}} characters",
|
||||
"multiple_title": "Adding multiple indices",
|
||||
"multiple_description": "Subsequent indices will keep your publish fees lower, but they will have less strength in future search results."
|
||||
},
|
||||
"multi_publish": {
|
||||
"title": "Publishing Status",
|
||||
"publish_failed": "Publish Failed",
|
||||
"file_chunk": "File Chunk",
|
||||
"file_processing": "File Processing",
|
||||
"success": "Published successfully",
|
||||
"attempt_retry": "Publish failed. Attempting retry..."
|
||||
}
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"lib-core": {
|
||||
"subtitle": {
|
||||
"subtitles": "subtitles es"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"supportedLanguages": [
|
||||
"en",
|
||||
"es"
|
||||
]
|
||||
}
|
||||
27
src/i18n/i18n.ts
Normal file
27
src/i18n/i18n.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createInstance } from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import compiled from './compiled-i18n.json';
|
||||
|
||||
export const supportedLanguages = compiled.supportedLanguages;
|
||||
|
||||
const libI18n = createInstance(); // ✅ this avoids conflict with consumer app
|
||||
|
||||
libI18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: compiled.resources,
|
||||
supportedLngs: compiled.supportedLanguages,
|
||||
fallbackLng: 'en',
|
||||
lng: typeof navigator !== 'undefined' ? navigator.language : 'en',
|
||||
defaultNS: 'lib-core',
|
||||
ns: ['lib-core'],
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
debug: false,
|
||||
});
|
||||
|
||||
export default libI18n;
|
||||
70
src/i18n/locales/en/lib-core.json
Normal file
70
src/i18n/locales/en/lib-core.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"subtitle": {
|
||||
"subtitles": "Subtitles",
|
||||
"no_subtitles": "No subtitles",
|
||||
"load_community_subs": "Load community subtitles",
|
||||
"off": "Off",
|
||||
"deleting_subtitle": "Deleting subtitle...",
|
||||
"deleted": "Deleted subtitle",
|
||||
"unable_delete": "unable to delete",
|
||||
"publishing": "Publishing subtitles...",
|
||||
"published": "Subtitles published",
|
||||
"unable_publish": "Unable to publish",
|
||||
"my_subtitles": "My Subtitles",
|
||||
"new": "New",
|
||||
"existing": "Existing",
|
||||
"import_subtitles": "Import subtitles"
|
||||
},
|
||||
"actions": {
|
||||
"remove": "remove",
|
||||
"publish": "publish",
|
||||
"delete": "delete",
|
||||
"publish_metadata": "publish metadata",
|
||||
"publish_index": "publish index",
|
||||
"cancel": "cancel",
|
||||
"continue": "continue",
|
||||
"close": "close",
|
||||
"retry": "retry"
|
||||
},
|
||||
"video": {
|
||||
"playback_speed": "Playback speed",
|
||||
"toggle_fullscreen": "Toggle Fullscreen (F)",
|
||||
"video_speed": "Video Speed. Increase (+ or >), Decrease (- or <)",
|
||||
"toggle_mute": "Toggle Mute (M), Raise (UP), Lower (DOWN)",
|
||||
"seek_video": "Seek video in 10% increments (0-9)",
|
||||
"reload_video": "Reload Video (R)",
|
||||
"play_pause": "Pause/Play (Spacebar)"
|
||||
},
|
||||
"index": {
|
||||
"title": "Index Manager",
|
||||
"create_new_index": "Create new index",
|
||||
"add_metadata": "Add metadata",
|
||||
"publishing_metadata": "Publishing metadata...",
|
||||
"published_metadata": "Successfully published metadata",
|
||||
"failed_metadata": "Failed to publish metadata",
|
||||
"example": "Example of how it could look like:",
|
||||
"metadata_title": "Title",
|
||||
"metadata_description": "Description",
|
||||
"metadata_title_placeholder": "Add a title for the link",
|
||||
"metadata_description_placeholder": "Add a description for the link",
|
||||
"characters": "characters",
|
||||
"publishing_index": "Publishing index...",
|
||||
"published_index": "Successfully published index",
|
||||
"failed_index": "Failed to publish index",
|
||||
"recommended_indices": "Recommended Indices",
|
||||
"add_search_term": "Add search term",
|
||||
"search_terms": "search terms",
|
||||
"recommendation_size": "It is recommended to keep your term character count below {{recommendedSize}} characters",
|
||||
"multiple_title": "Adding multiple indices",
|
||||
"multiple_description": "Subsequent indices will keep your publish fees lower, but they will have less strength in future search results."
|
||||
|
||||
},
|
||||
"multi_publish": {
|
||||
"title": "Publishing Status",
|
||||
"publish_failed": "Publish Failed",
|
||||
"file_chunk": "File Chunk",
|
||||
"file_processing": "File Processing",
|
||||
"success": "Published successfully",
|
||||
"attempt_retry": "Publish failed. Attempting retry..."
|
||||
}
|
||||
}
|
||||
5
src/i18n/locales/es/lib-core.json
Normal file
5
src/i18n/locales/es/lib-core.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"subtitle": {
|
||||
"subtitles": "subtitles es"
|
||||
}
|
||||
}
|
||||
53
src/i18n/processors.ts
Normal file
53
src/i18n/processors.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export const capitalizeAll = {
|
||||
type: 'postProcessor',
|
||||
name: 'capitalizeAll',
|
||||
process: (value: string) => value.toUpperCase(),
|
||||
};
|
||||
|
||||
export const capitalizeEachFirstChar = {
|
||||
type: 'postProcessor',
|
||||
name: 'capitalizeEachFirstChar',
|
||||
process: (value: string) => {
|
||||
if (!value?.trim()) return value;
|
||||
|
||||
const leadingSpaces = value.match(/^\s*/)?.[0] || '';
|
||||
const trailingSpaces = value.match(/\s*$/)?.[0] || '';
|
||||
|
||||
const core = value
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.map(
|
||||
(word) =>
|
||||
word.charAt(0).toLocaleUpperCase() + word.slice(1).toLocaleLowerCase()
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
return leadingSpaces + core + trailingSpaces;
|
||||
},
|
||||
};
|
||||
export const capitalizeFirstChar = {
|
||||
type: 'postProcessor',
|
||||
name: 'capitalizeFirstChar',
|
||||
process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1),
|
||||
};
|
||||
|
||||
export const capitalizeFirstWord = {
|
||||
type: 'postProcessor',
|
||||
name: 'capitalizeFirstWord',
|
||||
process: (value: string) => {
|
||||
if (!value?.trim()) return value;
|
||||
|
||||
const trimmed = value.trimStart();
|
||||
const firstSpaceIndex = trimmed.indexOf(' ');
|
||||
|
||||
if (firstSpaceIndex === -1) {
|
||||
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
|
||||
}
|
||||
|
||||
const firstWord = trimmed.slice(0, firstSpaceIndex);
|
||||
const restOfString = trimmed.slice(firstSpaceIndex);
|
||||
const trailingSpaces = value.slice(trimmed.length);
|
||||
|
||||
return firstWord.toUpperCase() + restOfString + trailingSpaces;
|
||||
},
|
||||
};
|
||||
@@ -196,7 +196,7 @@ startGlobalDownload: (
|
||||
let isPaused = false
|
||||
const callFunction = async (build?: boolean, isRecalling?: boolean) => {
|
||||
try {
|
||||
console.log('retryAttempts', retryAttempts, tries)
|
||||
|
||||
if ((isCalling || isPaused) && !build) return;
|
||||
isCalling = true;
|
||||
statusMap[resourceId] = getResourceStatus(resourceId);
|
||||
|
||||
Reference in New Issue
Block a user