fix loading

This commit is contained in:
2025-07-30 23:09:48 +03:00
parent 027263af71
commit c15032aeca
6 changed files with 330 additions and 285 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "qapp-core",
"version": "1.0.49",
"version": "1.0.51",
"description": "Qortal's core React library with global state, UI components, and utilities",
"main": "dist/index.js",
"module": "dist/index.mjs",

View File

@@ -18,6 +18,7 @@ interface LoadingVideoProps {
togglePlay: () => void;
startPlay: boolean;
downloadResource: () => void;
isStatusWrong: boolean
}
export const LoadingVideo = ({
status,
@@ -27,6 +28,7 @@ export const LoadingVideo = ({
togglePlay,
startPlay,
downloadResource,
isStatusWrong
}: LoadingVideoProps) => {
const getDownloadProgress = (percentLoaded: number) => {
const progress = percentLoaded;
@@ -80,7 +82,7 @@ export const LoadingVideo = ({
<> Refetching in 10 seconds</>
</>
) : status === "DOWNLOADED" ? (
) : (status === "DOWNLOADED" && !isStatusWrong) ? (
<>Download Completed: building video...</>
) : status === "FAILED_TO_DOWNLOAD" ? (
<>Unable to fetch video chunks from peers</>

View File

@@ -196,6 +196,7 @@ export const VideoPlayer = ({
seekTo,
togglePictureInPicture,
downloadResource,
isStatusWrong
} = useVideoPlayerController({
autoPlay,
playerRef,
@@ -357,7 +358,7 @@ export const VideoPlayer = ({
if (onPlayParent) {
onPlayParent();
}
}, [setIsPlaying]);
}, [setIsPlaying, onPlayParent]);
const onPause = useCallback(() => {
setIsPlaying(false);
@@ -762,6 +763,7 @@ export const VideoPlayer = ({
isLoading={isLoading}
startPlay={startPlay}
downloadResource={downloadResource}
isStatusWrong={isStatusWrong}
/>
<VideoElement
ref={videoRef}

View File

@@ -43,7 +43,7 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
const startedFetchRef = useRef(false);
const { playbackSettings, setPlaybackRate } = useVideoStore();
const { isReady, resourceUrl, status, percentLoaded, downloadResource } =
const { isReady, resourceUrl, status, localChunkCount, totalChunkCount , percentLoaded, downloadResource } =
useResourceStatus({
resource: !startedFetch ? null : qortalVideoResource,
retryAttempts,
@@ -304,5 +304,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
seekTo,
togglePictureInPicture,
downloadResource,
isStatusWrong: !isNaN(totalChunkCount) && !isNaN(localChunkCount) && totalChunkCount === 2 && (totalChunkCount < localChunkCount)
};
};

View File

@@ -71,77 +71,6 @@
}
}
},
"de": {
"lib-core": {
"subtitle": {
"subtitles": "Untertitel",
"no_subtitles": "Keine Untertitel",
"load_community_subs": "Community-Untertitel laden",
"off": "Aus",
"deleting_subtitle": "Untertitel wird gelöscht...",
"deleted": "Untertitel gelöscht",
"unable_delete": "Löschen nicht möglich",
"publishing": "Untertitel werden veröffentlicht...",
"published": "Untertitel veröffentlicht",
"unable_publish": "Veröffentlichung nicht möglich",
"my_subtitles": "Meine Untertitel",
"new": "Neu",
"existing": "Vorhanden",
"import_subtitles": "Untertitel importieren"
},
"actions": {
"remove": "entfernen",
"publish": "veröffentlichen",
"delete": "löschen",
"publish_metadata": "Metadaten veröffentlichen",
"publish_index": "Index veröffentlichen",
"cancel": "abbrechen",
"continue": "fortfahren",
"close": "schließen",
"retry": "wiederholen"
},
"video": {
"playback_speed": "Wiedergabegeschwindigkeit",
"toggle_fullscreen": "Vollbild umschalten (F)",
"video_speed": "Video-Geschwindigkeit. Erhöhen (+ oder >), Verringern (- oder <)",
"toggle_mute": "Stummschalten umschalten (M), Lauter (OBEN), Leiser (UNTEN)",
"seek_video": "Video in 10%-Schritten vorspulen (09)",
"reload_video": "Video neu laden (R)",
"play_pause": "Wiedergabe/Pause (Leertaste)"
},
"index": {
"title": "Index-Manager",
"create_new_index": "Neuen Index erstellen",
"add_metadata": "Metadaten hinzufügen",
"publishing_metadata": "Metadaten werden veröffentlicht...",
"published_metadata": "Metadaten erfolgreich veröffentlicht",
"failed_metadata": "Veröffentlichung der Metadaten fehlgeschlagen",
"example": "Beispiel, wie es aussehen könnte:",
"metadata_title": "Titel",
"metadata_description": "Beschreibung",
"metadata_title_placeholder": "Titel für den Link hinzufügen",
"metadata_description_placeholder": "Beschreibung für den Link hinzufügen",
"characters": "Zeichen",
"publishing_index": "Index wird veröffentlicht...",
"published_index": "Index erfolgreich veröffentlicht",
"failed_index": "Veröffentlichung des Index fehlgeschlagen",
"recommended_indices": "Empfohlene Indizes",
"add_search_term": "Suchbegriff hinzufügen",
"search_terms": "Suchbegriffe",
"recommendation_size": "Es wird empfohlen, die Zeichenanzahl pro Begriff unter {{recommendedSize}} Zeichen zu halten",
"multiple_title": "Mehrere Indizes hinzufügen",
"multiple_description": "Weitere Indizes senken die Veröffentlichungsgebühren, haben aber weniger Gewicht in zukünftigen Suchergebnissen."
},
"multi_publish": {
"title": "Veröffentlichungsstatus",
"publish_failed": "Veröffentlichung fehlgeschlagen",
"file_chunk": "Dateifragment",
"file_processing": "Datei wird verarbeitet",
"success": "Erfolgreich veröffentlicht",
"attempt_retry": "Veröffentlichung fehlgeschlagen. Erneuter Versuch..."
}
}
},
"en": {
"lib-core": {
"subtitle": {
@@ -213,6 +142,77 @@
}
}
},
"de": {
"lib-core": {
"subtitle": {
"subtitles": "Untertitel",
"no_subtitles": "Keine Untertitel",
"load_community_subs": "Community-Untertitel laden",
"off": "Aus",
"deleting_subtitle": "Untertitel wird gelöscht...",
"deleted": "Untertitel gelöscht",
"unable_delete": "Löschen nicht möglich",
"publishing": "Untertitel werden veröffentlicht...",
"published": "Untertitel veröffentlicht",
"unable_publish": "Veröffentlichung nicht möglich",
"my_subtitles": "Meine Untertitel",
"new": "Neu",
"existing": "Vorhanden",
"import_subtitles": "Untertitel importieren"
},
"actions": {
"remove": "entfernen",
"publish": "veröffentlichen",
"delete": "löschen",
"publish_metadata": "Metadaten veröffentlichen",
"publish_index": "Index veröffentlichen",
"cancel": "abbrechen",
"continue": "fortfahren",
"close": "schließen",
"retry": "wiederholen"
},
"video": {
"playback_speed": "Wiedergabegeschwindigkeit",
"toggle_fullscreen": "Vollbild umschalten (F)",
"video_speed": "Video-Geschwindigkeit. Erhöhen (+ oder >), Verringern (- oder <)",
"toggle_mute": "Stummschalten umschalten (M), Lauter (OBEN), Leiser (UNTEN)",
"seek_video": "Video in 10%-Schritten vorspulen (09)",
"reload_video": "Video neu laden (R)",
"play_pause": "Wiedergabe/Pause (Leertaste)"
},
"index": {
"title": "Index-Manager",
"create_new_index": "Neuen Index erstellen",
"add_metadata": "Metadaten hinzufügen",
"publishing_metadata": "Metadaten werden veröffentlicht...",
"published_metadata": "Metadaten erfolgreich veröffentlicht",
"failed_metadata": "Veröffentlichung der Metadaten fehlgeschlagen",
"example": "Beispiel, wie es aussehen könnte:",
"metadata_title": "Titel",
"metadata_description": "Beschreibung",
"metadata_title_placeholder": "Titel für den Link hinzufügen",
"metadata_description_placeholder": "Beschreibung für den Link hinzufügen",
"characters": "Zeichen",
"publishing_index": "Index wird veröffentlicht...",
"published_index": "Index erfolgreich veröffentlicht",
"failed_index": "Veröffentlichung des Index fehlgeschlagen",
"recommended_indices": "Empfohlene Indizes",
"add_search_term": "Suchbegriff hinzufügen",
"search_terms": "Suchbegriffe",
"recommendation_size": "Es wird empfohlen, die Zeichenanzahl pro Begriff unter {{recommendedSize}} Zeichen zu halten",
"multiple_title": "Mehrere Indizes hinzufügen",
"multiple_description": "Weitere Indizes senken die Veröffentlichungsgebühren, haben aber weniger Gewicht in zukünftigen Suchergebnissen."
},
"multi_publish": {
"title": "Veröffentlichungsstatus",
"publish_failed": "Veröffentlichung fehlgeschlagen",
"file_chunk": "Dateifragment",
"file_processing": "Datei wird verarbeitet",
"success": "Erfolgreich veröffentlicht",
"attempt_retry": "Veröffentlichung fehlgeschlagen. Erneuter Versuch..."
}
}
},
"es": {
"lib-core": {
"subtitle": {
@@ -642,8 +642,8 @@
},
"supportedLanguages": [
"ar",
"de",
"en",
"de",
"es",
"fr",
"it",

View File

@@ -1,28 +1,31 @@
import { create } from "zustand";
import { QortalGetMetadata, QortalMetadata } from "../types/interfaces/resources";
import {
QortalGetMetadata,
QortalMetadata,
} from "../types/interfaces/resources";
import { persist } from "zustand/middleware";
interface SearchCache {
[listName: string]: {
searches: {
[searchTerm: string]: QortalMetadata[]; // List of products for each search term
};
temporaryNewResources: QortalMetadata[],
temporaryNewResources: QortalMetadata[];
expiry: number; // Expiry timestamp for the whole list
searchParamsStringified: string;
};
}
export const mergeUniqueItems = (array1: QortalMetadata[], array2: QortalMetadata[]) => {
export const mergeUniqueItems = (
array1: QortalMetadata[],
array2: QortalMetadata[]
) => {
const mergedArray = [...array1, ...array2];
// Use a Map to ensure uniqueness based on `identifier-name`
const uniqueMap = new Map();
mergedArray.forEach(item => {
mergedArray.forEach((item) => {
if (item.identifier && item.name && item.service) {
const key = `${item.service}-${item.name}-${item.identifier}`;
uniqueMap.set(key, item);
@@ -33,241 +36,278 @@ export const mergeUniqueItems = (array1: QortalMetadata[], array2: QortalMetadat
};
export interface ListItem {
data: any
qortalMetadata: QortalMetadata
data: any;
qortalMetadata: QortalMetadata;
}
interface resourceCache {
[id: string]: {
data: ListItem | false | null; // Cached resource data
expiry: number; // Expiry timestamp in milliseconds
};
}
[id: string]: {
data: ListItem | false | null; // Cached resource data
expiry: number; // Expiry timestamp in milliseconds
};
}
interface DeletedResources {
[key: string]: { deleted: true; expiry: number }; // ✅ Added expiry field
}
interface DeletedResources {
[key: string]: { deleted: true; expiry: number }; // ✅ Added expiry field
}
interface CacheState {
resourceCache: resourceCache;
searchCache: SearchCache;
// Search cache actions
setResourceCache: (id: string, data: ListItem | false | null, customExpiry?: number) => void;
setResourceCache: (
id: string,
data: ListItem | false | null,
customExpiry?: number
) => void;
setSearchCache: (listName: string, searchTerm: string, data: QortalMetadata[], searchParamsStringified: string | null, customExpiry?: number) => void;
setSearchParamsForList: (ListName: string, searchParamsStringified: string)=> void;
getSearchCache: (listName: string, searchTerm: string) => QortalMetadata[] | null;
setSearchCache: (
listName: string,
searchTerm: string,
data: QortalMetadata[],
searchParamsStringified: string | null,
customExpiry?: number
) => void;
setSearchParamsForList: (
ListName: string,
searchParamsStringified: string
) => void;
getSearchCache: (
listName: string,
searchTerm: string
) => QortalMetadata[] | null;
clearExpiredCache: () => void;
getResourceCache: (id: string, ignoreExpire?: boolean) => ListItem | false | null;
addTemporaryResource: (listName: string, newResources: QortalMetadata[], customExpiry?: number)=> void;
getTemporaryResources:(listName: string)=> QortalMetadata[]
getResourceCache: (
id: string,
ignoreExpire?: boolean
) => ListItem | false | null;
addTemporaryResource: (
listName: string,
newResources: QortalMetadata[],
customExpiry?: number
) => void;
getTemporaryResources: (listName: string) => QortalMetadata[];
deletedResources: DeletedResources;
markResourceAsDeleted: (item: QortalMetadata | QortalGetMetadata) => void;
filterOutDeletedResources: (items: QortalMetadata[]) => QortalMetadata[];
isListExpired: (listName: string)=> boolean | string;
isListExpired: (listName: string) => boolean | string;
searchCacheExpiryDuration: number;
resourceCacheExpiryDuration: number;
setSearchCacheExpiryDuration: (duration: number) => void;
setResourceCacheExpiryDuration: (duration: number)=> void;
setResourceCacheExpiryDuration: (duration: number) => void;
deleteSearchCache: (listName: string) => void;
filterSearchCacheItemsByNames: (names: string[]) => void;
}
export const useCacheStore = create<CacheState>
((set, get) => ({
searchCacheExpiryDuration: 5 * 60 * 1000,
resourceCacheExpiryDuration: 30 * 60 * 1000,
resourceCache: {},
searchCache: {},
deletedResources: {},
setSearchCacheExpiryDuration: (duration) => set({ searchCacheExpiryDuration: duration }),
setResourceCacheExpiryDuration: (duration) => set({ resourceCacheExpiryDuration: duration }),
getResourceCache: (id, ignoreExpire) => {
const cache = get().resourceCache[id];
if (cache) {
if (cache.expiry > Date.now() || ignoreExpire) {
return cache.data; // ✅ Return data if not expired
} else {
set((state) => {
const updatedCache = { ...state.resourceCache };
delete updatedCache[id]; // ✅ Remove expired entry
return { resourceCache: updatedCache };
});
}
}
return null;
},
setResourceCache: (id, data, customExpiry) =>
export const useCacheStore = create<CacheState>((set, get) => ({
searchCacheExpiryDuration: 5 * 60 * 1000,
resourceCacheExpiryDuration: 30 * 60 * 1000,
resourceCache: {},
searchCache: {},
deletedResources: {},
setSearchCacheExpiryDuration: (duration) =>
set({ searchCacheExpiryDuration: duration }),
setResourceCacheExpiryDuration: (duration) =>
set({ resourceCacheExpiryDuration: duration }),
getResourceCache: (id, ignoreExpire) => {
const cache = get().resourceCache[id];
if (cache) {
if (cache.expiry > Date.now() || ignoreExpire) {
return cache.data; // ✅ Return data if not expired
} else {
set((state) => {
const expiry = Date.now() + (customExpiry || get().resourceCacheExpiryDuration);
return {
resourceCache: {
...state.resourceCache,
[id]: { data, expiry },
},
};
}),
const updatedCache = { ...state.resourceCache };
delete updatedCache[id]; // ✅ Remove expired entry
return { resourceCache: updatedCache };
});
}
}
return null;
},
setSearchCache: (listName, searchTerm, data, searchParamsStringified, customExpiry) =>
set((state) => {
const expiry = Date.now() + (customExpiry || get().searchCacheExpiryDuration);
return {
searchCache: {
...state.searchCache,
[listName]: {
searches: {
...(state.searchCache[listName]?.searches || {}),
[searchTerm]: data,
},
temporaryNewResources: state.searchCache[listName]?.temporaryNewResources || [],
expiry,
searchParamsStringified: searchParamsStringified === null ? state.searchCache[listName]?.searchParamsStringified : searchParamsStringified
},
},
};
}),
deleteSearchCache: (listName) =>
set((state) => {
const updatedSearchCache = { ...state.searchCache };
delete updatedSearchCache[listName];
return { searchCache: updatedSearchCache };
}),
setSearchParamsForList: (listName, searchParamsStringified) =>
set((state) => {
const existingList = state.searchCache[listName] || {};
return {
searchCache: {
...state.searchCache,
[listName]: {
...existingList,
searchParamsStringified,
},
},
};
}),
getSearchCache: (listName, searchTerm) => {
const cache = get().searchCache[listName];
if (cache) {
if (cache.expiry > Date.now()) {
return cache.searches[searchTerm] || null; // ✅ Return if valid
} else {
set((state) => {
const updatedCache = { ...state.searchCache };
delete updatedCache[listName]; // ✅ Remove expired list
return { searchCache: updatedCache };
});
}
}
return null;
setResourceCache: (id, data, customExpiry) =>
set((state) => {
const expiry =
Date.now() + (customExpiry || get().resourceCacheExpiryDuration);
return {
resourceCache: {
...state.resourceCache,
[id]: { data, expiry },
},
};
}),
addTemporaryResource: (listName, newResources, customExpiry) =>
set((state) => {
setSearchCache: (
listName,
searchTerm,
data,
searchParamsStringified,
customExpiry
) =>
set((state) => {
const expiry =
Date.now() + (customExpiry || get().searchCacheExpiryDuration);
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000);
const existingResources = state.searchCache[listName]?.temporaryNewResources || [];
// Merge & remove duplicates, keeping the latest by `created` timestamp
const uniqueResourcesMap = new Map<string, QortalMetadata>();
[...existingResources, ...newResources].forEach((item) => {
const key = `${item.service}-${item.name}-${item.identifier}`;
const existingItem = uniqueResourcesMap.get(key);
if (!existingItem || item.created > existingItem.created) {
uniqueResourcesMap.set(key, item);
}
});
return {
searchCache: {
...state.searchCache,
[listName]: {
...state.searchCache[listName],
temporaryNewResources: Array.from(uniqueResourcesMap.values()),
expiry,
},
return {
searchCache: {
...state.searchCache,
[listName]: {
searches: {
...(state.searchCache[listName]?.searches || {}),
[searchTerm]: data,
},
};
}),
temporaryNewResources:
state.searchCache[listName]?.temporaryNewResources || [],
expiry,
searchParamsStringified:
searchParamsStringified === null
? state.searchCache[listName]?.searchParamsStringified
: searchParamsStringified,
},
},
};
}),
deleteSearchCache: (listName) =>
set((state) => {
const updatedSearchCache = { ...state.searchCache };
delete updatedSearchCache[listName];
return { searchCache: updatedSearchCache };
}),
setSearchParamsForList: (listName, searchParamsStringified) =>
set((state) => {
const existingList = state.searchCache[listName] || {};
getTemporaryResources: (listName: string) => {
const cache = get().searchCache[listName];
if (cache && cache.expiry > Date.now()) {
return cache.temporaryNewResources || [];
return {
searchCache: {
...state.searchCache,
[listName]: {
...existingList,
searchParamsStringified,
},
},
};
}),
getSearchCache: (listName, searchTerm) => {
const cache = get().searchCache[listName];
if (cache) {
if (cache.expiry > Date.now()) {
return cache.searches[searchTerm] || null; // ✅ Return if valid
} else {
set((state) => {
const updatedCache = { ...state.searchCache };
delete updatedCache[listName]; // ✅ Remove expired list
return { searchCache: updatedCache };
});
}
}
return null;
},
addTemporaryResource: (listName, newResources, customExpiry) =>
set((state) => {
const expiry = Date.now() + (customExpiry || 5 * 60 * 1000);
const existingResources =
state.searchCache[listName]?.temporaryNewResources || [];
// Merge & remove duplicates, keeping the latest by `created` timestamp
const uniqueResourcesMap = new Map<string, QortalMetadata>();
[...existingResources, ...newResources].forEach((item) => {
const key = `${item.service}-${item.name}-${item.identifier}`;
const existingItem = uniqueResourcesMap.get(key);
if (!existingItem || item.created > existingItem.created) {
uniqueResourcesMap.set(key, item);
}
return [];
},
});
markResourceAsDeleted: (item) =>
set((state) => {
const now = Date.now();
const expiry = now + 5 * 60 * 1000; // ✅ Expires in 5 minutes
// ✅ Remove expired deletions before adding a new one
const validDeletedResources = Object.fromEntries(
Object.entries(state.deletedResources).filter(([_, value]) => value.expiry > now)
return {
searchCache: {
...state.searchCache,
[listName]: {
...state.searchCache[listName],
temporaryNewResources: Array.from(uniqueResourcesMap.values()),
expiry,
},
},
};
}),
getTemporaryResources: (listName: string) => {
const cache = get().searchCache[listName];
if (cache && cache.expiry > Date.now()) {
const resources = cache.temporaryNewResources || [];
return [...resources].sort((a, b) => b?.created - a?.created);
}
return [];
},
markResourceAsDeleted: (item) =>
set((state) => {
const now = Date.now();
const expiry = now + 5 * 60 * 1000; // ✅ Expires in 5 minutes
// ✅ Remove expired deletions before adding a new one
const validDeletedResources = Object.fromEntries(
Object.entries(state.deletedResources).filter(
([_, value]) => value.expiry > now
)
);
const key = `${item.service}-${item.name}-${item.identifier}`;
return {
deletedResources: {
...validDeletedResources, // ✅ Keep only non-expired ones
[key]: { deleted: true, expiry },
},
};
}),
filterOutDeletedResources: (items) => {
const deletedResources = get().deletedResources; // ✅ Read without modifying store
return items.filter(
(item) =>
!deletedResources[`${item.service}-${item.name}-${item.identifier}`]
);
},
isListExpired: (listName: string): boolean | string => {
const cache = get().searchCache[listName];
const isExpired = cache ? cache.expiry <= Date.now() : true; // ✅ Expired if expiry timestamp is in the past
return isExpired === true ? true : cache.searchParamsStringified;
},
clearExpiredCache: () =>
set((state) => {
const now = Date.now();
const validSearchCache = Object.fromEntries(
Object.entries(state.searchCache).filter(
([, value]) => value.expiry > now
)
);
return { searchCache: validSearchCache };
}),
filterSearchCacheItemsByNames: (names) =>
set((state) => {
const updatedSearchCache: SearchCache = {};
for (const [listName, list] of Object.entries(state.searchCache)) {
const updatedSearches: { [searchTerm: string]: QortalMetadata[] } = {};
for (const [term, items] of Object.entries(list.searches)) {
updatedSearches[term] = items.filter(
(item) => !names.includes(item.name)
);
const key = `${item.service}-${item.name}-${item.identifier}`;
return {
deletedResources: {
...validDeletedResources, // ✅ Keep only non-expired ones
[key]: { deleted: true, expiry },
},
};
}),
filterOutDeletedResources: (items) => {
const deletedResources = get().deletedResources; // ✅ Read without modifying store
return items.filter(
(item) => !deletedResources[`${item.service}-${item.name}-${item.identifier}`]
);
},
isListExpired: (listName: string): boolean | string => {
const cache = get().searchCache[listName];
const isExpired = cache ? cache.expiry <= Date.now() : true; // ✅ Expired if expiry timestamp is in the past
return isExpired === true ? true : cache.searchParamsStringified
},
}
clearExpiredCache: () =>
set((state) => {
const now = Date.now();
const validSearchCache = Object.fromEntries(
Object.entries(state.searchCache).filter(([, value]) => value.expiry > now)
);
return { searchCache: validSearchCache };
}),
filterSearchCacheItemsByNames: (names) =>
set((state) => {
const updatedSearchCache: SearchCache = {};
for (const [listName, list] of Object.entries(state.searchCache)) {
const updatedSearches: { [searchTerm: string]: QortalMetadata[] } = {};
for (const [term, items] of Object.entries(list.searches)) {
updatedSearches[term] = items.filter(
(item) => !names.includes(item.name)
);
updatedSearchCache[listName] = {
...list,
searches: updatedSearches,
};
}
updatedSearchCache[listName] = {
...list,
searches: updatedSearches,
};
}
return { searchCache: updatedSearchCache };
}),
return { searchCache: updatedSearchCache };
}),
);
}));