mirror of
				https://github.com/Qortal/qapp-core.git
				synced 2025-11-04 05:57: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,7 +118,7 @@ export const ProgressSlider = ({
 | 
			
		||||
  const debounceTimeoutRef = useRef<any>(null);
 | 
			
		||||
  const previousBlobUrlRef = useRef<string | null>(null);
 | 
			
		||||
 | 
			
		||||
const handleMouseMove = (e: React.MouseEvent) => {
 | 
			
		||||
  const handleMouseMove = (e: React.MouseEvent) => {
 | 
			
		||||
    const slider = sliderRef.current;
 | 
			
		||||
    if (!slider) return;
 | 
			
		||||
 | 
			
		||||
@@ -134,9 +135,8 @@ const handleMouseMove = (e: React.MouseEvent) => {
 | 
			
		||||
    setHoverX(e.clientX); // optional – can be removed unless used elsewhere
 | 
			
		||||
    setShowDuration(time);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
@@ -193,7 +193,7 @@ const handleMouseMove = (e: React.MouseEvent) => {
 | 
			
		||||
          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 FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
 | 
			
		||||
  const { t } = useLibTranslation();
 | 
			
		||||
 | 
			
		||||
export const PictureInPictureButton = ({
 | 
			
		||||
  isFullscreen,
 | 
			
		||||
  toggleRef,
 | 
			
		||||
  togglePictureInPicture,
 | 
			
		||||
  isScreenSmall,
 | 
			
		||||
}: any) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {!isFullscreen && (
 | 
			
		||||
    <CustomFontTooltip
 | 
			
		||||
          title="Picture in Picture (P)"
 | 
			
		||||
      title={t("video.toggle_fullscreen")}
 | 
			
		||||
      placement="bottom"
 | 
			
		||||
      arrow
 | 
			
		||||
    >
 | 
			
		||||
          <IconButton
 | 
			
		||||
            sx={{
 | 
			
		||||
              color: "white",
 | 
			
		||||
              padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
 | 
			
		||||
            }}
 | 
			
		||||
            ref={toggleRef}
 | 
			
		||||
            onClick={togglePictureInPicture}
 | 
			
		||||
          >
 | 
			
		||||
            <PictureInPicture />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
        </CustomFontTooltip>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const FullscreenButton = ({ toggleFullscreen, isScreenSmall }: any) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <CustomFontTooltip title="Toggle Fullscreen (F)" 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