chore: update Angular cache and TypeScript build info files
This commit is contained in:
parent
4eb339eb22
commit
d78afda4cd
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
2
.angular/cache/20.2.2/app/.tsbuildinfo
vendored
File diff suppressed because one or more lines are too long
BIN
.angular/cache/20.2.2/app/angular-compiler.db
vendored
BIN
.angular/cache/20.2.2/app/angular-compiler.db
vendored
Binary file not shown.
@ -91,15 +91,17 @@ import {
|
|||||||
provideNetlifyLoader,
|
provideNetlifyLoader,
|
||||||
registerLocaleData,
|
registerLocaleData,
|
||||||
setRootDomAdapter
|
setRootDomAdapter
|
||||||
} from "./chunk-H4LQPAO2.js";
|
} from "./chunk-QTXEQHHS.js";
|
||||||
import {
|
import {
|
||||||
XhrFactory,
|
XhrFactory,
|
||||||
parseCookieValue
|
parseCookieValue
|
||||||
} from "./chunk-OUSM42MY.js";
|
} from "./chunk-OUSM42MY.js";
|
||||||
import {
|
import {
|
||||||
DOCUMENT,
|
|
||||||
IMAGE_CONFIG
|
IMAGE_CONFIG
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
DOCUMENT
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import "./chunk-HWYXSU2G.js";
|
import "./chunk-HWYXSU2G.js";
|
||||||
import "./chunk-JRFR6BLO.js";
|
import "./chunk-JRFR6BLO.js";
|
||||||
import "./chunk-MARUHEWW.js";
|
import "./chunk-MARUHEWW.js";
|
||||||
|
@ -38,9 +38,10 @@ import {
|
|||||||
withNoXsrfProtection,
|
withNoXsrfProtection,
|
||||||
withRequestsMadeViaParent,
|
withRequestsMadeViaParent,
|
||||||
withXsrfConfiguration
|
withXsrfConfiguration
|
||||||
} from "./chunk-5DRVFSXL.js";
|
} from "./chunk-GND3ZHQO.js";
|
||||||
import "./chunk-OUSM42MY.js";
|
import "./chunk-OUSM42MY.js";
|
||||||
import "./chunk-FVA7C6JK.js";
|
import "./chunk-XYAQCRC2.js";
|
||||||
|
import "./chunk-GFLMLXUS.js";
|
||||||
import "./chunk-HWYXSU2G.js";
|
import "./chunk-HWYXSU2G.js";
|
||||||
import "./chunk-JRFR6BLO.js";
|
import "./chunk-JRFR6BLO.js";
|
||||||
import "./chunk-MARUHEWW.js";
|
import "./chunk-MARUHEWW.js";
|
||||||
|
158
.angular/cache/20.2.2/app/vite/deps/@angular_core.js
vendored
158
.angular/cache/20.2.2/app/vite/deps/@angular_core.js
vendored
@ -14,10 +14,8 @@ import {
|
|||||||
Attribute,
|
Attribute,
|
||||||
CLIENT_RENDER_MODE_FLAG,
|
CLIENT_RENDER_MODE_FLAG,
|
||||||
COMPILER_OPTIONS,
|
COMPILER_OPTIONS,
|
||||||
CONTAINER_HEADER_OFFSET,
|
|
||||||
CSP_NONCE,
|
CSP_NONCE,
|
||||||
CUSTOM_ELEMENTS_SCHEMA,
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
ChangeDetectionScheduler,
|
|
||||||
ChangeDetectionSchedulerImpl,
|
ChangeDetectionSchedulerImpl,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@ -37,23 +35,17 @@ import {
|
|||||||
DEFER_BLOCK_CONFIG,
|
DEFER_BLOCK_CONFIG,
|
||||||
DEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
|
DEFER_BLOCK_DEPENDENCY_INTERCEPTOR,
|
||||||
DEHYDRATED_BLOCK_REGISTRY,
|
DEHYDRATED_BLOCK_REGISTRY,
|
||||||
DOCUMENT,
|
|
||||||
DebugElement,
|
DebugElement,
|
||||||
DebugEventListener,
|
DebugEventListener,
|
||||||
DebugNode,
|
DebugNode,
|
||||||
DefaultIterableDiffer,
|
DefaultIterableDiffer,
|
||||||
DeferBlockBehavior,
|
DeferBlockBehavior,
|
||||||
DeferBlockState,
|
DeferBlockState,
|
||||||
DestroyRef,
|
|
||||||
Directive,
|
Directive,
|
||||||
ENABLE_ROOT_COMPONENT_BOOTSTRAP,
|
ENABLE_ROOT_COMPONENT_BOOTSTRAP,
|
||||||
ENVIRONMENT_INITIALIZER,
|
|
||||||
EffectScheduler,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
ElementRegistry,
|
ElementRegistry,
|
||||||
EmbeddedViewRef,
|
EmbeddedViewRef,
|
||||||
EnvironmentInjector,
|
|
||||||
ErrorHandler,
|
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
FactoryTarget,
|
FactoryTarget,
|
||||||
Framework,
|
Framework,
|
||||||
@ -65,16 +57,11 @@ import {
|
|||||||
HydrationStatus,
|
HydrationStatus,
|
||||||
IMAGE_CONFIG,
|
IMAGE_CONFIG,
|
||||||
IMAGE_CONFIG_DEFAULTS,
|
IMAGE_CONFIG_DEFAULTS,
|
||||||
INJECTOR$1,
|
|
||||||
INJECTOR_SCOPE,
|
|
||||||
INTERNAL_APPLICATION_ERROR_HANDLER,
|
|
||||||
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
||||||
IS_HYDRATION_DOM_REUSE_ENABLED,
|
IS_HYDRATION_DOM_REUSE_ENABLED,
|
||||||
IS_INCREMENTAL_HYDRATION_ENABLED,
|
IS_INCREMENTAL_HYDRATION_ENABLED,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
|
||||||
Input,
|
Input,
|
||||||
IterableDiffers,
|
IterableDiffers,
|
||||||
JSACTION_BLOCK_ELEMENT_MAP,
|
JSACTION_BLOCK_ELEMENT_MAP,
|
||||||
@ -86,13 +73,6 @@ import {
|
|||||||
MAX_ANIMATION_TIMEOUT,
|
MAX_ANIMATION_TIMEOUT,
|
||||||
MissingTranslationStrategy,
|
MissingTranslationStrategy,
|
||||||
ModuleWithComponentFactories,
|
ModuleWithComponentFactories,
|
||||||
NG_COMP_DEF,
|
|
||||||
NG_DIR_DEF,
|
|
||||||
NG_ELEMENT_ID,
|
|
||||||
NG_INJ_DEF,
|
|
||||||
NG_MOD_DEF,
|
|
||||||
NG_PIPE_DEF,
|
|
||||||
NG_PROV_DEF,
|
|
||||||
NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR,
|
NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR,
|
||||||
NO_CHANGE,
|
NO_CHANGE,
|
||||||
NO_ERRORS_SCHEMA,
|
NO_ERRORS_SCHEMA,
|
||||||
@ -106,19 +86,15 @@ import {
|
|||||||
NoopNgZone,
|
NoopNgZone,
|
||||||
Optional,
|
Optional,
|
||||||
Output,
|
Output,
|
||||||
OutputEmitterRef,
|
|
||||||
PACKAGE_ROOT_URL,
|
PACKAGE_ROOT_URL,
|
||||||
PERFORMANCE_MARK_PREFIX,
|
PERFORMANCE_MARK_PREFIX,
|
||||||
PLATFORM_ID,
|
PLATFORM_ID,
|
||||||
PLATFORM_INITIALIZER,
|
PLATFORM_INITIALIZER,
|
||||||
PROVIDED_NG_ZONE,
|
PROVIDED_NG_ZONE,
|
||||||
PendingTasks,
|
|
||||||
PendingTasksInternal,
|
|
||||||
Pipe,
|
Pipe,
|
||||||
PlatformRef,
|
PlatformRef,
|
||||||
Query,
|
Query,
|
||||||
QueryList,
|
QueryList,
|
||||||
R3Injector,
|
|
||||||
REQUEST,
|
REQUEST,
|
||||||
REQUEST_CONTEXT,
|
REQUEST_CONTEXT,
|
||||||
RESPONSE_INIT,
|
RESPONSE_INIT,
|
||||||
@ -126,9 +102,6 @@ import {
|
|||||||
Renderer2,
|
Renderer2,
|
||||||
RendererFactory2,
|
RendererFactory2,
|
||||||
RendererStyleFlags2,
|
RendererStyleFlags2,
|
||||||
ResourceImpl,
|
|
||||||
RuntimeError,
|
|
||||||
SIGNAL,
|
|
||||||
SSR_CONTENT_INTEGRITY_MARKER,
|
SSR_CONTENT_INTEGRITY_MARKER,
|
||||||
Sanitizer,
|
Sanitizer,
|
||||||
SecurityContext,
|
SecurityContext,
|
||||||
@ -155,9 +128,6 @@ import {
|
|||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
ViewRef,
|
ViewRef,
|
||||||
ViewRef2,
|
ViewRef2,
|
||||||
XSS_SECURITY_URL,
|
|
||||||
ZONELESS_ENABLED,
|
|
||||||
_global,
|
|
||||||
_sanitizeHtml,
|
_sanitizeHtml,
|
||||||
_sanitizeUrl,
|
_sanitizeUrl,
|
||||||
afterEveryRender,
|
afterEveryRender,
|
||||||
@ -166,8 +136,6 @@ import {
|
|||||||
allowSanitizationBypassAndThrow,
|
allowSanitizationBypassAndThrow,
|
||||||
annotateForHydration,
|
annotateForHydration,
|
||||||
asNativeElements,
|
asNativeElements,
|
||||||
assertInInjectionContext,
|
|
||||||
assertNotInReactiveContext,
|
|
||||||
assertPlatform,
|
assertPlatform,
|
||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
bypassSanitizationTrustHtml,
|
bypassSanitizationTrustHtml,
|
||||||
@ -182,13 +150,10 @@ import {
|
|||||||
compileNgModuleDefs,
|
compileNgModuleDefs,
|
||||||
compileNgModuleFactory,
|
compileNgModuleFactory,
|
||||||
compilePipe,
|
compilePipe,
|
||||||
computed,
|
|
||||||
contentChild,
|
contentChild,
|
||||||
contentChildren,
|
contentChildren,
|
||||||
convertToBitFlags,
|
|
||||||
createComponent,
|
createComponent,
|
||||||
createEnvironmentInjector,
|
createEnvironmentInjector,
|
||||||
createInjector,
|
|
||||||
createNgModule,
|
createNgModule,
|
||||||
createNgModuleRef,
|
createNgModuleRef,
|
||||||
createOrReusePlatformInjector,
|
createOrReusePlatformInjector,
|
||||||
@ -196,43 +161,32 @@ import {
|
|||||||
createPlatformFactory,
|
createPlatformFactory,
|
||||||
defaultIterableDiffers,
|
defaultIterableDiffers,
|
||||||
defaultKeyValueDiffers,
|
defaultKeyValueDiffers,
|
||||||
defineInjectable,
|
|
||||||
depsTracker,
|
depsTracker,
|
||||||
destroyPlatform,
|
destroyPlatform,
|
||||||
devModeEqual,
|
devModeEqual,
|
||||||
disableProfiling,
|
disableProfiling,
|
||||||
effect,
|
|
||||||
enableProdMode,
|
enableProdMode,
|
||||||
enableProfiling,
|
enableProfiling,
|
||||||
enableProfiling2,
|
enableProfiling2,
|
||||||
encapsulateResourceError,
|
|
||||||
findLocaleData,
|
findLocaleData,
|
||||||
flushModuleScopingQueueAsMuchAsPossible,
|
flushModuleScopingQueueAsMuchAsPossible,
|
||||||
formatRuntimeError,
|
|
||||||
forwardRef,
|
|
||||||
generateStandaloneInDeclarationsError,
|
generateStandaloneInDeclarationsError,
|
||||||
getAnimationElementRemovalRegistry,
|
|
||||||
getAsyncClassMetadataFn,
|
getAsyncClassMetadataFn,
|
||||||
getClosestComponentName,
|
getClosestComponentName,
|
||||||
getComponentDef,
|
|
||||||
getDebugNode,
|
getDebugNode,
|
||||||
getDeferBlocks$1,
|
getDeferBlocks$1,
|
||||||
getDirectives,
|
getDirectives,
|
||||||
getDocument,
|
getDocument,
|
||||||
getHostElement,
|
getHostElement,
|
||||||
getInjectableDef,
|
|
||||||
getLContext,
|
getLContext,
|
||||||
getLocaleCurrencyCode,
|
getLocaleCurrencyCode,
|
||||||
getLocalePluralCase,
|
getLocalePluralCase,
|
||||||
getModuleFactory,
|
getModuleFactory,
|
||||||
getNgModuleById,
|
getNgModuleById,
|
||||||
getOutputDestroyRef,
|
|
||||||
getPlatform,
|
getPlatform,
|
||||||
getSanitizationBypassType,
|
getSanitizationBypassType,
|
||||||
getTransferState,
|
getTransferState,
|
||||||
importProvidersFrom,
|
|
||||||
inferTagNameFromDefinition,
|
inferTagNameFromDefinition,
|
||||||
inject,
|
|
||||||
injectChangeDetectorRef,
|
injectChangeDetectorRef,
|
||||||
input,
|
input,
|
||||||
inputBinding,
|
inputBinding,
|
||||||
@ -241,16 +195,10 @@ import {
|
|||||||
isBoundToModule,
|
isBoundToModule,
|
||||||
isComponentDefPendingResolution,
|
isComponentDefPendingResolution,
|
||||||
isDevMode,
|
isDevMode,
|
||||||
isEnvironmentProviders,
|
|
||||||
isInjectable,
|
|
||||||
isNgModule,
|
isNgModule,
|
||||||
isPromise,
|
isPromise,
|
||||||
isSignal,
|
|
||||||
isStandalone,
|
|
||||||
isSubscribable,
|
isSubscribable,
|
||||||
isViewDirty,
|
isViewDirty,
|
||||||
linkedSignal,
|
|
||||||
makeEnvironmentProviders,
|
|
||||||
makeStateKey,
|
makeStateKey,
|
||||||
markForRefresh,
|
markForRefresh,
|
||||||
mergeApplicationConfig,
|
mergeApplicationConfig,
|
||||||
@ -263,9 +211,7 @@ import {
|
|||||||
performanceMarkFeature,
|
performanceMarkFeature,
|
||||||
platformCore,
|
platformCore,
|
||||||
provideAppInitializer,
|
provideAppInitializer,
|
||||||
provideBrowserGlobalErrorListeners,
|
|
||||||
provideCheckNoChangesConfig,
|
provideCheckNoChangesConfig,
|
||||||
provideEnvironmentInitializer,
|
|
||||||
provideNgReflectAttributes,
|
provideNgReflectAttributes,
|
||||||
providePlatformInitializer,
|
providePlatformInitializer,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
@ -279,30 +225,19 @@ import {
|
|||||||
resetCompiledComponents,
|
resetCompiledComponents,
|
||||||
resetJitOptions,
|
resetJitOptions,
|
||||||
resolveComponentResources,
|
resolveComponentResources,
|
||||||
resolveForwardRef,
|
|
||||||
resource,
|
|
||||||
restoreComponentResolutionQueue,
|
restoreComponentResolutionQueue,
|
||||||
runInInjectionContext,
|
|
||||||
setAllowDuplicateNgModuleIdsForTest,
|
setAllowDuplicateNgModuleIdsForTest,
|
||||||
setAlternateWeakRefImpl,
|
|
||||||
setClassMetadata,
|
setClassMetadata,
|
||||||
setClassMetadataAsync,
|
setClassMetadataAsync,
|
||||||
setCurrentInjector,
|
|
||||||
setDocument,
|
setDocument,
|
||||||
setInjectorProfilerContext,
|
|
||||||
setLocaleId,
|
setLocaleId,
|
||||||
setTestabilityGetter,
|
setTestabilityGetter,
|
||||||
signal,
|
|
||||||
startMeasuring,
|
startMeasuring,
|
||||||
stopMeasuring,
|
stopMeasuring,
|
||||||
store,
|
|
||||||
stringify,
|
|
||||||
transitiveScopesFor,
|
transitiveScopesFor,
|
||||||
triggerResourceLoading,
|
triggerResourceLoading,
|
||||||
truncateMiddle,
|
|
||||||
twoWayBinding,
|
twoWayBinding,
|
||||||
unregisterAllLocaleData,
|
unregisterAllLocaleData,
|
||||||
untracked,
|
|
||||||
unwrapSafeValue,
|
unwrapSafeValue,
|
||||||
viewChild,
|
viewChild,
|
||||||
viewChildren,
|
viewChildren,
|
||||||
@ -317,7 +252,6 @@ import {
|
|||||||
ɵsetClassDebugInfo,
|
ɵsetClassDebugInfo,
|
||||||
ɵsetUnknownElementStrictMode,
|
ɵsetUnknownElementStrictMode,
|
||||||
ɵsetUnknownPropertyStrictMode,
|
ɵsetUnknownPropertyStrictMode,
|
||||||
ɵunwrapWritableSignal,
|
|
||||||
ɵɵAnimationsFeature,
|
ɵɵAnimationsFeature,
|
||||||
ɵɵCopyDefinitionFeature,
|
ɵɵCopyDefinitionFeature,
|
||||||
ɵɵExternalStylesFeature,
|
ɵɵExternalStylesFeature,
|
||||||
@ -368,12 +302,9 @@ import {
|
|||||||
ɵɵdeferWhen,
|
ɵɵdeferWhen,
|
||||||
ɵɵdefineComponent,
|
ɵɵdefineComponent,
|
||||||
ɵɵdefineDirective,
|
ɵɵdefineDirective,
|
||||||
ɵɵdefineInjectable,
|
|
||||||
ɵɵdefineInjector,
|
|
||||||
ɵɵdefineNgModule,
|
ɵɵdefineNgModule,
|
||||||
ɵɵdefinePipe,
|
ɵɵdefinePipe,
|
||||||
ɵɵdirectiveInject,
|
ɵɵdirectiveInject,
|
||||||
ɵɵdisableBindings,
|
|
||||||
ɵɵdomElement,
|
ɵɵdomElement,
|
||||||
ɵɵdomElementContainer,
|
ɵɵdomElementContainer,
|
||||||
ɵɵdomElementContainerEnd,
|
ɵɵdomElementContainerEnd,
|
||||||
@ -389,7 +320,6 @@ import {
|
|||||||
ɵɵelementContainerStart,
|
ɵɵelementContainerStart,
|
||||||
ɵɵelementEnd,
|
ɵɵelementEnd,
|
||||||
ɵɵelementStart,
|
ɵɵelementStart,
|
||||||
ɵɵenableBindings,
|
|
||||||
ɵɵgetComponentDepsFactory,
|
ɵɵgetComponentDepsFactory,
|
||||||
ɵɵgetCurrentView,
|
ɵɵgetCurrentView,
|
||||||
ɵɵgetInheritedFactory,
|
ɵɵgetInheritedFactory,
|
||||||
@ -401,7 +331,6 @@ import {
|
|||||||
ɵɵi18nExp,
|
ɵɵi18nExp,
|
||||||
ɵɵi18nPostprocess,
|
ɵɵi18nPostprocess,
|
||||||
ɵɵi18nStart,
|
ɵɵi18nStart,
|
||||||
ɵɵinject,
|
|
||||||
ɵɵinjectAttribute,
|
ɵɵinjectAttribute,
|
||||||
ɵɵinterpolate,
|
ɵɵinterpolate,
|
||||||
ɵɵinterpolate1,
|
ɵɵinterpolate1,
|
||||||
@ -414,12 +343,8 @@ import {
|
|||||||
ɵɵinterpolate8,
|
ɵɵinterpolate8,
|
||||||
ɵɵinterpolateV,
|
ɵɵinterpolateV,
|
||||||
ɵɵinvalidFactory,
|
ɵɵinvalidFactory,
|
||||||
ɵɵinvalidFactoryDep,
|
|
||||||
ɵɵlistener,
|
ɵɵlistener,
|
||||||
ɵɵloadQuery,
|
ɵɵloadQuery,
|
||||||
ɵɵnamespaceHTML,
|
|
||||||
ɵɵnamespaceMathML,
|
|
||||||
ɵɵnamespaceSVG,
|
|
||||||
ɵɵnextContext,
|
ɵɵnextContext,
|
||||||
ɵɵngDeclareClassMetadata,
|
ɵɵngDeclareClassMetadata,
|
||||||
ɵɵngDeclareClassMetadataAsync,
|
ɵɵngDeclareClassMetadataAsync,
|
||||||
@ -458,11 +383,9 @@ import {
|
|||||||
ɵɵrepeaterTrackByIdentity,
|
ɵɵrepeaterTrackByIdentity,
|
||||||
ɵɵrepeaterTrackByIndex,
|
ɵɵrepeaterTrackByIndex,
|
||||||
ɵɵreplaceMetadata,
|
ɵɵreplaceMetadata,
|
||||||
ɵɵresetView,
|
|
||||||
ɵɵresolveBody,
|
ɵɵresolveBody,
|
||||||
ɵɵresolveDocument,
|
ɵɵresolveDocument,
|
||||||
ɵɵresolveWindow,
|
ɵɵresolveWindow,
|
||||||
ɵɵrestoreView,
|
|
||||||
ɵɵsanitizeHtml,
|
ɵɵsanitizeHtml,
|
||||||
ɵɵsanitizeResourceUrl,
|
ɵɵsanitizeResourceUrl,
|
||||||
ɵɵsanitizeScript,
|
ɵɵsanitizeScript,
|
||||||
@ -497,7 +420,86 @@ import {
|
|||||||
ɵɵvalidateIframeAttribute,
|
ɵɵvalidateIframeAttribute,
|
||||||
ɵɵviewQuery,
|
ɵɵviewQuery,
|
||||||
ɵɵviewQuerySignal
|
ɵɵviewQuerySignal
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
CONTAINER_HEADER_OFFSET,
|
||||||
|
ChangeDetectionScheduler,
|
||||||
|
DOCUMENT,
|
||||||
|
DestroyRef,
|
||||||
|
ENVIRONMENT_INITIALIZER,
|
||||||
|
EffectScheduler,
|
||||||
|
EnvironmentInjector,
|
||||||
|
ErrorHandler,
|
||||||
|
INJECTOR$1,
|
||||||
|
INJECTOR_SCOPE,
|
||||||
|
INTERNAL_APPLICATION_ERROR_HANDLER,
|
||||||
|
InjectionToken,
|
||||||
|
Injector,
|
||||||
|
NG_COMP_DEF,
|
||||||
|
NG_DIR_DEF,
|
||||||
|
NG_ELEMENT_ID,
|
||||||
|
NG_INJ_DEF,
|
||||||
|
NG_MOD_DEF,
|
||||||
|
NG_PIPE_DEF,
|
||||||
|
NG_PROV_DEF,
|
||||||
|
OutputEmitterRef,
|
||||||
|
PendingTasks,
|
||||||
|
PendingTasksInternal,
|
||||||
|
R3Injector,
|
||||||
|
ResourceImpl,
|
||||||
|
RuntimeError,
|
||||||
|
SIGNAL,
|
||||||
|
XSS_SECURITY_URL,
|
||||||
|
ZONELESS_ENABLED,
|
||||||
|
_global,
|
||||||
|
assertInInjectionContext,
|
||||||
|
assertNotInReactiveContext,
|
||||||
|
computed,
|
||||||
|
convertToBitFlags,
|
||||||
|
createInjector,
|
||||||
|
defineInjectable,
|
||||||
|
effect,
|
||||||
|
encapsulateResourceError,
|
||||||
|
formatRuntimeError,
|
||||||
|
forwardRef,
|
||||||
|
getAnimationElementRemovalRegistry,
|
||||||
|
getComponentDef,
|
||||||
|
getInjectableDef,
|
||||||
|
getOutputDestroyRef,
|
||||||
|
importProvidersFrom,
|
||||||
|
inject,
|
||||||
|
isEnvironmentProviders,
|
||||||
|
isInjectable,
|
||||||
|
isSignal,
|
||||||
|
isStandalone,
|
||||||
|
linkedSignal,
|
||||||
|
makeEnvironmentProviders,
|
||||||
|
provideBrowserGlobalErrorListeners,
|
||||||
|
provideEnvironmentInitializer,
|
||||||
|
resolveForwardRef,
|
||||||
|
resource,
|
||||||
|
runInInjectionContext,
|
||||||
|
setAlternateWeakRefImpl,
|
||||||
|
setCurrentInjector,
|
||||||
|
setInjectorProfilerContext,
|
||||||
|
signal,
|
||||||
|
store,
|
||||||
|
stringify,
|
||||||
|
truncateMiddle,
|
||||||
|
untracked,
|
||||||
|
ɵunwrapWritableSignal,
|
||||||
|
ɵɵdefineInjectable,
|
||||||
|
ɵɵdefineInjector,
|
||||||
|
ɵɵdisableBindings,
|
||||||
|
ɵɵenableBindings,
|
||||||
|
ɵɵinject,
|
||||||
|
ɵɵinvalidFactoryDep,
|
||||||
|
ɵɵnamespaceHTML,
|
||||||
|
ɵɵnamespaceMathML,
|
||||||
|
ɵɵnamespaceSVG,
|
||||||
|
ɵɵresetView,
|
||||||
|
ɵɵrestoreView
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import "./chunk-HWYXSU2G.js";
|
import "./chunk-HWYXSU2G.js";
|
||||||
import "./chunk-JRFR6BLO.js";
|
import "./chunk-JRFR6BLO.js";
|
||||||
import "./chunk-MARUHEWW.js";
|
import "./chunk-MARUHEWW.js";
|
||||||
|
253
.angular/cache/20.2.2/app/vite/deps/@angular_core_rxjs-interop.js
vendored
Normal file
253
.angular/cache/20.2.2/app/vite/deps/@angular_core_rxjs-interop.js
vendored
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
import {
|
||||||
|
DestroyRef,
|
||||||
|
Injector,
|
||||||
|
PendingTasks,
|
||||||
|
RuntimeError,
|
||||||
|
assertInInjectionContext,
|
||||||
|
assertNotInReactiveContext,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
encapsulateResourceError,
|
||||||
|
getOutputDestroyRef,
|
||||||
|
inject,
|
||||||
|
resource,
|
||||||
|
signal,
|
||||||
|
untracked
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
|
import "./chunk-HWYXSU2G.js";
|
||||||
|
import "./chunk-JRFR6BLO.js";
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
ReplaySubject,
|
||||||
|
takeUntil
|
||||||
|
} from "./chunk-MARUHEWW.js";
|
||||||
|
import {
|
||||||
|
__spreadProps,
|
||||||
|
__spreadValues
|
||||||
|
} from "./chunk-GOMI4DH3.js";
|
||||||
|
|
||||||
|
// node_modules/@angular/core/fesm2022/rxjs-interop.mjs
|
||||||
|
function takeUntilDestroyed(destroyRef) {
|
||||||
|
if (!destroyRef) {
|
||||||
|
ngDevMode && assertInInjectionContext(takeUntilDestroyed);
|
||||||
|
destroyRef = inject(DestroyRef);
|
||||||
|
}
|
||||||
|
const destroyed$ = new Observable((subscriber) => {
|
||||||
|
if (destroyRef.destroyed) {
|
||||||
|
subscriber.next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const unregisterFn = destroyRef.onDestroy(subscriber.next.bind(subscriber));
|
||||||
|
return unregisterFn;
|
||||||
|
});
|
||||||
|
return (source) => {
|
||||||
|
return source.pipe(takeUntil(destroyed$));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
var OutputFromObservableRef = class {
|
||||||
|
source;
|
||||||
|
destroyed = false;
|
||||||
|
destroyRef = inject(DestroyRef);
|
||||||
|
constructor(source) {
|
||||||
|
this.source = source;
|
||||||
|
this.destroyRef.onDestroy(() => {
|
||||||
|
this.destroyed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
subscribe(callbackFn) {
|
||||||
|
if (this.destroyed) {
|
||||||
|
throw new RuntimeError(953, ngDevMode && "Unexpected subscription to destroyed `OutputRef`. The owning directive/component is destroyed.");
|
||||||
|
}
|
||||||
|
const subscription = this.source.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
||||||
|
next: (value) => callbackFn(value)
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
unsubscribe: () => subscription.unsubscribe()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function outputFromObservable(observable, opts) {
|
||||||
|
ngDevMode && assertInInjectionContext(outputFromObservable);
|
||||||
|
return new OutputFromObservableRef(observable);
|
||||||
|
}
|
||||||
|
function outputToObservable(ref) {
|
||||||
|
const destroyRef = getOutputDestroyRef(ref);
|
||||||
|
return new Observable((observer) => {
|
||||||
|
const unregisterOnDestroy = destroyRef?.onDestroy(() => observer.complete());
|
||||||
|
const subscription = ref.subscribe((v) => observer.next(v));
|
||||||
|
return () => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
unregisterOnDestroy?.();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function toObservable(source, options) {
|
||||||
|
if (ngDevMode && !options?.injector) {
|
||||||
|
assertInInjectionContext(toObservable);
|
||||||
|
}
|
||||||
|
const injector = options?.injector ?? inject(Injector);
|
||||||
|
const subject = new ReplaySubject(1);
|
||||||
|
const watcher = effect(() => {
|
||||||
|
let value;
|
||||||
|
try {
|
||||||
|
value = source();
|
||||||
|
} catch (err) {
|
||||||
|
untracked(() => subject.error(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
untracked(() => subject.next(value));
|
||||||
|
}, { injector, manualCleanup: true });
|
||||||
|
injector.get(DestroyRef).onDestroy(() => {
|
||||||
|
watcher.destroy();
|
||||||
|
subject.complete();
|
||||||
|
});
|
||||||
|
return subject.asObservable();
|
||||||
|
}
|
||||||
|
function toSignal(source, options) {
|
||||||
|
typeof ngDevMode !== "undefined" && ngDevMode && assertNotInReactiveContext(toSignal, "Invoking `toSignal` causes new subscriptions every time. Consider moving `toSignal` outside of the reactive context and read the signal value where needed.");
|
||||||
|
const requiresCleanup = !options?.manualCleanup;
|
||||||
|
if (ngDevMode && requiresCleanup && !options?.injector) {
|
||||||
|
assertInInjectionContext(toSignal);
|
||||||
|
}
|
||||||
|
const cleanupRef = requiresCleanup ? options?.injector?.get(DestroyRef) ?? inject(DestroyRef) : null;
|
||||||
|
const equal = makeToSignalEqual(options?.equal);
|
||||||
|
let state;
|
||||||
|
if (options?.requireSync) {
|
||||||
|
state = signal({
|
||||||
|
kind: 0
|
||||||
|
/* StateKind.NoValue */
|
||||||
|
}, { equal });
|
||||||
|
} else {
|
||||||
|
state = signal({ kind: 1, value: options?.initialValue }, { equal });
|
||||||
|
}
|
||||||
|
let destroyUnregisterFn;
|
||||||
|
const sub = source.subscribe({
|
||||||
|
next: (value) => state.set({ kind: 1, value }),
|
||||||
|
error: (error) => {
|
||||||
|
state.set({ kind: 2, error });
|
||||||
|
destroyUnregisterFn?.();
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
destroyUnregisterFn?.();
|
||||||
|
}
|
||||||
|
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of
|
||||||
|
// "complete".
|
||||||
|
});
|
||||||
|
if (options?.requireSync && state().kind === 0) {
|
||||||
|
throw new RuntimeError(601, (typeof ngDevMode === "undefined" || ngDevMode) && "`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.");
|
||||||
|
}
|
||||||
|
destroyUnregisterFn = cleanupRef?.onDestroy(sub.unsubscribe.bind(sub));
|
||||||
|
return computed(() => {
|
||||||
|
const current = state();
|
||||||
|
switch (current.kind) {
|
||||||
|
case 1:
|
||||||
|
return current.value;
|
||||||
|
case 2:
|
||||||
|
throw current.error;
|
||||||
|
case 0:
|
||||||
|
throw new RuntimeError(601, (typeof ngDevMode === "undefined" || ngDevMode) && "`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.");
|
||||||
|
}
|
||||||
|
}, { equal: options?.equal });
|
||||||
|
}
|
||||||
|
function makeToSignalEqual(userEquality = Object.is) {
|
||||||
|
return (a, b) => a.kind === 1 && b.kind === 1 && userEquality(a.value, b.value);
|
||||||
|
}
|
||||||
|
function pendingUntilEvent(injector) {
|
||||||
|
if (injector === void 0) {
|
||||||
|
ngDevMode && assertInInjectionContext(pendingUntilEvent);
|
||||||
|
injector = inject(Injector);
|
||||||
|
}
|
||||||
|
const taskService = injector.get(PendingTasks);
|
||||||
|
return (sourceObservable) => {
|
||||||
|
return new Observable((originalSubscriber) => {
|
||||||
|
const removeTask = taskService.add();
|
||||||
|
let cleanedUp = false;
|
||||||
|
function cleanupTask() {
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeTask();
|
||||||
|
cleanedUp = true;
|
||||||
|
}
|
||||||
|
const innerSubscription = sourceObservable.subscribe({
|
||||||
|
next: (v) => {
|
||||||
|
originalSubscriber.next(v);
|
||||||
|
cleanupTask();
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
originalSubscriber.complete();
|
||||||
|
cleanupTask();
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
originalSubscriber.error(e);
|
||||||
|
cleanupTask();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
innerSubscription.add(() => {
|
||||||
|
originalSubscriber.unsubscribe();
|
||||||
|
cleanupTask();
|
||||||
|
});
|
||||||
|
return innerSubscription;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function rxResource(opts) {
|
||||||
|
if (ngDevMode && !opts?.injector) {
|
||||||
|
assertInInjectionContext(rxResource);
|
||||||
|
}
|
||||||
|
return resource(__spreadProps(__spreadValues({}, opts), {
|
||||||
|
loader: void 0,
|
||||||
|
stream: (params) => {
|
||||||
|
let sub;
|
||||||
|
const onAbort = () => sub?.unsubscribe();
|
||||||
|
params.abortSignal.addEventListener("abort", onAbort);
|
||||||
|
const stream = signal({ value: void 0 });
|
||||||
|
let resolve;
|
||||||
|
const promise = new Promise((r) => resolve = r);
|
||||||
|
function send(value) {
|
||||||
|
stream.set(value);
|
||||||
|
resolve?.(stream);
|
||||||
|
resolve = void 0;
|
||||||
|
}
|
||||||
|
const streamFn = opts.stream ?? opts.loader;
|
||||||
|
if (streamFn === void 0) {
|
||||||
|
throw new RuntimeError(990, ngDevMode && `Must provide \`stream\` option.`);
|
||||||
|
}
|
||||||
|
sub = streamFn(params).subscribe({
|
||||||
|
next: (value) => send({ value }),
|
||||||
|
error: (error) => {
|
||||||
|
send({ error: encapsulateResourceError(error) });
|
||||||
|
params.abortSignal.removeEventListener("abort", onAbort);
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
if (resolve) {
|
||||||
|
send({
|
||||||
|
error: new RuntimeError(991, ngDevMode && "Resource completed before producing a value")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
params.abortSignal.removeEventListener("abort", onAbort);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
outputFromObservable,
|
||||||
|
outputToObservable,
|
||||||
|
pendingUntilEvent,
|
||||||
|
rxResource,
|
||||||
|
takeUntilDestroyed,
|
||||||
|
toObservable,
|
||||||
|
toSignal
|
||||||
|
};
|
||||||
|
/*! Bundled license information:
|
||||||
|
|
||||||
|
@angular/core/fesm2022/rxjs-interop.mjs:
|
||||||
|
(**
|
||||||
|
* @license Angular v20.2.4
|
||||||
|
* (c) 2010-2025 Google LLC. https://angular.io/
|
||||||
|
* License: MIT
|
||||||
|
*)
|
||||||
|
*/
|
||||||
|
//# sourceMappingURL=@angular_core_rxjs-interop.js.map
|
7
.angular/cache/20.2.2/app/vite/deps/@angular_core_rxjs-interop.js.map
vendored
Normal file
7
.angular/cache/20.2.2/app/vite/deps/@angular_core_rxjs-interop.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,51 +1,53 @@
|
|||||||
import {
|
import {
|
||||||
getDOM
|
getDOM
|
||||||
} from "./chunk-H4LQPAO2.js";
|
} from "./chunk-QTXEQHHS.js";
|
||||||
import "./chunk-OUSM42MY.js";
|
import "./chunk-OUSM42MY.js";
|
||||||
import {
|
import {
|
||||||
ApplicationRef,
|
ApplicationRef,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
DestroyRef,
|
|
||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
Host,
|
Host,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
|
||||||
Input,
|
Input,
|
||||||
NgModule,
|
NgModule,
|
||||||
Optional,
|
Optional,
|
||||||
Output,
|
Output,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
RuntimeError,
|
|
||||||
Self,
|
Self,
|
||||||
SkipSelf,
|
SkipSelf,
|
||||||
Version,
|
Version,
|
||||||
afterNextRender,
|
afterNextRender,
|
||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
computed,
|
|
||||||
forwardRef,
|
|
||||||
inject,
|
|
||||||
isPromise,
|
isPromise,
|
||||||
isSubscribable,
|
isSubscribable,
|
||||||
setClassMetadata,
|
setClassMetadata,
|
||||||
signal,
|
|
||||||
untracked,
|
|
||||||
ɵɵInheritDefinitionFeature,
|
ɵɵInheritDefinitionFeature,
|
||||||
ɵɵNgOnChangesFeature,
|
ɵɵNgOnChangesFeature,
|
||||||
ɵɵProvidersFeature,
|
ɵɵProvidersFeature,
|
||||||
ɵɵattribute,
|
ɵɵattribute,
|
||||||
ɵɵclassProp,
|
ɵɵclassProp,
|
||||||
ɵɵdefineDirective,
|
ɵɵdefineDirective,
|
||||||
ɵɵdefineInjectable,
|
|
||||||
ɵɵdefineInjector,
|
|
||||||
ɵɵdefineNgModule,
|
ɵɵdefineNgModule,
|
||||||
ɵɵdirectiveInject,
|
ɵɵdirectiveInject,
|
||||||
ɵɵgetInheritedFactory,
|
ɵɵgetInheritedFactory,
|
||||||
ɵɵlistener
|
ɵɵlistener
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
DestroyRef,
|
||||||
|
InjectionToken,
|
||||||
|
Injector,
|
||||||
|
RuntimeError,
|
||||||
|
computed,
|
||||||
|
forwardRef,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
untracked,
|
||||||
|
ɵɵdefineInjectable,
|
||||||
|
ɵɵdefineInjector
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import {
|
import {
|
||||||
forkJoin
|
forkJoin
|
||||||
} from "./chunk-HWYXSU2G.js";
|
} from "./chunk-HWYXSU2G.js";
|
||||||
|
File diff suppressed because one or more lines are too long
@ -34,13 +34,14 @@ import {
|
|||||||
withI18nSupport,
|
withI18nSupport,
|
||||||
withIncrementalHydration,
|
withIncrementalHydration,
|
||||||
withNoHttpTransferCache
|
withNoHttpTransferCache
|
||||||
} from "./chunk-DYWB3JMR.js";
|
} from "./chunk-OUP2T4DW.js";
|
||||||
import "./chunk-5DRVFSXL.js";
|
import "./chunk-GND3ZHQO.js";
|
||||||
import {
|
import {
|
||||||
getDOM
|
getDOM
|
||||||
} from "./chunk-H4LQPAO2.js";
|
} from "./chunk-QTXEQHHS.js";
|
||||||
import "./chunk-OUSM42MY.js";
|
import "./chunk-OUSM42MY.js";
|
||||||
import "./chunk-FVA7C6JK.js";
|
import "./chunk-XYAQCRC2.js";
|
||||||
|
import "./chunk-GFLMLXUS.js";
|
||||||
import "./chunk-HWYXSU2G.js";
|
import "./chunk-HWYXSU2G.js";
|
||||||
import "./chunk-JRFR6BLO.js";
|
import "./chunk-JRFR6BLO.js";
|
||||||
import "./chunk-MARUHEWW.js";
|
import "./chunk-MARUHEWW.js";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Title
|
Title
|
||||||
} from "./chunk-DYWB3JMR.js";
|
} from "./chunk-OUP2T4DW.js";
|
||||||
import "./chunk-5DRVFSXL.js";
|
import "./chunk-GND3ZHQO.js";
|
||||||
import {
|
import {
|
||||||
HashLocationStrategy,
|
HashLocationStrategy,
|
||||||
LOCATION_INITIALIZED,
|
LOCATION_INITIALIZED,
|
||||||
@ -9,7 +9,7 @@ import {
|
|||||||
LocationStrategy,
|
LocationStrategy,
|
||||||
PathLocationStrategy,
|
PathLocationStrategy,
|
||||||
ViewportScroller
|
ViewportScroller
|
||||||
} from "./chunk-H4LQPAO2.js";
|
} from "./chunk-QTXEQHHS.js";
|
||||||
import "./chunk-OUSM42MY.js";
|
import "./chunk-OUSM42MY.js";
|
||||||
import {
|
import {
|
||||||
APP_BOOTSTRAP_LISTENER,
|
APP_BOOTSTRAP_LISTENER,
|
||||||
@ -20,69 +20,71 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
Console,
|
Console,
|
||||||
ContentChildren,
|
ContentChildren,
|
||||||
DOCUMENT,
|
|
||||||
DestroyRef,
|
|
||||||
Directive,
|
Directive,
|
||||||
ENVIRONMENT_INITIALIZER,
|
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EnvironmentInjector,
|
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
HostAttributeToken,
|
HostAttributeToken,
|
||||||
HostBinding,
|
HostBinding,
|
||||||
HostListener,
|
HostListener,
|
||||||
INTERNAL_APPLICATION_ERROR_HANDLER,
|
|
||||||
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
||||||
Injectable,
|
Injectable,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
|
||||||
Input,
|
Input,
|
||||||
NgModule,
|
NgModule,
|
||||||
NgModuleFactory$1,
|
NgModuleFactory$1,
|
||||||
NgZone,
|
NgZone,
|
||||||
Optional,
|
Optional,
|
||||||
Output,
|
Output,
|
||||||
PendingTasksInternal,
|
|
||||||
Renderer2,
|
Renderer2,
|
||||||
RuntimeError,
|
|
||||||
SkipSelf,
|
SkipSelf,
|
||||||
Version,
|
Version,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
afterNextRender,
|
afterNextRender,
|
||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
createEnvironmentInjector,
|
createEnvironmentInjector,
|
||||||
inject,
|
|
||||||
input,
|
input,
|
||||||
isInjectable,
|
|
||||||
isNgModule,
|
isNgModule,
|
||||||
isPromise,
|
isPromise,
|
||||||
isStandalone,
|
|
||||||
makeEnvironmentProviders,
|
|
||||||
performanceMarkFeature,
|
performanceMarkFeature,
|
||||||
provideAppInitializer,
|
provideAppInitializer,
|
||||||
reflectComponentType,
|
reflectComponentType,
|
||||||
runInInjectionContext,
|
|
||||||
setClassMetadata,
|
setClassMetadata,
|
||||||
signal,
|
|
||||||
untracked,
|
|
||||||
ɵɵNgOnChangesFeature,
|
ɵɵNgOnChangesFeature,
|
||||||
ɵɵattribute,
|
ɵɵattribute,
|
||||||
ɵɵcontentQuery,
|
ɵɵcontentQuery,
|
||||||
ɵɵdefineComponent,
|
ɵɵdefineComponent,
|
||||||
ɵɵdefineDirective,
|
ɵɵdefineDirective,
|
||||||
ɵɵdefineInjectable,
|
|
||||||
ɵɵdefineInjector,
|
|
||||||
ɵɵdefineNgModule,
|
ɵɵdefineNgModule,
|
||||||
ɵɵdirectiveInject,
|
ɵɵdirectiveInject,
|
||||||
ɵɵelement,
|
ɵɵelement,
|
||||||
ɵɵgetInheritedFactory,
|
ɵɵgetInheritedFactory,
|
||||||
ɵɵinject,
|
|
||||||
ɵɵinjectAttribute,
|
ɵɵinjectAttribute,
|
||||||
ɵɵinvalidFactory,
|
ɵɵinvalidFactory,
|
||||||
ɵɵlistener,
|
ɵɵlistener,
|
||||||
ɵɵloadQuery,
|
ɵɵloadQuery,
|
||||||
ɵɵqueryRefresh,
|
ɵɵqueryRefresh,
|
||||||
ɵɵsanitizeUrlOrResourceUrl
|
ɵɵsanitizeUrlOrResourceUrl
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
DOCUMENT,
|
||||||
|
DestroyRef,
|
||||||
|
ENVIRONMENT_INITIALIZER,
|
||||||
|
EnvironmentInjector,
|
||||||
|
INTERNAL_APPLICATION_ERROR_HANDLER,
|
||||||
|
InjectionToken,
|
||||||
|
Injector,
|
||||||
|
PendingTasksInternal,
|
||||||
|
RuntimeError,
|
||||||
|
inject,
|
||||||
|
isInjectable,
|
||||||
|
isStandalone,
|
||||||
|
makeEnvironmentProviders,
|
||||||
|
runInInjectionContext,
|
||||||
|
signal,
|
||||||
|
untracked,
|
||||||
|
ɵɵdefineInjectable,
|
||||||
|
ɵɵdefineInjector,
|
||||||
|
ɵɵinject
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import {
|
import {
|
||||||
defer,
|
defer,
|
||||||
isObservable
|
isObservable
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,79 +1,88 @@
|
|||||||
{
|
{
|
||||||
"hash": "f4c7eaa2",
|
"hash": "26d31ed9",
|
||||||
"configHash": "d859ec53",
|
"configHash": "69ef457b",
|
||||||
"lockfileHash": "9b1c4210",
|
"lockfileHash": "9b1c4210",
|
||||||
"browserHash": "6942cbde",
|
"browserHash": "2055d91d",
|
||||||
"optimized": {
|
"optimized": {
|
||||||
"@angular/common": {
|
"@angular/common": {
|
||||||
"src": "../../../../../../node_modules/@angular/common/fesm2022/common.mjs",
|
"src": "../../../../../../node_modules/@angular/common/fesm2022/common.mjs",
|
||||||
"file": "@angular_common.js",
|
"file": "@angular_common.js",
|
||||||
"fileHash": "92f641aa",
|
"fileHash": "58788c60",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@angular/common/http": {
|
"@angular/common/http": {
|
||||||
"src": "../../../../../../node_modules/@angular/common/fesm2022/http.mjs",
|
"src": "../../../../../../node_modules/@angular/common/fesm2022/http.mjs",
|
||||||
"file": "@angular_common_http.js",
|
"file": "@angular_common_http.js",
|
||||||
"fileHash": "3a8b8614",
|
"fileHash": "28e8e915",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@angular/core": {
|
"@angular/core": {
|
||||||
"src": "../../../../../../node_modules/@angular/core/fesm2022/core.mjs",
|
"src": "../../../../../../node_modules/@angular/core/fesm2022/core.mjs",
|
||||||
"file": "@angular_core.js",
|
"file": "@angular_core.js",
|
||||||
"fileHash": "b1ee9355",
|
"fileHash": "a1777273",
|
||||||
|
"needsInterop": false
|
||||||
|
},
|
||||||
|
"@angular/core/rxjs-interop": {
|
||||||
|
"src": "../../../../../../node_modules/@angular/core/fesm2022/rxjs-interop.mjs",
|
||||||
|
"file": "@angular_core_rxjs-interop.js",
|
||||||
|
"fileHash": "5aac569e",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@angular/forms": {
|
"@angular/forms": {
|
||||||
"src": "../../../../../../node_modules/@angular/forms/fesm2022/forms.mjs",
|
"src": "../../../../../../node_modules/@angular/forms/fesm2022/forms.mjs",
|
||||||
"file": "@angular_forms.js",
|
"file": "@angular_forms.js",
|
||||||
"fileHash": "5842af9d",
|
"fileHash": "76b8143b",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@angular/platform-browser": {
|
"@angular/platform-browser": {
|
||||||
"src": "../../../../../../node_modules/@angular/platform-browser/fesm2022/platform-browser.mjs",
|
"src": "../../../../../../node_modules/@angular/platform-browser/fesm2022/platform-browser.mjs",
|
||||||
"file": "@angular_platform-browser.js",
|
"file": "@angular_platform-browser.js",
|
||||||
"fileHash": "f65b040d",
|
"fileHash": "0219e3bd",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@angular/router": {
|
"@angular/router": {
|
||||||
"src": "../../../../../../node_modules/@angular/router/fesm2022/router.mjs",
|
"src": "../../../../../../node_modules/@angular/router/fesm2022/router.mjs",
|
||||||
"file": "@angular_router.js",
|
"file": "@angular_router.js",
|
||||||
"fileHash": "f605abad",
|
"fileHash": "e1237275",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"@google/genai": {
|
"@google/genai": {
|
||||||
"src": "../../../../../../node_modules/@google/genai/dist/web/index.mjs",
|
"src": "../../../../../../node_modules/@google/genai/dist/web/index.mjs",
|
||||||
"file": "@google_genai.js",
|
"file": "@google_genai.js",
|
||||||
"fileHash": "b3b8b992",
|
"fileHash": "89bd7952",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"rxjs": {
|
"rxjs": {
|
||||||
"src": "../../../../../../node_modules/rxjs/dist/esm5/index.js",
|
"src": "../../../../../../node_modules/rxjs/dist/esm5/index.js",
|
||||||
"file": "rxjs.js",
|
"file": "rxjs.js",
|
||||||
"fileHash": "0cb397ac",
|
"fileHash": "460c479b",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
},
|
},
|
||||||
"rxjs/operators": {
|
"rxjs/operators": {
|
||||||
"src": "../../../../../../node_modules/rxjs/dist/esm5/operators/index.js",
|
"src": "../../../../../../node_modules/rxjs/dist/esm5/operators/index.js",
|
||||||
"file": "rxjs_operators.js",
|
"file": "rxjs_operators.js",
|
||||||
"fileHash": "e706ebfa",
|
"fileHash": "0c6ce2bb",
|
||||||
"needsInterop": false
|
"needsInterop": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chunks": {
|
"chunks": {
|
||||||
"chunk-DYWB3JMR": {
|
"chunk-OUP2T4DW": {
|
||||||
"file": "chunk-DYWB3JMR.js"
|
"file": "chunk-OUP2T4DW.js"
|
||||||
},
|
},
|
||||||
"chunk-5DRVFSXL": {
|
"chunk-GND3ZHQO": {
|
||||||
"file": "chunk-5DRVFSXL.js"
|
"file": "chunk-GND3ZHQO.js"
|
||||||
},
|
},
|
||||||
"chunk-H4LQPAO2": {
|
"chunk-QTXEQHHS": {
|
||||||
"file": "chunk-H4LQPAO2.js"
|
"file": "chunk-QTXEQHHS.js"
|
||||||
},
|
},
|
||||||
"chunk-OUSM42MY": {
|
"chunk-OUSM42MY": {
|
||||||
"file": "chunk-OUSM42MY.js"
|
"file": "chunk-OUSM42MY.js"
|
||||||
},
|
},
|
||||||
"chunk-FVA7C6JK": {
|
"chunk-XYAQCRC2": {
|
||||||
"file": "chunk-FVA7C6JK.js"
|
"file": "chunk-XYAQCRC2.js"
|
||||||
|
},
|
||||||
|
"chunk-GFLMLXUS": {
|
||||||
|
"file": "chunk-GFLMLXUS.js"
|
||||||
},
|
},
|
||||||
"chunk-HWYXSU2G": {
|
"chunk-HWYXSU2G": {
|
||||||
"file": "chunk-HWYXSU2G.js"
|
"file": "chunk-HWYXSU2G.js"
|
||||||
|
File diff suppressed because one or more lines are too long
3756
.angular/cache/20.2.2/app/vite/deps/chunk-GFLMLXUS.js
vendored
Normal file
3756
.angular/cache/20.2.2/app/vite/deps/chunk-GFLMLXUS.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
.angular/cache/20.2.2/app/vite/deps/chunk-GFLMLXUS.js.map
vendored
Normal file
7
.angular/cache/20.2.2/app/vite/deps/chunk-GFLMLXUS.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -5,19 +5,25 @@ import {
|
|||||||
import {
|
import {
|
||||||
APP_BOOTSTRAP_LISTENER,
|
APP_BOOTSTRAP_LISTENER,
|
||||||
ApplicationRef,
|
ApplicationRef,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NgModule,
|
||||||
|
NgZone,
|
||||||
|
TransferState,
|
||||||
|
makeStateKey,
|
||||||
|
performanceMarkFeature,
|
||||||
|
setClassMetadata,
|
||||||
|
ɵɵdefineNgModule
|
||||||
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
DestroyRef,
|
DestroyRef,
|
||||||
EnvironmentInjector,
|
EnvironmentInjector,
|
||||||
Inject,
|
|
||||||
Injectable,
|
|
||||||
InjectionToken,
|
InjectionToken,
|
||||||
Injector,
|
Injector,
|
||||||
NgModule,
|
|
||||||
NgZone,
|
|
||||||
PendingTasks,
|
PendingTasks,
|
||||||
ResourceImpl,
|
ResourceImpl,
|
||||||
RuntimeError,
|
RuntimeError,
|
||||||
TransferState,
|
|
||||||
assertInInjectionContext,
|
assertInInjectionContext,
|
||||||
computed,
|
computed,
|
||||||
encapsulateResourceError,
|
encapsulateResourceError,
|
||||||
@ -25,17 +31,13 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
linkedSignal,
|
linkedSignal,
|
||||||
makeEnvironmentProviders,
|
makeEnvironmentProviders,
|
||||||
makeStateKey,
|
|
||||||
performanceMarkFeature,
|
|
||||||
runInInjectionContext,
|
runInInjectionContext,
|
||||||
setClassMetadata,
|
|
||||||
signal,
|
signal,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
ɵɵdefineInjectable,
|
ɵɵdefineInjectable,
|
||||||
ɵɵdefineInjector,
|
ɵɵdefineInjector,
|
||||||
ɵɵdefineNgModule,
|
|
||||||
ɵɵinject
|
ɵɵinject
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
concatMap,
|
concatMap,
|
||||||
@ -2776,4 +2778,4 @@ export {
|
|||||||
* License: MIT
|
* License: MIT
|
||||||
*)
|
*)
|
||||||
*/
|
*/
|
||||||
//# sourceMappingURL=chunk-5DRVFSXL.js.map
|
//# sourceMappingURL=chunk-GND3ZHQO.js.map
|
File diff suppressed because one or more lines are too long
@ -1,13 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
withHttpTransferCache
|
withHttpTransferCache
|
||||||
} from "./chunk-5DRVFSXL.js";
|
} from "./chunk-GND3ZHQO.js";
|
||||||
import {
|
import {
|
||||||
CommonModule,
|
CommonModule,
|
||||||
DomAdapter,
|
DomAdapter,
|
||||||
PLATFORM_BROWSER_ID,
|
PLATFORM_BROWSER_ID,
|
||||||
getDOM,
|
getDOM,
|
||||||
setRootDomAdapter
|
setRootDomAdapter
|
||||||
} from "./chunk-H4LQPAO2.js";
|
} from "./chunk-QTXEQHHS.js";
|
||||||
import {
|
import {
|
||||||
XhrFactory,
|
XhrFactory,
|
||||||
parseCookieValue
|
parseCookieValue
|
||||||
@ -19,15 +19,9 @@ import {
|
|||||||
ApplicationRef,
|
ApplicationRef,
|
||||||
CSP_NONCE,
|
CSP_NONCE,
|
||||||
Console,
|
Console,
|
||||||
DOCUMENT,
|
|
||||||
ENVIRONMENT_INITIALIZER,
|
|
||||||
ErrorHandler,
|
|
||||||
INJECTOR_SCOPE,
|
|
||||||
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
IS_ENABLED_BLOCKING_INITIAL_NAVIGATION,
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
|
||||||
MAX_ANIMATION_TIMEOUT,
|
MAX_ANIMATION_TIMEOUT,
|
||||||
NgModule,
|
NgModule,
|
||||||
NgZone,
|
NgZone,
|
||||||
@ -36,7 +30,6 @@ import {
|
|||||||
PLATFORM_INITIALIZER,
|
PLATFORM_INITIALIZER,
|
||||||
RendererFactory2,
|
RendererFactory2,
|
||||||
RendererStyleFlags2,
|
RendererStyleFlags2,
|
||||||
RuntimeError,
|
|
||||||
SecurityContext,
|
SecurityContext,
|
||||||
TESTABILITY,
|
TESTABILITY,
|
||||||
TESTABILITY_GETTER,
|
TESTABILITY_GETTER,
|
||||||
@ -45,9 +38,6 @@ import {
|
|||||||
TracingService,
|
TracingService,
|
||||||
Version,
|
Version,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
XSS_SECURITY_URL,
|
|
||||||
ZONELESS_ENABLED,
|
|
||||||
_global,
|
|
||||||
_sanitizeHtml,
|
_sanitizeHtml,
|
||||||
_sanitizeUrl,
|
_sanitizeUrl,
|
||||||
allowSanitizationBypassAndThrow,
|
allowSanitizationBypassAndThrow,
|
||||||
@ -57,12 +47,7 @@ import {
|
|||||||
bypassSanitizationTrustStyle,
|
bypassSanitizationTrustStyle,
|
||||||
bypassSanitizationTrustUrl,
|
bypassSanitizationTrustUrl,
|
||||||
createPlatformFactory,
|
createPlatformFactory,
|
||||||
formatRuntimeError,
|
|
||||||
forwardRef,
|
|
||||||
getAnimationElementRemovalRegistry,
|
|
||||||
inject,
|
|
||||||
internalCreateApplication,
|
internalCreateApplication,
|
||||||
makeEnvironmentProviders,
|
|
||||||
platformCore,
|
platformCore,
|
||||||
setClassMetadata,
|
setClassMetadata,
|
||||||
setDocument,
|
setDocument,
|
||||||
@ -71,11 +56,28 @@ import {
|
|||||||
withEventReplay,
|
withEventReplay,
|
||||||
withI18nSupport,
|
withI18nSupport,
|
||||||
withIncrementalHydration,
|
withIncrementalHydration,
|
||||||
|
ɵɵdefineNgModule
|
||||||
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
DOCUMENT,
|
||||||
|
ENVIRONMENT_INITIALIZER,
|
||||||
|
ErrorHandler,
|
||||||
|
INJECTOR_SCOPE,
|
||||||
|
InjectionToken,
|
||||||
|
Injector,
|
||||||
|
RuntimeError,
|
||||||
|
XSS_SECURITY_URL,
|
||||||
|
ZONELESS_ENABLED,
|
||||||
|
_global,
|
||||||
|
formatRuntimeError,
|
||||||
|
forwardRef,
|
||||||
|
getAnimationElementRemovalRegistry,
|
||||||
|
inject,
|
||||||
|
makeEnvironmentProviders,
|
||||||
ɵɵdefineInjectable,
|
ɵɵdefineInjectable,
|
||||||
ɵɵdefineInjector,
|
ɵɵdefineInjector,
|
||||||
ɵɵdefineNgModule,
|
|
||||||
ɵɵinject
|
ɵɵinject
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import {
|
import {
|
||||||
__spreadValues
|
__spreadValues
|
||||||
} from "./chunk-GOMI4DH3.js";
|
} from "./chunk-GOMI4DH3.js";
|
||||||
@ -2073,4 +2075,4 @@ export {
|
|||||||
* License: MIT
|
* License: MIT
|
||||||
*)
|
*)
|
||||||
*/
|
*/
|
||||||
//# sourceMappingURL=chunk-DYWB3JMR.js.map
|
//# sourceMappingURL=chunk-OUP2T4DW.js.map
|
File diff suppressed because one or more lines are too long
@ -3,18 +3,13 @@ import {
|
|||||||
Attribute,
|
Attribute,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
DEFAULT_CURRENCY_CODE,
|
DEFAULT_CURRENCY_CODE,
|
||||||
DOCUMENT,
|
|
||||||
DestroyRef,
|
|
||||||
Directive,
|
Directive,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Host,
|
Host,
|
||||||
IMAGE_CONFIG,
|
IMAGE_CONFIG,
|
||||||
IMAGE_CONFIG_DEFAULTS,
|
IMAGE_CONFIG_DEFAULTS,
|
||||||
INTERNAL_APPLICATION_ERROR_HANDLER,
|
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
InjectionToken,
|
|
||||||
Injector,
|
|
||||||
Input,
|
Input,
|
||||||
IterableDiffers,
|
IterableDiffers,
|
||||||
KeyValueDiffers,
|
KeyValueDiffers,
|
||||||
@ -27,37 +22,44 @@ import {
|
|||||||
Pipe,
|
Pipe,
|
||||||
Renderer2,
|
Renderer2,
|
||||||
RendererStyleFlags2,
|
RendererStyleFlags2,
|
||||||
RuntimeError,
|
|
||||||
TemplateRef,
|
TemplateRef,
|
||||||
Version,
|
Version,
|
||||||
ViewContainerRef,
|
ViewContainerRef,
|
||||||
booleanAttribute,
|
booleanAttribute,
|
||||||
createNgModule,
|
createNgModule,
|
||||||
findLocaleData,
|
findLocaleData,
|
||||||
formatRuntimeError,
|
|
||||||
getLocaleCurrencyCode,
|
getLocaleCurrencyCode,
|
||||||
getLocalePluralCase,
|
getLocalePluralCase,
|
||||||
inject,
|
|
||||||
isPromise,
|
isPromise,
|
||||||
isSubscribable,
|
isSubscribable,
|
||||||
numberAttribute,
|
numberAttribute,
|
||||||
performanceMarkFeature,
|
performanceMarkFeature,
|
||||||
registerLocaleData,
|
registerLocaleData,
|
||||||
setClassMetadata,
|
setClassMetadata,
|
||||||
stringify,
|
|
||||||
untracked,
|
|
||||||
unwrapSafeValue,
|
unwrapSafeValue,
|
||||||
ɵɵNgOnChangesFeature,
|
ɵɵNgOnChangesFeature,
|
||||||
ɵɵdefineDirective,
|
ɵɵdefineDirective,
|
||||||
ɵɵdefineInjectable,
|
|
||||||
ɵɵdefineInjector,
|
|
||||||
ɵɵdefineNgModule,
|
ɵɵdefineNgModule,
|
||||||
ɵɵdefinePipe,
|
ɵɵdefinePipe,
|
||||||
ɵɵdirectiveInject,
|
ɵɵdirectiveInject,
|
||||||
ɵɵinject,
|
|
||||||
ɵɵinjectAttribute,
|
ɵɵinjectAttribute,
|
||||||
ɵɵstyleProp
|
ɵɵstyleProp
|
||||||
} from "./chunk-FVA7C6JK.js";
|
} from "./chunk-XYAQCRC2.js";
|
||||||
|
import {
|
||||||
|
DOCUMENT,
|
||||||
|
DestroyRef,
|
||||||
|
INTERNAL_APPLICATION_ERROR_HANDLER,
|
||||||
|
InjectionToken,
|
||||||
|
Injector,
|
||||||
|
RuntimeError,
|
||||||
|
formatRuntimeError,
|
||||||
|
inject,
|
||||||
|
stringify,
|
||||||
|
untracked,
|
||||||
|
ɵɵdefineInjectable,
|
||||||
|
ɵɵdefineInjector,
|
||||||
|
ɵɵinject
|
||||||
|
} from "./chunk-GFLMLXUS.js";
|
||||||
import {
|
import {
|
||||||
Subject
|
Subject
|
||||||
} from "./chunk-MARUHEWW.js";
|
} from "./chunk-MARUHEWW.js";
|
||||||
@ -5188,4 +5190,4 @@ export {
|
|||||||
* License: MIT
|
* License: MIT
|
||||||
*)
|
*)
|
||||||
*/
|
*/
|
||||||
//# sourceMappingURL=chunk-H4LQPAO2.js.map
|
//# sourceMappingURL=chunk-QTXEQHHS.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
7
.angular/cache/20.2.2/app/vite/deps/chunk-XYAQCRC2.js.map
vendored
Normal file
7
.angular/cache/20.2.2/app/vite/deps/chunk-XYAQCRC2.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
54
api/README.md
Normal file
54
api/README.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
# NewTube Unified Search API
|
||||||
|
|
||||||
|
This document describes the unified search endpoint used by the frontend SearchBar and Search page.
|
||||||
|
|
||||||
|
Base URL
|
||||||
|
- Development: http://localhost:4000/api
|
||||||
|
- Production: /api
|
||||||
|
|
||||||
|
Endpoint
|
||||||
|
- GET /api/search
|
||||||
|
|
||||||
|
Query parameters
|
||||||
|
- q (string, required, min length 2)
|
||||||
|
- providers (string, optional): Comma-separated list of provider ids. Allowed values: yt, dm, tw, pt, od, ru. Defaults to all when omitted or invalid.
|
||||||
|
- page (integer, optional): Page index starting at 1. Default: 1.
|
||||||
|
- pageSize (integer, optional): Page size, max 50. Default: 24.
|
||||||
|
- sort (string, optional): Reserved for future use.
|
||||||
|
|
||||||
|
Response
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"q": "string",
|
||||||
|
"providers": ["yt", "dm", "tw", "pt", "od", "ru"],
|
||||||
|
"groups": {
|
||||||
|
"yt": [
|
||||||
|
{ "id": "string", "title": "string", "thumbnail": "string", "uploaderName": "string", "url": "string", "type": "video" }
|
||||||
|
],
|
||||||
|
"dm": [],
|
||||||
|
"tw": [],
|
||||||
|
"pt": [],
|
||||||
|
"od": [],
|
||||||
|
"ru": []
|
||||||
|
},
|
||||||
|
"page": 1,
|
||||||
|
"pageSize": 24
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Error responses
|
||||||
|
- 400 { error: "invalid_query", details: "min_length_2" }
|
||||||
|
- 500 { error: "search_failed", details: "..." }
|
||||||
|
|
||||||
|
Providers
|
||||||
|
- yt: YouTube
|
||||||
|
- dm: Dailymotion
|
||||||
|
- tw: Twitch
|
||||||
|
- pt: PeerTube
|
||||||
|
- od: Odysee
|
||||||
|
- ru: Rumble
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Each provider is queried in parallel with a per-provider limit equal to `pageSize`.
|
||||||
|
- The endpoint currently does not expose a `total` field; the frontend should offer a simple Next page affordance or infinite scroll when appropriate.
|
||||||
|
- Future: support for `sort`.
|
BIN
db/newtube.db
BIN
db/newtube.db
Binary file not shown.
BIN
server/db/newtube.db
Normal file
BIN
server/db/newtube.db
Normal file
Binary file not shown.
@ -2,6 +2,24 @@ import express from 'express';
|
|||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
|
// Configuration CORS
|
||||||
|
const corsOptions = {
|
||||||
|
origin: ['http://localhost:4200', 'http://localhost:4000', 'http://localhost:3000'],
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
credentials: true,
|
||||||
|
maxAge: 86400 // 24h
|
||||||
|
};
|
||||||
|
|
||||||
|
// Middleware de logging
|
||||||
|
const requestLogger = (req, res, next) => {
|
||||||
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
|
||||||
|
console.log('Headers:', JSON.stringify(req.headers, null, 2));
|
||||||
|
console.log('Query:', JSON.stringify(req.query, null, 2));
|
||||||
|
console.log('Body:', JSON.stringify(req.body, null, 2));
|
||||||
|
next();
|
||||||
|
};
|
||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
@ -117,11 +135,16 @@ app.use(helmet({
|
|||||||
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
||||||
}));
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
app.use(cors({
|
app.use(cors(corsOptions));
|
||||||
origin: true,
|
app.options('*', cors(corsOptions)); // Pré-vol CORS
|
||||||
credentials: true,
|
|
||||||
}));
|
// Logging des requêtes
|
||||||
|
app.use(requestLogger);
|
||||||
|
|
||||||
|
// Routes API
|
||||||
|
app.use('/api', r);
|
||||||
|
|
||||||
// Downloads directory
|
// Downloads directory
|
||||||
const downloadsRoot = path.join(process.cwd(), 'tmp', 'downloads');
|
const downloadsRoot = path.join(process.cwd(), 'tmp', 'downloads');
|
||||||
@ -1390,10 +1413,20 @@ app.all('/api/twitch-auth/*', (req, res) => forwardJson(req, res, 'https://id.tw
|
|||||||
// -------------------- Unified search endpoint (GET) --------------------
|
// -------------------- Unified search endpoint (GET) --------------------
|
||||||
app.get('/api/search', async (req, res) => {
|
app.get('/api/search', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[SEARCH] Requête reçue - Query:', req.query);
|
||||||
const { q, providers } = req.query;
|
const { q, providers } = req.query;
|
||||||
if (!q || typeof q !== 'string' || q.trim().length === 0) {
|
|
||||||
return res.status(400).json({ error: 'missing_query' });
|
// Validation des paramètres
|
||||||
|
if (!q || typeof q !== 'string' || q.trim().length < 2) {
|
||||||
|
console.log('[SEARCH] Requête invalide - q manquant ou trop court:', q);
|
||||||
|
return res.status(400).json({ error: 'q is required and must be at least 2 characters long' });
|
||||||
}
|
}
|
||||||
|
const pageNum = Math.max(1, Number(req.query.page || 1));
|
||||||
|
const pageSize = Math.min(50, Math.max(1, Number(req.query.pageSize || 24)));
|
||||||
|
// Optional sort parameter (normalized to known set)
|
||||||
|
const allowedSort = new Set(['relevance', 'date', 'views']);
|
||||||
|
let sort = (typeof req.query.sort === 'string' ? req.query.sort.trim().toLowerCase() : 'relevance');
|
||||||
|
if (!allowedSort.has(sort)) sort = 'relevance';
|
||||||
// Validate and normalize providers list (default to all supported when none/invalid)
|
// Validate and normalize providers list (default to all supported when none/invalid)
|
||||||
const requested = typeof providers === 'string' ? String(providers) : '';
|
const requested = typeof providers === 'string' ? String(providers) : '';
|
||||||
const validProviders = validateProviders(requested);
|
const validProviders = validateProviders(requested);
|
||||||
@ -1403,8 +1436,8 @@ app.get('/api/search', async (req, res) => {
|
|||||||
validProviders.map((providerId) => {
|
validProviders.map((providerId) => {
|
||||||
const mod = providerRegistry[/** @type {any} */(providerId)];
|
const mod = providerRegistry[/** @type {any} */(providerId)];
|
||||||
if (!mod || typeof mod.search !== 'function') return Promise.resolve([]);
|
if (!mod || typeof mod.search !== 'function') return Promise.resolve([]);
|
||||||
// Basic options: limit per provider; could be extended with page, etc.
|
// Basic options include pagination and sort hints
|
||||||
return Promise.resolve().then(() => mod.search(q, { limit: 10 }));
|
return Promise.resolve().then(() => mod.search(q, { limit: pageSize, page: pageNum, sort }));
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1420,7 +1453,7 @@ app.get('/api/search', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.json({ q, providers: validProviders, groups });
|
return res.json({ q, providers: validProviders, groups, page: pageNum, pageSize, sort });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(500).json({ error: 'search_failed', details: String(e?.message || e) });
|
return res.status(500).json({ error: 'search_failed', details: String(e?.message || e) });
|
||||||
}
|
}
|
||||||
|
@ -10,18 +10,22 @@
|
|||||||
* @property {string=} type
|
* @property {string=} type
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @type {{ id: 'yt', label: string, search: (q: string, opts: { limit: number, page?: number }) => Promise<Suggestion[]> }} */
|
/** @type {{ id: 'yt', label: string, search: (q: string, opts: { limit: number, page?: number, sort?: 'relevance'|'date'|'views' }) => Promise<Suggestion[]> }} */
|
||||||
const handler = {
|
const handler = {
|
||||||
id: 'yt',
|
id: 'yt',
|
||||||
label: 'YouTube',
|
label: 'YouTube',
|
||||||
async search(q, opts) {
|
async search(q, opts) {
|
||||||
const { limit = 10 } = opts;
|
const { limit = 10, sort = 'relevance' } = opts || {};
|
||||||
try {
|
try {
|
||||||
const API_KEY = process.env.YOUTUBE_API_KEY;
|
const API_KEY = process.env.YOUTUBE_API_KEY;
|
||||||
if (!API_KEY) {
|
if (!API_KEY) {
|
||||||
throw new Error('YOUTUBE_API_KEY not configured');
|
throw new Error('YOUTUBE_API_KEY not configured');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let order = 'relevance';
|
||||||
|
if (sort === 'date') order = 'date';
|
||||||
|
else if (sort === 'views') order = 'viewCount';
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://www.googleapis.com/youtube/v3/search?` +
|
`https://www.googleapis.com/youtube/v3/search?` +
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
@ -30,7 +34,7 @@ const handler = {
|
|||||||
type: 'video',
|
type: 'video',
|
||||||
maxResults: Math.min(limit, 50).toString(),
|
maxResults: Math.min(limit, 50).toString(),
|
||||||
key: API_KEY,
|
key: API_KEY,
|
||||||
order: 'relevance'
|
order
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
24
src/app/search/adapters/base.ts
Normal file
24
src/app/search/adapters/base.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult, SearchItem, ProviderId } from '../models';
|
||||||
|
|
||||||
|
export abstract class HttpAdapter implements ProviderAdapter {
|
||||||
|
abstract key: ProviderId;
|
||||||
|
abstract label: string;
|
||||||
|
constructor(protected http: HttpClient) {}
|
||||||
|
abstract search(params: ProviderSearchParams, signal: AbortSignal): Promise<SearchResult>;
|
||||||
|
|
||||||
|
protected mapItem(partial: Partial<SearchItem> & { id: string; title: string; watchUrl: string }): SearchItem {
|
||||||
|
return {
|
||||||
|
provider: this.key,
|
||||||
|
channel: undefined,
|
||||||
|
durationSec: undefined,
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: false,
|
||||||
|
isShort: false,
|
||||||
|
thumbUrl: undefined,
|
||||||
|
...partial,
|
||||||
|
} as SearchItem;
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/dm.ts
Normal file
26
src/app/search/adapters/dm.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class DmAdapter implements ProviderAdapter {
|
||||||
|
key = 'dm' as const;
|
||||||
|
label = 'Dailymotion';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'dm' } }));
|
||||||
|
const items = (res?.groups?.dm || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: it.type === 'live',
|
||||||
|
isShort: it.isShort === true
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.dm) ? res.groups.dm.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/od.ts
Normal file
26
src/app/search/adapters/od.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class OdAdapter implements ProviderAdapter {
|
||||||
|
key = 'od' as const;
|
||||||
|
label = 'Odysee';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'od' } }));
|
||||||
|
const items = (res?.groups?.od || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: false,
|
||||||
|
isShort: it.isShort === true
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.od) ? res.groups.od.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/pt.ts
Normal file
26
src/app/search/adapters/pt.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class PtAdapter implements ProviderAdapter {
|
||||||
|
key = 'pt' as const;
|
||||||
|
label = 'PeerTube';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'pt' } }));
|
||||||
|
const items = (res?.groups?.pt || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: false,
|
||||||
|
isShort: false
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.pt) ? res.groups.pt.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/ru.ts
Normal file
26
src/app/search/adapters/ru.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class RuAdapter implements ProviderAdapter {
|
||||||
|
key = 'ru' as const;
|
||||||
|
label = 'Rumble';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'ru' } }));
|
||||||
|
const items = (res?.groups?.ru || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: false,
|
||||||
|
isShort: false
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.ru) ? res.groups.ru.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/tw.ts
Normal file
26
src/app/search/adapters/tw.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class TwAdapter implements ProviderAdapter {
|
||||||
|
key = 'tw' as const;
|
||||||
|
label = 'Twitch';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'tw' } }));
|
||||||
|
const items = (res?.groups?.tw || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: it.type === 'live',
|
||||||
|
isShort: false
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.tw) ? res.groups.tw.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
26
src/app/search/adapters/yt.ts
Normal file
26
src/app/search/adapters/yt.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from '../models';
|
||||||
|
|
||||||
|
export class YtAdapter implements ProviderAdapter {
|
||||||
|
key = 'yt' as const;
|
||||||
|
label = 'YouTube';
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
async search(params: ProviderSearchParams, _signal: AbortSignal): Promise<SearchResult> {
|
||||||
|
const res = await firstValueFrom(this.http.get<any>(`/api/search`, { params: { q: params.q, providers: 'yt' } }));
|
||||||
|
const items = (res?.groups?.yt || []).map((it: any) => ({
|
||||||
|
id: it.id,
|
||||||
|
provider: this.key,
|
||||||
|
title: it.title,
|
||||||
|
channel: it.uploaderName,
|
||||||
|
durationSec: it.duration,
|
||||||
|
thumbUrl: it.thumbnail,
|
||||||
|
watchUrl: it.url || '',
|
||||||
|
views: undefined,
|
||||||
|
publishedAt: undefined,
|
||||||
|
isLive: it.type === 'live',
|
||||||
|
isShort: it.isShort === true
|
||||||
|
}));
|
||||||
|
return { items, total: Array.isArray(res?.groups?.yt) ? res.groups.yt.length : 0 };
|
||||||
|
}
|
||||||
|
}
|
21
src/app/search/api.v1.ts
Normal file
21
src/app/search/api.v1.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export type ProviderId = 'yt' | 'dm' | 'tw' | 'pt' | 'od' | 'ru';
|
||||||
|
|
||||||
|
export interface SuggestionItemV1 {
|
||||||
|
title: string;
|
||||||
|
id: string;
|
||||||
|
duration?: number;
|
||||||
|
isShort?: boolean;
|
||||||
|
thumbnail?: string;
|
||||||
|
uploaderName?: string;
|
||||||
|
url?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResponseV1 {
|
||||||
|
q: string;
|
||||||
|
providers: ProviderId[];
|
||||||
|
groups: Record<ProviderId, SuggestionItemV1[]>;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sort?: 'relevance' | 'date' | 'views';
|
||||||
|
}
|
36
src/app/search/models.ts
Normal file
36
src/app/search/models.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
export type ProviderId = 'yt'|'dm'|'tw'|'pt'|'od'|'ru';
|
||||||
|
|
||||||
|
export interface SearchItem {
|
||||||
|
id: string;
|
||||||
|
provider: ProviderId;
|
||||||
|
title: string;
|
||||||
|
channel?: string;
|
||||||
|
durationSec?: number;
|
||||||
|
views?: number;
|
||||||
|
publishedAt?: string; // ISO
|
||||||
|
isLive?: boolean;
|
||||||
|
isShort?: boolean;
|
||||||
|
thumbUrl?: string;
|
||||||
|
watchUrl: string; // internal or external
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
items: SearchItem[];
|
||||||
|
total?: number;
|
||||||
|
nextPageToken?: string; // or page: string|number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderSearchParams {
|
||||||
|
q: string;
|
||||||
|
pageToken?: string; // or page
|
||||||
|
sort?: 'relevance'|'date'|'views'|'duration';
|
||||||
|
time?: 'today'|'7d'|'30d'|'all';
|
||||||
|
length?: 'short'|'medium'|'long'|'all';
|
||||||
|
type?: 'video'|'live'|'shorts'|'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderAdapter {
|
||||||
|
key: ProviderId;
|
||||||
|
label: string;
|
||||||
|
search(params: ProviderSearchParams, signal: AbortSignal): Promise<SearchResult>;
|
||||||
|
}
|
@ -1,49 +1,126 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, debounceTime, map, switchMap } from 'rxjs';
|
import { BehaviorSubject, combineLatest, distinctUntilChanged, debounceTime, map, switchMap, filter, shareReplay, from, of } from 'rxjs';
|
||||||
import type { ProviderId } from '../core/providers/provider-registry';
|
import type { ProviderId } from '../core/providers/provider-registry';
|
||||||
|
import type { SuggestionItemV1, SearchResponseV1 } from './api.v1';
|
||||||
|
import { YtAdapter } from './adapters/yt';
|
||||||
|
import { DmAdapter } from './adapters/dm';
|
||||||
|
import { TwAdapter } from './adapters/tw';
|
||||||
|
import { PtAdapter } from './adapters/pt';
|
||||||
|
import { OdAdapter } from './adapters/od';
|
||||||
|
import { RuAdapter } from './adapters/ru';
|
||||||
|
import type { ProviderAdapter, ProviderSearchParams, SearchResult } from './models';
|
||||||
|
|
||||||
export interface SuggestionItem {
|
export type SuggestionItem = SuggestionItemV1;
|
||||||
title: string;
|
export type SearchResponse = SearchResponseV1;
|
||||||
id: string;
|
|
||||||
duration?: number;
|
|
||||||
isShort?: boolean;
|
|
||||||
thumbnail?: string;
|
|
||||||
uploaderName?: string;
|
|
||||||
url?: string;
|
|
||||||
type?: string; // 'video' | 'channel' | ...
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResponse {
|
type CacheKey = string;
|
||||||
q: string;
|
interface CacheEntry { t: number; data: SearchResult; }
|
||||||
providers: ProviderId[];
|
|
||||||
groups: Record<ProviderId, SuggestionItem[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
|
|
||||||
|
// Subjects for params from UI/URL
|
||||||
readonly q$ = new BehaviorSubject<string>('');
|
readonly q$ = new BehaviorSubject<string>('');
|
||||||
readonly providers$ = new BehaviorSubject<ProviderId[] | 'all'>('all');
|
readonly providers$ = new BehaviorSubject<ProviderId[] | 'all'>('all');
|
||||||
|
readonly page$ = new BehaviorSubject<number>(1);
|
||||||
|
readonly pageSize$ = new BehaviorSubject<number>(24);
|
||||||
|
readonly sort$ = new BehaviorSubject<'relevance' | 'date' | 'views' | 'duration'>('relevance');
|
||||||
|
|
||||||
readonly params$ = combineLatest([this.q$, this.providers$]).pipe(
|
// In-memory cache 60s per (provider, q, params)
|
||||||
|
private cache = new Map<CacheKey, CacheEntry>();
|
||||||
|
private cacheTtlMs = 60_000;
|
||||||
|
|
||||||
|
// Adapters registry
|
||||||
|
private adapters: Record<ProviderId, ProviderAdapter> = {
|
||||||
|
yt: new YtAdapter(this.http),
|
||||||
|
dm: new DmAdapter(this.http),
|
||||||
|
tw: new TwAdapter(this.http),
|
||||||
|
pt: new PtAdapter(this.http),
|
||||||
|
od: new OdAdapter(this.http),
|
||||||
|
ru: new RuAdapter(this.http)
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly params$ = combineLatest([this.q$, this.providers$, this.page$, this.pageSize$, this.sort$]).pipe(
|
||||||
debounceTime(120),
|
debounceTime(120),
|
||||||
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
|
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Public request stream: aggregates groups by provider, runs adapters in parallel with timeout/abort/retry
|
||||||
readonly request$ = this.params$.pipe(
|
readonly request$ = this.params$.pipe(
|
||||||
switchMap(([q, prov]) => {
|
filter(([q]) => typeof q === 'string' && q.trim().length >= 2),
|
||||||
const providersParam = (prov === 'all')
|
switchMap(([q, prov, page, pageSize, sort]) => from(this.runAdapters({
|
||||||
? 'yt,dm,tw,pt,od,ru'
|
q: String(q),
|
||||||
: (Array.isArray(prov) && prov.length > 0 ? prov.join(',') : '');
|
pageToken: String(page || 1),
|
||||||
return this.http.get<SearchResponse>('/api/search', {
|
sort: (sort as any) || 'relevance',
|
||||||
params: { q, providers: providersParam }
|
// global filter knobs could be added here: time/length/type in the future
|
||||||
});
|
}, prov)))
|
||||||
})
|
,
|
||||||
|
// Map normalized items to legacy "groups" response expected by UI
|
||||||
|
map((byProvider) => {
|
||||||
|
const groups = Object.entries(byProvider).reduce((acc, [pid, result]) => {
|
||||||
|
const arr: SuggestionItemV1[] = (result?.items || []).map(it => ({
|
||||||
|
id: it.id,
|
||||||
|
title: it.title,
|
||||||
|
duration: it.durationSec,
|
||||||
|
isShort: it.isShort,
|
||||||
|
thumbnail: it.thumbUrl,
|
||||||
|
uploaderName: it.channel,
|
||||||
|
url: it.watchUrl,
|
||||||
|
type: it.isLive ? 'live' : 'video'
|
||||||
|
}));
|
||||||
|
(acc as any)[pid as ProviderId] = arr;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<ProviderId, SuggestionItemV1[]>);
|
||||||
|
const providers = Object.keys(byProvider) as ProviderId[];
|
||||||
|
const resp: SearchResponse = { q: this.q$.value, providers, groups };
|
||||||
|
return resp;
|
||||||
|
}),
|
||||||
|
shareReplay(1)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Orchestrate all active providers in parallel with timeout(8s), retry(1) and abort on param change.
|
||||||
|
private async runAdapters(params: ProviderSearchParams, prov: ProviderId[] | 'all') {
|
||||||
|
const active: ProviderId[] = prov === 'all' ? ['yt','dm','tw','pt','od','ru'] : (Array.isArray(prov) ? prov : []);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const signal = controller.signal;
|
||||||
|
const tasks = active.map(async (pid) => {
|
||||||
|
const adapter = this.adapters[pid];
|
||||||
|
if (!adapter) return [pid, { items: [] as any[] } as SearchResult] as const;
|
||||||
|
const key: CacheKey = `${pid}|${params.q}|${params.pageToken}|${params.sort}`;
|
||||||
|
const now = Date.now();
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
if (cached && (now - cached.t) < this.cacheTtlMs) {
|
||||||
|
return [pid, cached.data] as const;
|
||||||
|
}
|
||||||
|
const perProviderTimeout = 8000;
|
||||||
|
const withTimeout = <T>(p: Promise<T>): Promise<T> => new Promise((resolve, reject) => {
|
||||||
|
const tid = setTimeout(() => reject(new Error('timeout')), perProviderTimeout);
|
||||||
|
p.then(v => { clearTimeout(tid); resolve(v); }).catch(e => { clearTimeout(tid); reject(e); });
|
||||||
|
});
|
||||||
|
const attempt = async (): Promise<SearchResult> => withTimeout(adapter.search(params, signal));
|
||||||
|
try {
|
||||||
|
const res = await attempt().catch(async (e) => {
|
||||||
|
// retry once on network-like errors
|
||||||
|
if (e && (e.name === 'AbortError' || String(e.message || '').includes('abort'))) throw e;
|
||||||
|
try { return await attempt(); } catch (err) { throw err; }
|
||||||
|
});
|
||||||
|
this.cache.set(key, { t: Date.now(), data: res });
|
||||||
|
return [pid, res] as const;
|
||||||
|
} catch {
|
||||||
|
// Swallow errors per provider, return empty
|
||||||
|
return [pid, { items: [] }] as const;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const results = await Promise.all(tasks);
|
||||||
|
return Object.fromEntries(results) as Record<ProviderId, SearchResult>;
|
||||||
|
}
|
||||||
|
|
||||||
// Convenience setter helpers
|
// Convenience setter helpers
|
||||||
setQuery(q: string) { this.q$.next(q || ''); }
|
setQuery(q: string) { this.q$.next(q || ''); }
|
||||||
setProviders(list: ProviderId[] | 'all') { this.providers$.next(list && (Array.isArray(list) ? list : 'all')); }
|
setProviders(list: ProviderId[] | 'all') { this.providers$.next(list && (Array.isArray(list) ? list : 'all')); }
|
||||||
|
setPage(page: number) { this.page$.next(Math.max(1, Math.floor(page || 1))); }
|
||||||
|
setPageSize(size: number) { this.pageSize$.next(Math.min(50, Math.max(1, Math.floor(size || 24)))); }
|
||||||
|
setSort(sort: 'relevance' | 'date' | 'views' | 'duration') { this.sort$.next(sort || 'relevance'); }
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
<ul *ngIf="!isLoading() && filteredSearchHistory().length > 0" class="space-y-3">
|
<ul *ngIf="!isLoading() && filteredSearchHistory().length > 0" class="space-y-3">
|
||||||
<li *ngFor="let s of filteredSearchHistory()"
|
<li *ngFor="let s of filteredSearchHistory()"
|
||||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||||
<a [routerLink]="['/search']" [queryParams]="{ q: s.query, provider: getSearchProvider(s).id }"
|
<a [routerLink]="['/search']" [queryParams]="getSearchProvider(s).providers ? { q: s.query, providers: getSearchProvider(s).providers } : { q: s.query, provider: getSearchProvider(s).id }"
|
||||||
class="block p-4 pr-16 hover:no-underline">
|
class="block p-4 pr-16 hover:no-underline">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-slate-400 mt-0.5">
|
<span class="text-slate-400 mt-0.5">
|
||||||
@ -80,15 +80,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1 text-xs text-slate-400">
|
<div class="flex items-center mt-1 text-xs text-slate-400">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||||
[ngStyle]="getProviderColors(getSearchProvider(s).id)"
|
[ngStyle]="getProviderColors(getSearchProvider(s).id)">
|
||||||
[ngClass]="{
|
|
||||||
'bg-red-600/15 text-red-400 border-red-500/30': getSearchProvider(s).id === 'youtube',
|
|
||||||
'bg-sky-600/15 text-sky-300 border-sky-500/30': getSearchProvider(s).id === 'vimeo',
|
|
||||||
'bg-blue-600/15 text-blue-300 border-blue-500/30': getSearchProvider(s).id === 'dailymotion',
|
|
||||||
'bg-amber-600/15 text-amber-300 border-amber-500/30': getSearchProvider(s).id === 'peertube',
|
|
||||||
'bg-green-600/15 text-green-300 border-green-500/30': getSearchProvider(s).id === 'rumble',
|
|
||||||
'bg-slate-800/80 text-slate-300 border-slate-700': !['youtube', 'vimeo', 'dailymotion', 'peertube', 'rumble'].includes(getSearchProvider(s).id)
|
|
||||||
}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -122,7 +114,7 @@
|
|||||||
<ul *ngIf="searchHistory().length > 0" class="space-y-3">
|
<ul *ngIf="searchHistory().length > 0" class="space-y-3">
|
||||||
<li *ngFor="let s of searchHistory()"
|
<li *ngFor="let s of searchHistory()"
|
||||||
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
class="group relative bg-slate-700/50 hover:bg-slate-700/80 rounded-lg overflow-hidden transition-all duration-200">
|
||||||
<a [routerLink]="['/search']" [queryParams]="{ q: s.query, provider: getSearchProvider(s).id }"
|
<a [routerLink]="['/search']" [queryParams]="getSearchProvider(s).providers ? { q: s.query, providers: getSearchProvider(s).providers } : { q: s.query, provider: getSearchProvider(s).id }"
|
||||||
class="block p-4 pr-16 hover:no-underline">
|
class="block p-4 pr-16 hover:no-underline">
|
||||||
<div class="flex items-start gap-2">
|
<div class="flex items-start gap-2">
|
||||||
<span class="text-slate-400 mt-0.5">
|
<span class="text-slate-400 mt-0.5">
|
||||||
@ -136,15 +128,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center mt-1 text-xs text-slate-400">
|
<div class="flex items-center mt-1 text-xs text-slate-400">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
<span class="inline-flex items-center px-2 py-0.5 rounded mr-2 border"
|
||||||
[ngStyle]="getProviderColors(getSearchProvider(s).id)"
|
[ngStyle]="getProviderColors(getSearchProvider(s).id)">
|
||||||
[ngClass]="{
|
|
||||||
'bg-red-600/15 text-red-400 border-red-500/30': getSearchProvider(s).id === 'youtube',
|
|
||||||
'bg-sky-600/15 text-sky-300 border-sky-500/30': getSearchProvider(s).id === 'vimeo',
|
|
||||||
'bg-blue-600/15 text-blue-300 border-blue-500/30': getSearchProvider(s).id === 'dailymotion',
|
|
||||||
'bg-amber-600/15 text-amber-300 border-amber-500/30': getSearchProvider(s).id === 'peertube',
|
|
||||||
'bg-green-600/15 text-green-300 border-green-500/30': getSearchProvider(s).id === 'rumble',
|
|
||||||
'bg-slate-800/80 text-slate-300 border-slate-700': !['youtube', 'vimeo', 'dailymotion', 'peertube', 'rumble'].includes(getSearchProvider(s).id)
|
|
||||||
}">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ChangeDetectionStrategy, Component, inject, signal, computed, effect } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, inject, signal, computed, effect } from '@angular/core';
|
||||||
import { NgClass } from '@angular/common';
|
|
||||||
import { CommonModule, DatePipe } from '@angular/common';
|
import { CommonModule, DatePipe } from '@angular/common';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
@ -12,7 +11,7 @@ import { HistoryService, SearchHistoryItem, WatchHistoryItem } from '../../../se
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './history.component.html',
|
templateUrl: './history.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, RouterModule, FormsModule, NgClass]
|
imports: [CommonModule, RouterModule, FormsModule]
|
||||||
})
|
})
|
||||||
export class HistoryComponent {
|
export class HistoryComponent {
|
||||||
private history = inject(HistoryService);
|
private history = inject(HistoryService);
|
||||||
@ -32,6 +31,20 @@ export class HistoryComponent {
|
|||||||
// Sujet pour la recherche avec debounce
|
// Sujet pour la recherche avec debounce
|
||||||
private searchTerms = new Subject<string>();
|
private searchTerms = new Subject<string>();
|
||||||
|
|
||||||
|
|
||||||
|
// Obtenir le nom d'affichage d'un provider
|
||||||
|
getProviderName(providerId: string): string {
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
'youtube': 'YouTube',
|
||||||
|
'dailymotion': 'Dailymotion',
|
||||||
|
'twitch': 'Twitch',
|
||||||
|
'peertube': 'PeerTube',
|
||||||
|
'odysee': 'Odysee',
|
||||||
|
'rumble': 'Rumble'
|
||||||
|
};
|
||||||
|
return names[providerId.toLowerCase()] || providerId;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Chargement initial des données
|
// Chargement initial des données
|
||||||
this.reload();
|
this.reload();
|
||||||
@ -151,13 +164,25 @@ export class HistoryComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get provider from search filters or query params
|
getSearchProvider(item: SearchHistoryItem): { name: string; id: string; providers?: string[] } {
|
||||||
getSearchProvider(item: SearchHistoryItem): { name: string; id: string } {
|
// Try to get providers from filters first
|
||||||
// Try to get provider from filters first
|
|
||||||
if (item.filters_json) {
|
if (item.filters_json) {
|
||||||
try {
|
try {
|
||||||
const filters = JSON.parse(item.filters_json);
|
const filters = JSON.parse(item.filters_json);
|
||||||
if (filters.provider) {
|
if (filters.providers) {
|
||||||
|
// Multiple providers available
|
||||||
|
const providers = Array.isArray(filters.providers) ? filters.providers : [filters.providers];
|
||||||
|
if (providers.length === 1) {
|
||||||
|
const providerName = this.formatProviderName(providers[0]);
|
||||||
|
return { name: providerName, id: providers[0].toLowerCase() };
|
||||||
|
} else {
|
||||||
|
const firstProviderName = this.formatProviderName(providers[0]);
|
||||||
|
const othersCount = providers.length - 1;
|
||||||
|
const name = `${firstProviderName} + ${othersCount} autre${othersCount > 1 ? 's' : ''}`;
|
||||||
|
return { name: name, id: providers[0], providers: providers };
|
||||||
|
}
|
||||||
|
} else if (filters.provider) {
|
||||||
|
// Single provider
|
||||||
const providerName = this.formatProviderName(filters.provider);
|
const providerName = this.formatProviderName(filters.provider);
|
||||||
return { name: providerName, id: String(filters.provider).toLowerCase() };
|
return { name: providerName, id: String(filters.provider).toLowerCase() };
|
||||||
}
|
}
|
||||||
@ -178,16 +203,10 @@ export class HistoryComponent {
|
|||||||
// Not a valid URL or no provider in query params
|
// Not a valid URL or no provider in query params
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to the selected provider if available
|
|
||||||
const defaultProvider = this.getDefaultProvider();
|
|
||||||
if (defaultProvider) {
|
|
||||||
return { name: this.formatProviderName(defaultProvider), id: defaultProvider.toLowerCase() };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to 'all' if no provider can be determined
|
// Fallback to 'all' if no provider can be determined
|
||||||
return { name: 'Tous', id: 'all' };
|
return { name: 'Tous', id: 'all' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the default provider from the current URL or settings
|
// Get the default provider from the current URL or settings
|
||||||
private getDefaultProvider(): string | null {
|
private getDefaultProvider(): string | null {
|
||||||
try {
|
try {
|
||||||
@ -195,11 +214,11 @@ export class HistoryComponent {
|
|||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
const provider = url.searchParams.get('provider');
|
const provider = url.searchParams.get('provider');
|
||||||
if (provider) return provider.toLowerCase();
|
if (provider) return provider.toLowerCase();
|
||||||
|
|
||||||
// Or get from localStorage if available
|
// Or get from localStorage if available
|
||||||
const savedProvider = localStorage.getItem('selectedProvider');
|
const savedProvider = localStorage.getItem('selectedProvider');
|
||||||
if (savedProvider) return savedProvider.toLowerCase();
|
if (savedProvider) return savedProvider.toLowerCase();
|
||||||
|
|
||||||
// Default to youtube if nothing else
|
// Default to youtube if nothing else
|
||||||
return 'youtube';
|
return 'youtube';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -207,6 +226,10 @@ export class HistoryComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProvidersWithInfo(item: SearchHistoryItem) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
// Format provider ID to a display name
|
// Format provider ID to a display name
|
||||||
private formatProviderName(providerId: string): string {
|
private formatProviderName(providerId: string): string {
|
||||||
const providerMap: { [key: string]: string } = {
|
const providerMap: { [key: string]: string } = {
|
||||||
@ -225,27 +248,66 @@ export class HistoryComponent {
|
|||||||
return this.formatProviderName(providerId);
|
return this.formatProviderName(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brand colors for provider badges (inline to avoid Tailwind purge issues)
|
|
||||||
getProviderColors(providerId: string): { [key: string]: string } {
|
// Obtenir les styles pour un provider
|
||||||
const id = (providerId || '').toLowerCase();
|
getProviderColors(providerId: string | null | undefined): { [key: string]: string } {
|
||||||
|
if (!providerId) {
|
||||||
|
return {
|
||||||
|
'background-color': 'rgb(51 65 85 / 0.15)',
|
||||||
|
'color': 'rgb(148 163 184)',
|
||||||
|
'border-color': 'rgb(71 85 105 / 0.3)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = providerId.toLowerCase();
|
||||||
|
let bgColor = 'rgb(51 65 85 / 0.15)';
|
||||||
|
let textColor = 'rgb(148 163 184)';
|
||||||
|
let borderColor = 'rgb(71 85 105 / 0.3)';
|
||||||
|
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case 'youtube':
|
case 'youtube':
|
||||||
return { backgroundColor: 'rgba(220, 38, 38, 0.15)', color: 'rgb(248, 113, 113)', borderColor: 'rgba(239, 68, 68, 0.3)' };
|
case 'yt':
|
||||||
case 'vimeo':
|
bgColor = 'rgb(220 38 38 / 0.15)';
|
||||||
return { backgroundColor: 'rgba(2, 132, 199, 0.15)', color: 'rgb(125, 211, 252)', borderColor: 'rgba(14, 165, 233, 0.3)' };
|
textColor = 'rgb(248 113 113)';
|
||||||
|
borderColor = 'rgb(239 68 68 / 0.3)';
|
||||||
|
break;
|
||||||
case 'dailymotion':
|
case 'dailymotion':
|
||||||
return { backgroundColor: 'rgba(37, 99, 235, 0.15)', color: 'rgb(147, 197, 253)', borderColor: 'rgba(59, 130, 246, 0.3)' };
|
case 'dm':
|
||||||
case 'peertube':
|
bgColor = 'rgb(37 99 235 / 0.15)';
|
||||||
return { backgroundColor: 'rgba(245, 158, 11, 0.15)', color: 'rgb(252, 211, 77)', borderColor: 'rgba(245, 158, 11, 0.3)' };
|
textColor = 'rgb(96 165 250)';
|
||||||
case 'rumble':
|
borderColor = 'rgb(59 130 246 / 0.3)';
|
||||||
return { backgroundColor: 'rgba(22, 163, 74, 0.15)', color: 'rgb(134, 239, 172)', borderColor: 'rgba(34, 197, 94, 0.3)' };
|
break;
|
||||||
case 'twitch':
|
case 'twitch':
|
||||||
return { backgroundColor: 'rgba(168, 85, 247, 0.15)', color: 'rgb(216, 180, 254)', borderColor: 'rgba(168, 85, 247, 0.3)' };
|
case 'tw':
|
||||||
|
bgColor = 'rgb(147 51 234 / 0.15)';
|
||||||
|
textColor = 'rgb(192 132 252)';
|
||||||
|
borderColor = 'rgb(168 85 247 / 0.3)';
|
||||||
|
break;
|
||||||
|
case 'peertube':
|
||||||
|
case 'pt':
|
||||||
|
bgColor = 'rgb(217 119 6 / 0.15)';
|
||||||
|
textColor = 'rgb(251 191 36)';
|
||||||
|
borderColor = 'rgb(245 158 11 / 0.3)';
|
||||||
|
break;
|
||||||
case 'odysee':
|
case 'odysee':
|
||||||
return { backgroundColor: 'rgba(236, 72, 153, 0.15)', color: 'rgb(251, 207, 232)', borderColor: 'rgba(236, 72, 153, 0.3)' };
|
case 'od':
|
||||||
default:
|
bgColor = 'rgb(22 163 74 / 0.15)';
|
||||||
return { backgroundColor: 'rgba(30, 41, 59, 0.8)', color: 'rgb(203, 213, 225)', borderColor: 'rgb(51, 65, 85)' };
|
textColor = 'rgb(74 222 128)';
|
||||||
|
borderColor = 'rgb(34 197 94 / 0.3)';
|
||||||
|
break;
|
||||||
|
case 'rumble':
|
||||||
|
case 'ru':
|
||||||
|
bgColor = 'rgb(234 88 12 / 0.15)';
|
||||||
|
textColor = 'rgb(251 146 60)';
|
||||||
|
borderColor = 'rgb(249 115 22 / 0.3)';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'background-color': bgColor,
|
||||||
|
'color': textColor,
|
||||||
|
'border-color': borderColor
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format seconds to HH:MM:SS or MM:SS
|
// Format seconds to HH:MM:SS or MM:SS
|
||||||
@ -255,7 +317,6 @@ export class HistoryComponent {
|
|||||||
const hours = Math.floor(seconds / 3600);
|
const hours = Math.floor(seconds / 3600);
|
||||||
const minutes = Math.floor((seconds % 3600) / 60);
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
|
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,6 +102,8 @@ export class HeaderComponent {
|
|||||||
qp.providers = 'yt,dm,tw,pt,od,ru';
|
qp.providers = 'yt,dm,tw,pt,od,ru';
|
||||||
}
|
}
|
||||||
if (theme) qp.theme = theme;
|
if (theme) qp.theme = theme;
|
||||||
|
try { console.debug('[Header] onSearchBoxSubmit -> navigate', qp); } catch {}
|
||||||
|
this.suggestionsOpen.set(false);
|
||||||
this.router.navigate(['/search'], { queryParams: qp });
|
this.router.navigate(['/search'], { queryParams: qp });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<form class="w-full relative" (submit)="onSubmit($event)" (keydown)="handleKeydown($event)">
|
<form class="w-full relative" novalidate (ngSubmit)="onSubmit($event)" (submit)="onSubmit($event)" (keydown)="handleKeydown($event)" role="search" aria-label="Recherche">
|
||||||
<!-- Input -->
|
<!-- Input -->
|
||||||
<div class="flex items-center gap-2 rounded-lg ring-1 ring-slate-600/50 bg-slate-800/70 px-3 py-2">
|
<div class="flex items-center gap-2 rounded-lg ring-1 ring-slate-600/50 bg-slate-800/70 px-3 py-2">
|
||||||
<!-- Provider chips -->
|
<!-- Provider chips -->
|
||||||
@ -25,14 +25,20 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- Text input -->
|
<!-- Text input -->
|
||||||
<input type="search"
|
<input #qInput type="search"
|
||||||
class="flex-1 bg-transparent outline-none text-slate-100 placeholder-slate-400 px-2"
|
class="flex-1 bg-transparent outline-none text-slate-100 placeholder-slate-400 px-2"
|
||||||
[placeholder]="placeholder"
|
[placeholder]="placeholder"
|
||||||
[ngModel]="query()" (ngModelChange)="queryUpdate($event)" (keydown)="handleKeydown($event)" name="q"/>
|
[ngModel]="query()" (ngModelChange)="queryUpdate($event)" (keydown)="handleKeydown($event)" (keydown.enter)="onSubmit($event)" name="q"
|
||||||
|
autocomplete="off" aria-label="Rechercher des vidéos"/>
|
||||||
|
|
||||||
<!-- Actions -->
|
<!-- Actions -->
|
||||||
<button type="button" (click)="openPicker()" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600">@</button>
|
<button type="button" (click)="openPicker()" class="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600" aria-label="Choisir des fournisseurs">@</button>
|
||||||
<button type="submit" class="ml-1 px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white">Search</button>
|
<button *ngIf="(query() || '').length > 0" type="button" (click)="queryUpdate('')" class="text-xs px-2 py-1 rounded hover:bg-slate-700/70" aria-label="Effacer la recherche">×</button>
|
||||||
|
<button type="submit" class="ml-1 px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white"
|
||||||
|
(click)="onSubmit($event)"
|
||||||
|
[attr.title]="(query() || '').trim().length < 2 ? 'Tapez au moins 2 caractères' : null">
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Provider Picker Modal -->
|
<!-- Provider Picker Modal -->
|
||||||
|
@ -1,27 +1,44 @@
|
|||||||
import { Component, EventEmitter, Output, Input, signal, computed, effect, inject, HostListener } from '@angular/core';
|
import { Component, EventEmitter, Output, Input, signal, computed, effect, inject, HostListener, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { ProviderId, PROVIDERS } from '../../app/core/providers/provider-registry';
|
import { ProviderId, PROVIDERS } from '../../app/core/providers/provider-registry';
|
||||||
import { ProviderPickerComponent } from './provider-picker.component';
|
import { ProviderPickerComponent } from './provider-picker.component';
|
||||||
import { SearchService } from '../../app/search/search.service';
|
import { SearchService } from '../../app/search/search.service';
|
||||||
import { UserService, type UserPreferences } from '../../services/user.service';
|
import { UserService, type UserPreferences } from '../../services/user.service';
|
||||||
import { HistoryService, type SearchHistoryItem } from '../../services/history.service';
|
import { HistoryService, type SearchHistoryItem } from '../../services/history.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
// No overlays; we render fixed-position modals with high z-index
|
// No overlays; we render fixed-position modals with high z-index
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-search-box',
|
selector: 'app-search-box',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, FormsModule, ProviderPickerComponent],
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
ProviderPickerComponent,
|
||||||
|
HttpClientModule // Nécessaire pour SearchService
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
SearchService, // Fournir le service au niveau du composant
|
||||||
|
UserService, // S'assurer que UserService est fourni
|
||||||
|
HistoryService // S'assurer que HistoryService est fourni
|
||||||
|
],
|
||||||
templateUrl: './search-box.component.html'
|
templateUrl: './search-box.component.html'
|
||||||
})
|
})
|
||||||
export class SearchBoxComponent {
|
export class SearchBoxComponent {
|
||||||
private search = inject(SearchService);
|
private search = inject(SearchService);
|
||||||
private users = inject(UserService);
|
private users = inject(UserService);
|
||||||
private history = inject(HistoryService);
|
private history = inject(HistoryService);
|
||||||
|
@ViewChild('qInput', { static: false }) qInputRef?: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
// Input/Output bindings
|
// Input/Output bindings
|
||||||
@Input() placeholder: string = 'Search videos…';
|
@Input() placeholder: string = 'Search videos…';
|
||||||
@Output() submitted = new EventEmitter<{ q: string; providers: ProviderId[] | 'all' }>();
|
@Output() submitted = new EventEmitter<{ q: string; providers: ProviderId[] | 'all' }>();
|
||||||
|
// Emits debounced query changes for autosuggest
|
||||||
|
@Output() searchChange = new EventEmitter<string>();
|
||||||
|
|
||||||
// Local state signals
|
// Local state signals
|
||||||
query = signal('');
|
query = signal('');
|
||||||
@ -56,11 +73,23 @@ export class SearchBoxComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Flag to avoid clobbering user input after initial hydration from SearchService
|
||||||
|
let hydratedFromService = false;
|
||||||
|
// Debounced query change emitter for suggestions/autocomplete
|
||||||
|
this._qChanges.pipe(
|
||||||
|
debounceTime(300),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
takeUntilDestroyed()
|
||||||
|
).subscribe((value) => {
|
||||||
|
this.searchChange.emit((value || '').trim());
|
||||||
|
});
|
||||||
|
|
||||||
// Reflect SearchService into local state to restore from URL/preference
|
// Reflect SearchService into local state to restore from URL/preference
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
if (hydratedFromService) return; // only hydrate once
|
||||||
const q = this.search.q$.value;
|
const q = this.search.q$.value;
|
||||||
const prov = this.search.providers$.value;
|
const prov = this.search.providers$.value;
|
||||||
if (typeof q === 'string' && q !== this.query()) this.query.set(q);
|
if (typeof q === 'string') this.query.set(q);
|
||||||
if (prov === 'all') {
|
if (prov === 'all') {
|
||||||
this.providerMode.set('all');
|
this.providerMode.set('all');
|
||||||
this.selected.set([]);
|
this.selected.set([]);
|
||||||
@ -68,7 +97,7 @@ export class SearchBoxComponent {
|
|||||||
this.providerMode.set('custom');
|
this.providerMode.set('custom');
|
||||||
this.selected.set(prov);
|
this.selected.set(prov);
|
||||||
}
|
}
|
||||||
|
hydratedFromService = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply default providers from user preferences if none explicitly set
|
// Apply default providers from user preferences if none explicitly set
|
||||||
@ -89,7 +118,11 @@ export class SearchBoxComponent {
|
|||||||
|
|
||||||
// Update query and handle inline '@provider' prefix to open filtered picker
|
// Update query and handle inline '@provider' prefix to open filtered picker
|
||||||
queryUpdate(value: string) {
|
queryUpdate(value: string) {
|
||||||
|
try { console.log('[SearchBox] queryUpdate', { value }); } catch {}
|
||||||
this.query.set(value);
|
this.query.set(value);
|
||||||
|
// Keep SearchService in sync with current text to avoid stale overwrite
|
||||||
|
try { this.search.q$.next(value); } catch {}
|
||||||
|
this._qChanges.next(value);
|
||||||
const m = /(^|\s)@([a-z]{1,12})$/i.exec(value);
|
const m = /(^|\s)@([a-z]{1,12})$/i.exec(value);
|
||||||
if (m) {
|
if (m) {
|
||||||
const term = m[2] || '';
|
const term = m[2] || '';
|
||||||
@ -153,8 +186,14 @@ export class SearchBoxComponent {
|
|||||||
if (pid) { ev.preventDefault(); this.toggleChip(pid); }
|
if (pid) { ev.preventDefault(); this.toggleChip(pid); }
|
||||||
}
|
}
|
||||||
if (ev.key === 'Escape') {
|
if (ev.key === 'Escape') {
|
||||||
|
// Escape clears transient UI; if nothing is open, also clear the query
|
||||||
|
const hadAny = this.pickerOpen() || this.suggestionsOpen() || this.atOpen() || this.quickOpen();
|
||||||
this.pickerOpen.set(false);
|
this.pickerOpen.set(false);
|
||||||
this.suggestionsOpen.set(false);
|
this.suggestionsOpen.set(false);
|
||||||
|
this.atOpen.set(false);
|
||||||
|
if (!hadAny && (this.query() || '').length > 0) {
|
||||||
|
this.queryUpdate('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,14 +228,37 @@ export class SearchBoxComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(ev: Event) {
|
onSubmit(ev: Event) {
|
||||||
ev.preventDefault();
|
try { ev.preventDefault(); } catch {}
|
||||||
const q = (this.query() || '').trim();
|
let q = (this.query() || '').trim();
|
||||||
if (!q) return;
|
if ((!q || q.length === 0) && this.qInputRef?.nativeElement) {
|
||||||
|
try {
|
||||||
|
const raw = this.qInputRef.nativeElement.value || '';
|
||||||
|
q = String(raw).trim();
|
||||||
|
console.log('[SearchBox] fallback native value used', { q, qLength: q.length });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
// Debug: minimal console trace to confirm handler runs
|
||||||
|
try {
|
||||||
|
console.log('[SearchBox] onSubmit called', {
|
||||||
|
q,
|
||||||
|
qLength: q.length,
|
||||||
|
hasParentHandler: !!this.submitted.observers.length
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Error in SearchBox onSubmit:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.length < 2) {
|
||||||
|
console.log('[SearchBox] Query too short, not submitting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const prov = this.providersForRequest();
|
const prov = this.providersForRequest();
|
||||||
// Update service for global listeners
|
console.log('[SearchBox] Emitting submit event', { q, providers: prov });
|
||||||
this.search.setQuery(q);
|
|
||||||
this.search.setProviders(prov);
|
// Emit payload to parent (Header/Search page)
|
||||||
this.submitted.emit({ q, providers: prov });
|
this.submitted.emit({ q, providers: prov });
|
||||||
|
console.log('[SearchBox] Submit event emitted');
|
||||||
}
|
}
|
||||||
|
|
||||||
openPicker() { this.pickerOpen.set(true); }
|
openPicker() { this.pickerOpen.set(true); }
|
||||||
@ -244,4 +306,7 @@ export class SearchBoxComponent {
|
|||||||
document.body.style.overflow = open ? 'hidden' : '';
|
document.body.style.overflow = open ? 'hidden' : '';
|
||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Internal subject for debouncing query changes
|
||||||
|
private _qChanges = new Subject<string>();
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<div class="container mx-auto p-4 sm:p-6">
|
<div class="container mx-auto p-4 sm:p-6">
|
||||||
<h2 class="text-3xl font-bold mb-6 text-slate-100 border-l-4 border-red-500 pl-4">{{ pageHeading() }}</h2>
|
<header class="mb-4">
|
||||||
|
<h1 class="text-xl sm:text-2xl font-semibold text-slate-100">{{ pageHeading() }}</h1>
|
||||||
|
<p class="text-sm text-white/60 mt-1">{{ activeProvidersDisplay() }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
@if (notice()) {
|
@if (notice()) {
|
||||||
<div class="mb-4 bg-amber-900/40 border border-amber-600 text-amber-200 p-3 rounded">
|
<div class="mb-4 bg-amber-900/40 border border-amber-600 text-amber-200 p-3 rounded">
|
||||||
@ -27,21 +30,68 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Unified multi-provider grouped suggestions -->
|
<!-- Unified per-provider sections -->
|
||||||
@if (showUnified()) {
|
@if (showUnified()) {
|
||||||
<app-search-suggestions [groups]="groups()" [query]="q()"></app-search-suggestions>
|
<section *ngFor="let p of activeProviders()">
|
||||||
|
<div class="sticky top-0 z-10 flex items-center justify-between bg-black/50 backdrop-blur px-4 py-2 border-b border-white/10">
|
||||||
|
<h2 class="text-sm font-medium">{{ providerDisplay(p) }} <span class="text-white/40" *ngIf="groups()[p]">({{ groups()[p].length || 0 }})</span></h2>
|
||||||
|
<button class="inline-flex items-center rounded-xl px-3 py-1.5 text-xs border border-white/20 hover:bg-white/5"
|
||||||
|
(click)="onViewAll(p)"
|
||||||
|
aria-label="Voir tout sur {{ providerDisplay(p) }}">
|
||||||
|
Voir tout sur {{ providerDisplay(p) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="groups()[p]?.length; else noResTpl" class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4">
|
||||||
|
<a *ngFor="let v of groups()[p]"
|
||||||
|
class="group block rounded-2xl border border-white/10 bg-white/5 hover:bg-white/10 transition overflow-hidden"
|
||||||
|
[href]="v.url || '#'" target="_blank" rel="noopener noreferrer"
|
||||||
|
[attr.aria-label]="v.title">
|
||||||
|
<div class="relative">
|
||||||
|
<img [src]="v.thumbnail" [alt]="v.title" class="w-full h-44 object-cover" loading="lazy">
|
||||||
|
</div>
|
||||||
|
<div class="p-3">
|
||||||
|
<div class="text-sm font-medium text-slate-100 line-clamp-2">{{ v.title }}</div>
|
||||||
|
<div class="mt-1 text-xs text-white/60 line-clamp-1">{{ v.uploaderName }}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ng-template #noResTpl>
|
||||||
|
<div class="px-4 py-6 text-white/50 text-sm">Aucun résultat pour ce fournisseur.</div>
|
||||||
|
</ng-template>
|
||||||
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
<input
|
@if (hasQuery()) {
|
||||||
#searchInput
|
<div class="my-4 flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||||
type="text"
|
<div class="flex items-center gap-2">
|
||||||
class="flex-1 bg-slate-900 text-slate-100 border border-slate-700 rounded-l-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-red-500"
|
<label class="text-sm text-slate-300" for="sortSelect">Trier par</label>
|
||||||
placeholder="Rechercher..."
|
<select id="sortSelect" class="bg-slate-900 text-slate-100 border border-slate-700 rounded px-2 py-1 text-sm"
|
||||||
[value]="q()"
|
[value]="sortParam()" (change)="onSortChange($event)">
|
||||||
(input)="onSearchInput($event)"
|
<option value="relevance">Pertinence</option>
|
||||||
(keydown.enter)="onSearchEnter($event)"
|
<option value="date">Date</option>
|
||||||
(keydown.escape)="closeSearchResults()"
|
<option value="views">Vues</option>
|
||||||
/>
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 justify-end">
|
||||||
|
<button class="px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100 disabled:opacity-50"
|
||||||
|
(click)="prevPage()" [disabled]="!canPrev()">Précédent</button>
|
||||||
|
<div class="text-sm text-slate-300">Page {{ pageParam() }}</div>
|
||||||
|
<button class="px-3 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-100" (click)="nextPage()">Suivant</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (showUnified() && !loading() && unifiedTotal() === 0 && hasQuery()) {
|
||||||
|
<p class="text-slate-400">Aucun résultat pour « {{ q() }} » avec les fournisseurs sélectionnés.</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (error()) {
|
||||||
|
<div class="mb-4 bg-red-900/40 border border-red-600 text-red-200 p-3 rounded flex items-center justify-between">
|
||||||
|
<span>{{ error() }}</span>
|
||||||
|
<button class="px-3 py-1 rounded bg-red-600 hover:bg-red-500 text-white text-sm" (click)="onSearchBarSubmit({ q: q(), providers: providersListParam() || 'all' })">Réessayer</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, signal, untracked, ViewChild } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, signal, untracked, ViewChild } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||||
import { YoutubeApiService } from '../../services/youtube-api.service';
|
import { YoutubeApiService } from '../../services/youtube-api.service';
|
||||||
import { Video } from '../../models/video.model';
|
import { Video } from '../../models/video.model';
|
||||||
import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anchor.component';
|
import { InfiniteAnchorComponent } from '../shared/infinite-anchor/infinite-anchor.component';
|
||||||
@ -11,19 +11,20 @@ import { Title } from '@angular/platform-browser';
|
|||||||
import { TranslatePipe } from '../../pipes/translate.pipe';
|
import { TranslatePipe } from '../../pipes/translate.pipe';
|
||||||
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
|
import { LikeButtonComponent } from '../shared/components/like-button/like-button.component';
|
||||||
import { AddToPlaylistComponent } from '../shared/components/add-to-playlist/add-to-playlist.component';
|
import { AddToPlaylistComponent } from '../shared/components/add-to-playlist/add-to-playlist.component';
|
||||||
import { SearchSuggestionsComponent } from './search-suggestions.component';
|
|
||||||
import { SearchService, type SearchResponse } from '../../app/search/search.service';
|
import { SearchService, type SearchResponse } from '../../app/search/search.service';
|
||||||
import type { ProviderId } from '../../app/core/providers/provider-registry';
|
import type { ProviderId } from '../../app/core/providers/provider-registry';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-search',
|
selector: 'app-search',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './search.component.html',
|
templateUrl: './search.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [CommonModule, RouterLink, InfiniteAnchorComponent, TranslatePipe, LikeButtonComponent, AddToPlaylistComponent, SearchSuggestionsComponent]
|
imports: [CommonModule, RouterLink, InfiniteAnchorComponent, TranslatePipe, LikeButtonComponent, AddToPlaylistComponent]
|
||||||
})
|
})
|
||||||
export class SearchComponent {
|
export class SearchComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
|
private router = inject(Router);
|
||||||
private api = inject(YoutubeApiService);
|
private api = inject(YoutubeApiService);
|
||||||
private instances = inject(InstanceService);
|
private instances = inject(InstanceService);
|
||||||
private history = inject(HistoryService);
|
private history = inject(HistoryService);
|
||||||
@ -46,10 +47,16 @@ export class SearchComponent {
|
|||||||
providerParam = signal<Provider | null>(null);
|
providerParam = signal<Provider | null>(null);
|
||||||
themeParam = signal<string | null>(null);
|
themeParam = signal<string | null>(null);
|
||||||
providersListParam = signal<ProviderId[] | 'all' | null>(null);
|
providersListParam = signal<ProviderId[] | 'all' | null>(null);
|
||||||
|
pageParam = signal<number>(1);
|
||||||
|
sortParam = signal<'relevance' | 'date' | 'views'>('relevance');
|
||||||
|
|
||||||
// Unified multi-provider response (grouped suggestions)
|
// Unified multi-provider response (grouped suggestions)
|
||||||
groups = signal<Record<ProviderId, any[]>>({} as any);
|
groups = signal<Record<ProviderId, any[]>>({} as any);
|
||||||
showUnified = signal<boolean>(false);
|
showUnified = signal<boolean>(false);
|
||||||
|
error = signal<string | null>(null);
|
||||||
|
|
||||||
|
// Dedup key to avoid recording the same search multiple times
|
||||||
|
private lastRecordedKey: string | null = null;
|
||||||
|
|
||||||
hasQuery = computed(() => this.q().length > 0);
|
hasQuery = computed(() => this.q().length > 0);
|
||||||
providerLabel = computed(() => {
|
providerLabel = computed(() => {
|
||||||
@ -60,7 +67,33 @@ export class SearchComponent {
|
|||||||
}
|
}
|
||||||
return this.instances.selectedProviderLabel();
|
return this.instances.selectedProviderLabel();
|
||||||
});
|
});
|
||||||
pageHeading = computed(() => `Search - ${this.providerLabel()}${this.q() ? ' - ' + this.q() : ''}`);
|
|
||||||
|
// Active providers derived from `providers` query param or fallback to all available
|
||||||
|
activeProviders = computed<ProviderId[]>(() => {
|
||||||
|
const p = this.providersListParam();
|
||||||
|
if (p === 'all' || p == null) {
|
||||||
|
try { return (this.instances.providers() || []).map(x => x.id as unknown as ProviderId); }
|
||||||
|
catch { return ['yt','dm','tw','pt','od','ru']; }
|
||||||
|
}
|
||||||
|
if (Array.isArray(p) && p.length > 0) return p as ProviderId[];
|
||||||
|
return ['yt','dm','tw','pt','od','ru'];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subtitle display: codes separated by middot
|
||||||
|
activeProvidersDisplay = computed(() => this.activeProviders().join(' · '));
|
||||||
|
|
||||||
|
// Helper to display provider label from registry/instances
|
||||||
|
providerDisplay(p: ProviderId): string {
|
||||||
|
try {
|
||||||
|
const found = (this.instances.providers() || []).find(x => (x.id as any) === p);
|
||||||
|
return found?.label || p.toUpperCase();
|
||||||
|
} catch { return p.toUpperCase(); }
|
||||||
|
}
|
||||||
|
// Title: Search ${q ? ` — “${q}”` : ''}
|
||||||
|
pageHeading = computed(() => {
|
||||||
|
const q = this.q();
|
||||||
|
return `Search ${q ? ` — “${q}”` : ''}`;
|
||||||
|
});
|
||||||
// Public computed used by the template to avoid referencing private `instances`
|
// Public computed used by the template to avoid referencing private `instances`
|
||||||
selectedProviderForView = computed(() => this.providerParam() || this.instances.selectedProvider());
|
selectedProviderForView = computed(() => this.providerParam() || this.instances.selectedProvider());
|
||||||
|
|
||||||
@ -68,6 +101,16 @@ export class SearchComponent {
|
|||||||
// For Twitch provider: 'twitch_all' | 'twitch_live' | 'twitch_vod'.
|
// For Twitch provider: 'twitch_all' | 'twitch_live' | 'twitch_vod'.
|
||||||
filterTag = signal<string>('all');
|
filterTag = signal<string>('all');
|
||||||
|
|
||||||
|
// Unified total results count (sum of groups lengths)
|
||||||
|
unifiedTotal = computed(() => {
|
||||||
|
const g = this.groups();
|
||||||
|
try {
|
||||||
|
return Object.values(g).reduce((acc: number, arr: any) => acc + (Array.isArray(arr) ? arr.length : 0), 0);
|
||||||
|
} catch { return 0; }
|
||||||
|
});
|
||||||
|
|
||||||
|
canPrev = computed(() => (this.pageParam() || 1) > 1);
|
||||||
|
|
||||||
// Helper computed: filtered list based on active tag (non-Twitch only)
|
// Helper computed: filtered list based on active tag (non-Twitch only)
|
||||||
filteredResults = computed(() => {
|
filteredResults = computed(() => {
|
||||||
const tag = this.filterTag();
|
const tag = this.filterTag();
|
||||||
@ -247,24 +290,29 @@ export class SearchComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe once to unified multi-provider results and reflect to UI
|
// Subscribe once to unified multi-provider results and reflect to UI
|
||||||
this.unified.request$.subscribe({
|
this.unified.request$.pipe(takeUntilDestroyed()).subscribe({
|
||||||
next: (resp: SearchResponse) => {
|
next: (resp: SearchResponse) => {
|
||||||
this.groups.set(resp.groups as any);
|
this.groups.set(resp.groups as any);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.error.set(null);
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.groups.set({} as any);
|
this.groups.set({} as any);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
|
this.error.set('Le service de recherche est temporairement indisponible. Réessayer.');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to query param changes (so subsequent searches update)
|
// Listen to query param changes (so subsequent searches update)
|
||||||
this.route.queryParamMap.subscribe((pm) => {
|
this.route.queryParamMap.pipe(takeUntilDestroyed()).subscribe((pm) => {
|
||||||
const q = (pm.get('q') || '').trim();
|
const q = (pm.get('q') || '').trim();
|
||||||
this.q.set(q);
|
this.q.set(q);
|
||||||
const prov = (pm.get('provider') as Provider) || null;
|
const prov = (pm.get('provider') as Provider) || (pm.get('p') as Provider) || null;
|
||||||
const theme = pm.get('theme');
|
const theme = pm.get('theme');
|
||||||
const providersParam = (pm.get('providers') || '').trim();
|
const providersParam = (pm.get('providers') || '').trim();
|
||||||
|
const page = Number(pm.get('page') || '1');
|
||||||
|
const pageSize = Number(pm.get('pageSize') || '24');
|
||||||
|
const sort = pm.get('sort') || undefined;
|
||||||
// Parse providers CSV if present
|
// Parse providers CSV if present
|
||||||
let providersList: ProviderId[] | 'all' | null = null;
|
let providersList: ProviderId[] | 'all' | null = null;
|
||||||
if (providersParam) {
|
if (providersParam) {
|
||||||
@ -274,6 +322,13 @@ export class SearchComponent {
|
|||||||
this.providerParam.set(prov);
|
this.providerParam.set(prov);
|
||||||
this.themeParam.set(theme);
|
this.themeParam.set(theme);
|
||||||
this.providersListParam.set(providersList);
|
this.providersListParam.set(providersList);
|
||||||
|
const pageNum = isFinite(page) ? page : 1;
|
||||||
|
this.pageParam.set(pageNum);
|
||||||
|
this.unified.setPage(pageNum);
|
||||||
|
if (isFinite(pageSize)) this.unified.setPageSize(pageSize);
|
||||||
|
const sortNorm = (sort === 'date' || sort === 'views') ? sort : 'relevance';
|
||||||
|
this.sortParam.set(sortNorm);
|
||||||
|
this.unified.setSort(sortNorm);
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
this.notice.set(null);
|
this.notice.set(null);
|
||||||
@ -285,15 +340,33 @@ export class SearchComponent {
|
|||||||
const providersToUse = providersList || [provider as ProviderId];
|
const providersToUse = providersList || [provider as ProviderId];
|
||||||
this.unified.setQuery(q);
|
this.unified.setQuery(q);
|
||||||
this.unified.setProviders(providersToUse);
|
this.unified.setProviders(providersToUse);
|
||||||
|
|
||||||
|
// Record this search once per unique (q, providers) combination
|
||||||
|
try {
|
||||||
|
const key = `${q}|${Array.isArray(providersToUse) ? providersToUse.join(',') : String(providersToUse)}`;
|
||||||
|
if (this.lastRecordedKey !== key) {
|
||||||
|
this.lastRecordedKey = key;
|
||||||
|
const recFilters: any = { provider, providers: Array.isArray(providersToUse) ? providersToUse : 'all' };
|
||||||
|
this.history.recordSearch(q, recFilters).subscribe({
|
||||||
|
next: () => {},
|
||||||
|
error: (err) => console.error('Error recording search:', err)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error in recordSearch:', err);
|
||||||
|
}
|
||||||
|
|
||||||
this.showUnified.set(true);
|
this.showUnified.set(true);
|
||||||
this.loading.set(true);
|
this.loading.set(true);
|
||||||
this.groups.set({} as any); // Clear previous results
|
this.groups.set({} as any); // Clear previous results
|
||||||
|
this.error.set(null);
|
||||||
} else {
|
} else {
|
||||||
this.results.set([]);
|
this.results.set([]);
|
||||||
this.nextCursor.set(null);
|
this.nextCursor.set(null);
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.groups.set({} as any);
|
this.groups.set({} as any);
|
||||||
this.showUnified.set(false);
|
this.showUnified.set(false);
|
||||||
|
this.error.set(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -464,30 +537,21 @@ export class SearchComponent {
|
|||||||
return qp;
|
return qp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit handler from the embedded SearchBox: keep URL as source of truth
|
||||||
|
onSearchBarSubmit(evt: { q: string; providers: ProviderId[] | 'all' }) {
|
||||||
|
const q = (evt?.q || '').trim();
|
||||||
|
if (!q) return;
|
||||||
|
const provider = this.providerParam() || this.instances.selectedProvider();
|
||||||
|
const qp: any = { q, p: provider, provider: null };
|
||||||
|
if (Array.isArray(evt.providers)) qp.providers = evt.providers.join(',');
|
||||||
|
else if (evt.providers === 'all') qp.providers = 'yt,dm,tw,pt,od,ru';
|
||||||
|
qp.page = 1; // reset pagination on new search
|
||||||
|
this.router.navigate([], { relativeTo: this.route, queryParams: qp, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChild('searchInput') searchInput?: ElementRef<HTMLInputElement>;
|
@ViewChild('searchInput') searchInput?: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
onSearchEnter(event: Event) {
|
// Legacy input handlers removed; SearchBox handles keyboard and submit
|
||||||
const keyboardEvent = event as KeyboardEvent;
|
|
||||||
keyboardEvent.preventDefault();
|
|
||||||
const query = this.q().trim();
|
|
||||||
if (query) {
|
|
||||||
// Route unified search regardless of providers param
|
|
||||||
const provParam = this.providersListParam();
|
|
||||||
const provider = this.providerParam() || this.instances.selectedProvider();
|
|
||||||
const providersToUse = (provParam && (provParam === 'all' || (Array.isArray(provParam) && provParam.length > 0)))
|
|
||||||
? (provParam as any)
|
|
||||||
: [provider as unknown as ProviderId];
|
|
||||||
this.unified.setQuery(query);
|
|
||||||
this.unified.setProviders(providersToUse);
|
|
||||||
this.showUnified.set(true);
|
|
||||||
this.loading.set(true);
|
|
||||||
this.closeSearchResults();
|
|
||||||
// Retirer le focus de l'input
|
|
||||||
if (this.searchInput) {
|
|
||||||
this.searchInput.nativeElement.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSearchResults() {
|
closeSearchResults() {
|
||||||
// Cette méthode sera appelée pour fermer les résultats de recherche
|
// Cette méthode sera appelée pour fermer les résultats de recherche
|
||||||
@ -506,4 +570,29 @@ export class SearchComponent {
|
|||||||
setFilterTag(key: string) {
|
setFilterTag(key: string) {
|
||||||
this.filterTag.set(key);
|
this.filterTag.set(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pagination controls
|
||||||
|
nextPage() {
|
||||||
|
const next = (this.pageParam() || 1) + 1;
|
||||||
|
this.router.navigate([], { relativeTo: this.route, queryParams: { page: next }, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
prevPage() {
|
||||||
|
const prev = Math.max(1, (this.pageParam() || 1) - 1);
|
||||||
|
this.router.navigate([], { relativeTo: this.route, queryParams: { page: prev }, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort control
|
||||||
|
onSortChange(event: Event) {
|
||||||
|
const value = (event.target as HTMLSelectElement)?.value as 'relevance' | 'date' | 'views' | '';
|
||||||
|
const sort = (value === 'date' || value === 'views') ? value : 'relevance';
|
||||||
|
this.router.navigate([], { relativeTo: this.route, queryParams: { sort, page: 1 }, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// CTA handler: navigate to focus on a single provider while preserving q
|
||||||
|
onViewAll(p: ProviderId) {
|
||||||
|
const q = (this.q() || '').trim();
|
||||||
|
const qp: any = { provider: p, p, providers: p, page: 1 };
|
||||||
|
if (q) qp.q = q;
|
||||||
|
this.router.navigate([], { relativeTo: this.route, queryParams: qp, queryParamsHandling: 'merge' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ export interface SearchHistoryItem {
|
|||||||
id: string;
|
id: string;
|
||||||
query: string;
|
query: string;
|
||||||
filters_json?: string | null;
|
filters_json?: string | null;
|
||||||
|
providers?: string[]; // Liste des providers utilisés (ex: ['yt', 'dm', 'tw'])
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,10 +40,16 @@ export class HistoryService {
|
|||||||
|
|
||||||
// --- Search ---
|
// --- Search ---
|
||||||
recordSearch(query: string, filters?: Record<string, any>): Observable<{ id: string; created_at: string }> {
|
recordSearch(query: string, filters?: Record<string, any>): Observable<{ id: string; created_at: string }> {
|
||||||
|
const url = `${this.apiBase()}/user/history/search`;
|
||||||
return this.http.post<{ id: string; created_at: string }>(
|
return this.http.post<{ id: string; created_at: string }>(
|
||||||
'/proxy/api/user/history/search',
|
url,
|
||||||
{ query, filters },
|
{ query, filters },
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
|
).pipe(
|
||||||
|
tap({
|
||||||
|
next: (res) => console.debug('[HistoryService] Search recorded', { query, filters, res }),
|
||||||
|
error: (err) => console.error('[HistoryService] Failed to record search', err)
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,13 +57,13 @@ export class HistoryService {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('limit', String(limit));
|
params.set('limit', String(limit));
|
||||||
if (before) params.set('before', before);
|
if (before) params.set('before', before);
|
||||||
return this.http.get<SearchHistoryItem[]>(`/proxy/api/user/history/search?${params.toString()}`, { withCredentials: true });
|
return this.http.get<SearchHistoryItem[]>(`${this.apiBase()}/user/history/search?${params.toString()}`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Watch ---
|
// --- Watch ---
|
||||||
recordWatchStart(provider: string, videoId: string, title?: string | null, watchedAt?: string, thumbnail?: string | null, durationSeconds?: number): Observable<WatchHistoryItem> {
|
recordWatchStart(provider: string, videoId: string, title?: string | null, watchedAt?: string, thumbnail?: string | null, durationSeconds?: number): Observable<WatchHistoryItem> {
|
||||||
return this.http.post<WatchHistoryItem>(
|
return this.http.post<WatchHistoryItem>(
|
||||||
'/proxy/api/user/history/watch',
|
`${this.apiBase()}/user/history/watch`,
|
||||||
{ provider, videoId, title: title ?? null, watchedAt, thumbnail: thumbnail ?? null, durationSeconds },
|
{ provider, videoId, title: title ?? null, watchedAt, thumbnail: thumbnail ?? null, durationSeconds },
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
@ -89,16 +96,16 @@ export class HistoryService {
|
|||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
params.set('limit', String(limit));
|
params.set('limit', String(limit));
|
||||||
if (before) params.set('before', before);
|
if (before) params.set('before', before);
|
||||||
return this.http.get<WatchHistoryItem[]>(`/proxy/api/user/history/watch?${params.toString()}`, { withCredentials: true });
|
return this.http.get<WatchHistoryItem[]>(`${this.apiBase()}/user/history/watch?${params.toString()}`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Delete single items ---
|
// --- Delete single items ---
|
||||||
deleteSearchItem(id: string): Observable<void> {
|
deleteSearchItem(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`/proxy/api/user/history/search/${encodeURIComponent(id)}`, { withCredentials: true });
|
return this.http.delete<void>(`${this.apiBase()}/user/history/search/${encodeURIComponent(id)}`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteWatchItem(id: string): Observable<void> {
|
deleteWatchItem(id: string): Observable<void> {
|
||||||
return this.http.delete<void>(`/proxy/api/user/history/watch/${encodeURIComponent(id)}`, { withCredentials: true });
|
return this.http.delete<void>(`${this.apiBase()}/user/history/watch/${encodeURIComponent(id)}`, { withCredentials: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recherche dans l'historique des recherches
|
// Recherche dans l'historique des recherches
|
||||||
@ -107,7 +114,7 @@ export class HistoryService {
|
|||||||
params.set('q', query);
|
params.set('q', query);
|
||||||
params.set('limit', String(limit));
|
params.set('limit', String(limit));
|
||||||
return this.http.get<SearchHistoryItem[]>(
|
return this.http.get<SearchHistoryItem[]>(
|
||||||
`/proxy/api/user/history/search?${params.toString()}`,
|
`${this.apiBase()}/user/history/search?${params.toString()}`,
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -118,7 +125,7 @@ export class HistoryService {
|
|||||||
params.set('q', query);
|
params.set('q', query);
|
||||||
params.set('limit', String(limit));
|
params.set('limit', String(limit));
|
||||||
return this.http.get<WatchHistoryItem[]>(
|
return this.http.get<WatchHistoryItem[]>(
|
||||||
`/proxy/api/user/history/watch?${params.toString()}`,
|
`${this.apiBase()}/user/history/watch?${params.toString()}`,
|
||||||
{ withCredentials: true }
|
{ withCredentials: true }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user