Angular Stories Player
@reelkit/angular-stories-player का इस्तेमाल करते हुए Angular के लिए Instagram जैसा stories प्लेयर ओवरले। यह React और Vue प्लेयर के साथ वही @reelkit/stories-core इंजन साझा करता है, इसलिए व्यवहार, CSS क्लास और थीमिंग टोकन एक जैसे ही हैं।
फ़ीचर
इंस्टॉलेशन
npm install @reelkit/angular-stories-player @reelkit/angular lucide-angularस्टाइलशीट एक बार रजिस्टर करें:
// Anywhere that runs at startup — main.ts, app.config.ts, a component file.
import '@reelkit/angular-stories-player/styles.css';
// Or, if you keep CSS out of TypeScript, from your global stylesheet:
// @import '@reelkit/angular-stories-player/styles.css';
// Or register it in angular.json:
// "styles": ["node_modules/@reelkit/angular-stories-player/styles.css"]आइकन
डिफ़ॉल्ट हेडर और नेविगेशन तीर आइकन के लिए lucide-angular इस्तेमाल करते हैं। अगर आप कोई दूसरी आइकन लाइब्रेरी चाहते हैं, तो उन्हें rkStoriesHeader और rkStoriesNavigation टेम्पलेट स्लॉट से बदल दें।
क्विक स्टार्ट
rk-stories-overlay पूरी स्क्रीन का प्लेयर रेंडर करता है। Instagram जैसे प्रवेश बिंदुओं के लिए इसे rk-stories-ring-list के साथ जोड़ें: रिंग अपने समूह के इंडेक्स के साथ (selected) भेजती है, जिसे होस्ट isOpen सेट करने से पहले initialGroupIndex के रूप में रख लेता है। प्लेयर खुद को कभी बंद नहीं करता; वह (closed) भेजता है, और होस्ट isOpen को वापस false कर देता है।
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
RkStoriesOverlayComponent,
RkStoriesRingListComponent,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
@Component({
selector: 'app-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups" (selected)="open($event)" />
<!-- The player reports a close instead of closing itself, so the host
decides: the ✕ button, a swipe down, Escape and the last story
finishing all arrive as (closed). -->
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
[initialGroupIndex]="groupIndex()"
(closed)="isOpen.set(false)"
/>
`,
})
export class FeedComponent {
protected readonly groups: StoriesGroup[] = [
{
author: { id: 'alice', name: 'Alice', avatar: '/alice.jpg', verified: true },
stories: [
{ id: 's1', mediaType: 'image', src: '/story-1.jpg' },
{ id: 's2', mediaType: 'video', src: '/story-2.mp4', poster: '/poster-2.jpg' },
],
},
{
author: { id: 'bob', name: 'Bob', avatar: '/bob.jpg' },
stories: [{ id: 's3', mediaType: 'image', src: '/story-3.jpg' }],
},
];
protected readonly isOpen = signal(false);
protected readonly groupIndex = signal(0);
protected open(index: number): void {
this.groupIndex.set(index);
this.isOpen.set(true);
}
}ओवरले कहाँ रेंडर होता है
React और Vue प्लेयर अपना ओवरले document.body के अंत में ले जाते हैं, पर rk-stories-overlay वहीं रेंडर होता है जहाँ आप उसे रखते हैं और position: fixed से पूरी स्क्रीन ढकता है। उसे ऐसे किसी पूर्वज के भीतर न रखें जिस पर transform, perspective, filter या will-change: transform हो: वह पूर्वज ओवरले का containing block बन जाता है, इसलिए प्लेयर व्यूपोर्ट की जगह उसी के आकार का हो जाता है और उसके overflow से कट जाता है। अपना स्टैकिंग कॉन्टेक्स्ट बनाने वाला पूर्वज भी सीमित करता है कि --rk-stories-overlay-z प्लेयर को कितना ऊपर उठा सकता है। बदले में, किसी भी पूर्वज पर सेट किए गए --rk-stories-* टोकन प्लेयर तक पहुँचते हैं।
URL स्टेट
लाइव डेमो देखें →RkStoriesUrlOverlayComponent एक अलग कंपोनेंट है जिसकी खुली-बंद स्थिति एड्रेस बार में रहती है। दोनों अक्ष एक ही पैरामीटर — ?story=<group>.<story> — में चलते हैं, इसलिए चल रही स्टोरी का एक ऐसा लिंक बनता है जिसे साझा किया जा सकता है, बुकमार्क किया जा सकता है और बैक बटन से बंद किया जा सकता है। createOverlayUrlState और urlIndexTwoAxisKey से कंट्रोलर बनाएँ और उसे controller के रूप में दें।
तैयार कीज़
स्टोरीज़ दो-अक्ष वाली हैं, इसलिए कंट्रोलर में दो-अक्ष वाली key फैलाएँ: urlIndexTwoAxisKey (उपयोगकर्ता और स्टोरी स्थिति से) या urlStableIdTwoAxisKey (उपयोगकर्ता स्थायी id से) — दोनों @reelkit/angular-stories-player से फिर से एक्सपोर्ट होती हैं। URL स्थिति गाइड और Core API देखें।
import { ChangeDetectionStrategy, Component, Injector, inject, runInInjectionContext, type OnInit } from '@angular/core';
import {
RkStoriesUrlOverlayComponent,
RkStoriesRingListComponent,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
import {
createOverlayUrlState,
urlIndexTwoAxisKey,
type TwoAxisPosition,
type UrlStateController,
} from '@reelkit/angular';
import { createRouterUrlAdapter } from '@reelkit/angular/ng-router-url-adapter';
@Component({
selector: 'app-url-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesUrlOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups" (selected)="open($event)" />
<rk-stories-url-overlay [controller]="stories" [groups]="groups" />
`,
})
export class UrlFeedComponent implements OnInit {
private readonly injector = inject(Injector);
protected groups: StoriesGroup[] = [];
protected stories!: UrlStateController<TwoAxisPosition>;
ngOnInit(): void {
// Both the controller and the router adapter need an injection context,
// and ngOnInit does not run in one, so borrow the component's.
this.stories = runInInjectionContext(this.injector, () =>
createOverlayUrlState({
param: 'story',
adapter: createRouterUrlAdapter(),
...urlIndexTwoAxisKey({
outerCount: () => this.groups.length,
innerCounts: () => this.groups.map((group) => group.stories.length),
}),
}),
) as UrlStateController<TwoAxisPosition>;
}
// Swiping only replaces the entry, so one back step always closes the
// player rather than walking back through every story.
protected open(groupIndex: number): void {
this.stories.set(`${groupIndex}.0`);
}
}- खुलने पर हिस्ट्री में एक एंट्री जुड़ती है। स्टोरीज़ के बीच बढ़ना और उपयोगकर्ता बदलना, दोनों उसी एंट्री को बदलते हैं, इसलिए कितनी भी नेविगेशन से एंट्री नहीं बढ़तीं और एक बार बैक दबाने पर प्लेयर हमेशा बंद हो जाता है। बैक बंद करता है, पिछली स्टोरी पर नहीं ले जाता।
- अंदरूनी नेविगेशन भी URL में जाती है। एक ही उपयोगकर्ता की स्टोरीज़ में आगे बढ़ने पर
?story=2.n अपडेट होता है, इसलिए डीप लिंक ठीक उसी स्टोरी पर पहुँचता है। - बैक तभी बंद करता है जब प्लेयर ऐप के भीतर से खोला गया हो — तब उस लिंक ने एक एंट्री जोड़ी थी। नए टैब में सीधे खोले गए साझा लिंक के पीछे कोई हिस्ट्री नहीं होती, इसलिए ब्राउज़र का बैक साइट से बाहर ले जाता है; ✕ बटन या Escape पैरामीटर को वहीं हटा देते हैं और पेज पर बनाए रखते हैं।
- ऐसा पैरामीटर जो किसी उपयोगकर्ता या स्टोरी को नहीं बताता — पुराना बुकमार्क, हाथ से बदला मान, समूह के अंत से आगे की स्टोरी — पड़ोसी को खोलने के बजाय URL से हटा दिया जाता है।
- राउटर वाले ऐप को राउटर पर आधारित अडैप्टर देना चाहिए, ताकि नेविगेशन का एकमात्र स्रोत राउटर ही रहे।
@reelkit/angular/ng-router-url-adapterकाcreateRouterUrlAdapterAngular राउटर के लिए तैयार अडैप्टर है; राउटर के पीछे से हिस्ट्री लिखने पर उसकी location पुरानी रह जाती है और अगली नेविगेशन पर पैरामीटर गिर जाता है।
स्थायी लिंक। उपयोगकर्ता डिफ़ॉल्ट रूप से स्थिति से पहचाना जाता है, इसलिए बुकमार्क किया गया ?story=2.0 फ़ीड का क्रम बदलते ही किसी और को खोल देगा। इसके बजाय उसे स्थायी id से पहचानें — outerCodec id को URL में लिखता है और outerLocator बताता है कि वह कहाँ है। समूह के भीतर स्टोरी के साथ वही करने के लिए innerCodec को innerLocate और innerIdentify के साथ दें; न दें तो स्टोरी वाला आधा हिस्सा समूह के भीतर सामान्य स्थानीय इंडेक्स ही रहता है।
// ?story=alice.s3 — each axis addressed by a stable id, so a link survives
// the feed being reordered or paged.
import { urlStableIdKey } from '@reelkit/angular';
const key = urlIndexTwoAxisKey({
outerCodec: urlStableIdKey({ items: () => groups }).codec,
outerLocator: {
locate: (id) => groups.findIndex((group) => group.author.id === id),
identify: (index) => groups[index].author.id,
},
outerCount: () => groups.length,
innerCounts: () => groups.map((group) => group.stories.length),
innerCodec: urlStableIdKey({ items: () => [] }).codec,
innerLocate: (outer, id) =>
groups[outer].stories.findIndex((story) => story.id === id),
innerIdentify: (outer, index) => groups[outer].stories[index].id,
});अनंत फ़ीड। पेजिंग outerLocator का काम है और codec से स्वतंत्र है। locate सिंक्रोनस है, इसलिए वह सिर्फ़ पहले से लोड समूहों के बारे में बता सकता है — 20 समूह लोड वाली फ़ीड में 400वें समूह का साझा लिंक खाली लौटता है। locateAsync इसका सहारा है और तभी चलता है जब locate चूक जाए; उसके बाद स्टोरी उसी समूह की सीमा में बाँध दी जाती है जिस पर बात रुकी।
वही locateAsync, बाहरी अक्ष
यह वही locateAsync पेजर है जो एक-अक्ष वाली कीज़ लेती हैं — दो-अक्ष वाली key में यह आपके दिए outerLocator पर चलता है, इसलिए पेजिंग उपयोगकर्ताओं वाले अक्ष पर होती है और स्टोरी हल हुए समूह के भीतर स्थानीय इंडेक्स बनी रहती है।
// locateAsync is asked only when locate misses — the group the link names is
// past what the feed has loaded. Page up to it, commit, and answer with its
// index; the story half is re-bounded against whichever group it settles on.
const key = urlIndexTwoAxisKey({
outerCount: () => this.groups().length,
innerCounts: () => this.groups().map((group) => group.stories.length),
outerLocator: {
locate: (index) => (index < this.groups().length ? index : null),
identify: (index) => index,
locateAsync: async (index) => {
const loaded = await this.loadUntilGroup(index);
if (!loaded) return null; // exhausted — the link names no group
this.groups.set(loaded); // commit — the player renders from this signal
return index;
},
},
});- जब तक
locateAsyncचल रहा है, प्लेयर बंद रहता है और पैरामीटर वैसा का वैसा, इसलिए डीप लिंक फ़ेच के बाद भी बचा रहता है।nullया अस्वीकृति पैरामीटर हटा देती है। - URL बदल जाने के बाद, बंद हो जाने के बाद या अनमाउंट के बाद आया जवाब छोड़ दिया जाता है — धीमा फ़ेच ऐसी स्टोरी नहीं खोल सकता जिसकी अब किसी ने माँग नहीं की।
createOverlayUrlStateके सभी विकल्प Angular API संदर्भ में हैं, और चरण-दर-चरण व्याख्या Angular गाइड में है।
जो देखा जा चुका है उसे याद रखना
जब तक किसी समूह को अंत तक न देखा जाए, उसकी रिंग ग्रेडिएंट दिखाती है, और समूह दोबारा उस पहली story पर खुलता है जिसे दर्शक ने अभी नहीं देखा। दोनों बातें एक ही createStoriesViewedStateController से आती हैं, जिसे वहीं एक बार बनाया जाता है जहाँ फ़ीड रहती है और रिंग सूची व प्लेयर को viewed के रूप में दिया जाता है: वे store पढ़ते हैं, उसके बदलावों के साथ चलते हैं और जो दिखाया गया उसे खुद दर्ज करते हैं। सहेजी गई प्रविष्टि लेखक और story को id से पहचानती है, इसलिए फ़ीड का क्रम बदलने पर भी बनी रहती है, और यही controller React और Vue में भी काम करता है।
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
RkStoriesOverlayComponent,
RkStoriesRingListComponent,
createStoriesViewedStateController,
type StoriesGroup,
} from '@reelkit/angular-stories-player';
@Component({
selector: 'app-remembering-feed',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RkStoriesOverlayComponent, RkStoriesRingListComponent],
template: `
<rk-stories-ring-list [groups]="groups()" [viewed]="viewed" (selected)="open($event)" />
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups()"
[initialGroupIndex]="groupIndex()"
[viewed]="viewed"
(closed)="isOpen.set(false)"
/>
`,
})
export class RememberingFeedComponent {
protected readonly groups = signal<StoriesGroup[]>([]);
// One controller for both: the rings mute as a user is watched to the end,
// and the player opens each user on their first unseen story. It reads the
// groups through the signal, so users paged in later are counted too.
protected readonly viewed = createStoriesViewedStateController({
storageKey: 'my-app-stories-seen',
groups: () => this.groups(),
});
protected readonly isOpen = signal(false);
protected readonly groupIndex = signal(0);
protected open(index: number): void {
this.groupIndex.set(index);
this.isOpen.set(true);
}
}- खुलने वाली story भी गिनी जाती है। प्लेयर खुलते ही स्क्रीन पर मौजूद story देखी गई दर्ज होती है, इसलिए एक story वाला समूह खोलकर बंद करने से वह देखा हुआ मान लिया जाता है।
- आगे swipe करने पर भी वहीं से शुरू होता है। इस session में पहली बार पहुँचे हर समूह के लिए
resumeStoryIndexसे पूछा जाता है, जिस समूह पर प्लेयर खुलता है उसके लिए भी, जब तकinitialStoryIndexसीधे कोई story न बताए; जिस समूह से पहले swipe करके गुज़र चुके हैं, वह वहीं खुलता है जहाँ छोड़ा गया था। स्पष्ट रूप से दिया गयाresumeStoryIndexइनपुट controller के जवाब पर भारी पड़ता है। - लिंक फिर भी जीतता है।
rk-stories-url-overlayके साथ, सहेजा हुआ कुछ भी हो, पैरामीटर तय करता है कि प्लेयर कहाँ खुलेगा। बाकी हर जगह resume callback तय करता है। - गिनती एक स्थिति है, जोड़ नहीं। प्रविष्टि उस सबसे आगे की story को बताती है जहाँ तक पहुँचा गया, इसलिए देखे जा चुके समूह में story जोड़ने पर उसकी रिंग फिर चमकने लगती है, और बीच से एक story हटाने पर गिनती घट जाती है।
- Storage बदला जा सकता है। टैब बंद होने पर भूलने के लिए
storage: createSessionStorageAdapter()दें, या अपनाStorageAdapter। दो खुले टैब ब्राउज़र के storage event से आपस में तालमेल में रहते हैं। - सिर्फ़ खुलने पर बनने वाला प्लेयर बहुत देर से पढ़ता है।
rk-stories-overlayबनते ही store पढ़ लेता है, जबकि वह अभी बंद ही होता है, औरrk-stories-ring-listभी ऐसा ही करता है।@if (open)में लपेटे गए overlay को ऐसा कोई पल नहीं मिलता: होस्ट के constructor मेंattachViewedState(() => this.viewed)कॉल करें, जो होस्ट के रहने तक जुड़ा रहता है। विकल्प, और जिन निचले स्तर के हिस्सों से यह बना है, वे Stories Core पेज पर हैं।
डेस्कटॉप कैरोसेल
प ्लेयर को Instagram वेब व्यूअर की तरह सजाने के लिए desktopLayout="carousel" सेट करें: चालू story अपने सामान्य आकार में बीच में रहती है, और हर तरफ़ पड़ोसी दो समूह तक छोटे, धुँधले कार्ड के रूप में रहते हैं। किसी कार्ड पर क्लिक करने से वह समूह खुलता है, और समूह बदलते समय कार्ड सरकते हैं। रिंग सूची वाला वही viewed controller दें, तो देखे जा चुके समूह के कार्ड को फीकी रिंग मिलती है।
// 'carousel' lays the neighbouring users out as preview cards either side of
// the active story, the way Instagram does on the web. A phone always shows
// the active story alone, whatever this says.
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
desktopLayout="carousel"
(closed)="isOpen.set(false)"
/>
`,
})
export class CarouselFeedComponent {}- फ़ोन पर सादा प्लेयर ही रहता है। 768px चौड़ाई तक कार्ड रेंडर नहीं होते और swipe
groupTransitionका इस्तेमाल करते हैं, जैसे डिफ़ॉल्ट'single'लेआउट में। उस चौड़ाई के आर-पार आकार बदलने पर लेआउट तुरंत बदल जाता है। जब तक कार्ड दिखते हैं, समूह घूमते नहीं बल्कि सरकते हैं,groupTransitionकुछ भी कहे। - हर कार्ड वह जगह दिखाता है जहाँ समूह खुलेगा: इस session में जिस story पर उसे छोड़ा गया था, वरना जो
resumeStoryIndexलौटाए। यह वीडियो का poster या इमेज दिखाता है। जिस story में दोनों में से कुछ नहीं है, जैसे ग्रेडिएंट पर टेक्स्ट वाली story, उसे आपकाrkStoriesSlideटेम्पलेट प्लेयर के आकार में बनाता है और फिर छोटा किया जाता है; बिना poster वाला वीडियो कार्ड की पृष्ठभूमि दिखाता है। उस झलक कोisActive: falseमिलता है, और उसके lifecycle callback कुछ नहीं करते, इसलिए वह स्क्रीन वाली story का टाइमर नहीं चला सकती। स्लाइड बनते ही जो कुछ करती है, जैसे प्लेबैक या analytics, उसेisActiveके पीछे रखें। - टाइमर स्लाइड का इंतज़ार करता है। कार्ड का सरकना पूरा होने तक story अपना टाइमर शुरू नहीं करती, और उसके लिए
(storyViewed)नहीं चलता। - समूह बदलने का हर तरीका सरकता है: का र्ड पर क्लिक,
StoriesApi.goToGroup,nextGroupऔरprevGroup, और समूह के अंत के आगे तीर वाली कुंजियाँ। touch swipe प्लेयर को खुद हिलाता है, इसलिए उसके ऊपर कोई सरकना नहीं होता। - कार्ड का रूप बदलना
rkStoriesGroupPreviewस्लॉट से होता है, जो कस्टम कैरोसेल कार्ड में दिखाया गया है। कार्ड की जगह, स्केल और सरकना प्लेयर ही संभालता है।
हर समूह का अपना प्रोग्रेस बार और हेडर
डिफ़ॉल्ट रूप से एक प्रोग्रेस बार और एक हेडर प्लेयर के ऊपर रहते हैं और समूह बदल जाने के बाद नए समूह पर आ जाते हैं। chromePlacement="group" सेट करने पर हर समूह को अपनी slide के अंदर अपना बार और हेडर मिलता है, और दोनों Instagram की तरह समूह के साथ घूमते हैं।
// Every group carries its own progress bar and header, so both turn with the
// group instead of switching above the player once it has changed.
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
chromePlacement="group"
(closed)="isOpen.set(false)"
/>
`,
})
export class GroupChromeFeedComponent {}- पड़ोसी समूह दिखाता है कि वह कहाँ रुका है। जब प्लेयर उसकी ओर घूमता है, उसका बार और हेडर वह story दिखाते हैं जिस पर समूह खुलेगा, जिसका अभी कुछ भी नहीं चला है।
- छोड़ा जा रहा समूह जैसा था वैसा रहता है। घुमाव ख़त्म होने तक उसका बार अपनी प्रोग्रेस बनाए रखता है, इसलिए अंत तक देखा गया समूह घुमाव के बीच ख़ाली नहीं होता, भरा ही रहता है।
- ये टेम्पलेट स्क्रीन पर मौजूद हर समूह के लिए बनते हैं।
rkStoriesProgressBarऔरrkStoriesHeaderके कॉन्टेक्स्ट मेंgroupIndexऔरisActiveहोते हैं; पड़ोसी समूह के लिएisActivefalseहोता है और प्रोग्रेस के सिग्नल नहीं बदलते। rkStoriesHeaderटेम्पलेट swipe क्षेत्र के अंदर होता है। उस पर tap करने से story बदलती है, जब तक tap किसीbutton, लिंक याrole="button"वाले element पर न पड़े, इसलिए उसके हर control को इन्हीं में से एक बनाएँ।- डेस्कटॉप कैरोसेल slide के दौरान नहीं बदलता। कार्ड slide होते समय प्लेयर छिपा रहता है, इसलिए यह विकल्प समूह खुलने के बाद दिखता है।
टेम्पलेट स्लॉट
आठ ng-template डायरेक्टिव प्लेयर के किसी भी ह िस्से को बदल देते हैं। हर एक को टाइप किया हुआ कॉन्टेक्स्ट मिलता है, जिसमें हर मान का नाम होता है — वही नाम जो React और Vue स्लॉट इस्तेमाल करते हैं — इसलिए हेडर स्लॉट में let-author="author" लेखक दे देता है। मुख्य मान implicit भी होता है, इसलिए सिर्फ़ let-author भी यही करता है। जो स्लॉट आप नहीं देते, वहाँ डिफ़ॉल्ट प्रस्तुति बनी रहती है।
| डायरेक्टिव | क्लास | कॉन्टेक्स्ट | विवरण |
|---|---|---|---|
rkStoriesError | RkStoriesErrorDirective | { $implicit: story, story, storyIndex, groupIndex } | अंतर्निहित "Content unavailable" पैनल को बदलता है, जो चालू story के ऊपर बनता है |
rkStoriesFooter | RkStoriesFooterDirective | { $implicit: story, story, author, storyIndex } | चालू story के नीचे फ़ुटर जोड़ता है; प्लेयर खुद कोई फ़ुटर नहीं बनाता |
rkStoriesGroupPreview | RkStoriesGroupPreviewDirective | { $implicit: group, group, groupIndex, story, offset, viewedCount, onOpen } | डेस्कटॉप कैरोसेल के कार्ड को भरता है; जगह, स्केल और स्लाइडिंग प्लेयर ही संभालता रहता है |
rkStoriesHeader | RkStoriesHeaderDirective | { $implicit: author, author, story, storyIndex, isPaused, isMuted, isVideo, groupIndex, isActive, onToggleSound, onTogglePause, onClose } | डिफ़ॉल्ट हेडर (लेखक, पॉज़, साउंड, बंद करना) बदलता है। chromePlacement="group" के साथ यह swipe क्षेत्र के अंदर होता है; देखें हर समूह का अपना प्रोग्रेस बार और हेडर। |
rkStoriesLoading | RkStoriesLoadingDirective | { $implicit: story, story, storyIndex, groupIndex } | चालू story लोड होते समय हेडर में दिखने वाले डिफ़ॉल्ट स्पिनर को बदलता है |
rkStoriesNavigation | RkStoriesNavigationDirective | { onPrevStory, onNextStory, onPrevGroup, onNextGroup, $implicit: { the same four } } | डेस्कटॉप के दोनों तीर बदल देता है |
rkStoriesProgressBar | RkStoriesProgressBarDirective | { $implicit: group, group, totalStories, activeIndex, progress, groupIndex, isActive } | canvas प्रोग्रेस बार को बदलता है। activeIndex और progress कोर सिग्नल हैं — उन्हें toAngularSignal से जोड़ें। |
rkStoriesSlide | RkStoriesSlideDirective | { $implicit: story, story, index, groupIndex, isActive, size, activeGroupIndex, activeStoryIndex, onDurationReady, onReady, onWaiting, onError, onEnded } | डिफ़ॉल्ट इमेज और वीडियो स्लाइड बदलता है |
import {
RkStoriesOverlayComponent,
RkStoriesHeaderDirective,
RkStoriesFooterDirective,
} from '@reelkit/angular-stories-player';
@Component({
imports: [
RkStoriesOverlayComponent,
RkStoriesHeaderDirective,
RkStoriesFooterDirective,
],
template: `
<rk-stories-overlay [isOpen]="isOpen()" [groups]="groups" (closed)="isOpen.set(false)">
<!-- The author is the implicit value; everything else is named. -->
<ng-template rkStoriesHeader let-author let-onClose="onClose">
<header class="my-header">
<img [src]="author.avatar" [alt]="author.name" />
<strong>{{ author.name }}</strong>
<button type="button" (click)="onClose()">Close</button>
</header>
</ng-template>
<!-- The player draws no footer of its own; this adds one. -->
<ng-template rkStoriesFooter let-story let-author="author">
<footer class="my-footer">Reply to {{ author.name }}</footer>
</ng-template>
</rk-stories-overlay>
`,
})
export class SlottedComponent {}STORIES_TEMPLATE_SLOT_DIRECTIVES आठों क्लास को एक ही array के रूप में एक्सपोर्ट करता है, उस कंपोनेंट के लिए जो कई स्लॉट इंपोर्ट करता है।
हर स्लॉट प्लेयर के अपने injector से रेंडर होता है, इसलिए स्लॉट के भीतर बना कंपोनेंट प्लेयर के provider तक पहुँच पाता है — स्लाइड टेम्पलेट में रखा rk-video-story-slide वही साउंड स्टेट पाता है जिसे हेडर का बटन लिखता है।
कस्टम स्लाइड
import {
RkStoriesSlideDirective,
RkImageStorySlideComponent,
RkVideoStorySlideComponent,
} from '@reelkit/angular-stories-player';
// A slide template is projected content, so it cannot pick up the player's
// story type on its own — cast to your own feed's type where you know it.
@Component({
template: `
<rk-stories-overlay [isOpen]="isOpen()" [groups]="groups" (closed)="isOpen.set(false)">
<ng-template
rkStoriesSlide
let-story
let-groupIndex="groupIndex"
let-index="index"
let-activeGroupIndex="activeGroupIndex"
let-activeStoryIndex="activeStoryIndex"
let-onReady="onReady"
let-onError="onError"
let-onDurationReady="onDurationReady"
>
@if (story.mediaType === 'video') {
<!-- Every video plays through one shared element, which this slide
claims while both indexes point at it. -->
<rk-video-story-slide
[src]="story.src"
[poster]="story.poster"
[groupIndex]="groupIndex"
[storyIndex]="index"
[activeGroupIndex]="activeGroupIndex"
[activeStoryIndex]="activeStoryIndex"
(durationReady)="onDurationReady($event)"
(playbackStarted)="onReady()"
(failed)="onError()"
/>
} @else {
<rk-image-story-slide
[src]="story.src"
(loaded)="onReady()"
(failed)="onError()"
/>
}
</ng-template>
</rk-stories-overlay>
`,
})
export class CustomSlideComponent {}कस्टम स्लाइड rkStoriesSlide कॉन्टेक्स्ट के callbacks से प्लेयर को बताती है कि उसकी सामग्री किस हाल में है। प्लेयर इन पर सिर्फ़ स्क्रीन वाली story के लिए अमल करता है; किसी और स्लाइड से आई कॉल अनदेखी कर दी जाती है।
| Callback | कब |
|---|---|
onReady | सामग्री तैयार है (इमेज लोड हुई, वीडियो चल रहा है)। प्रोग्रेस टाइमर शुरू होता है, या कैरोसेल स्लाइड ख़त्म होने का इंतज़ार करता है। |
onWaiting | सामग्री रुक गई है (प्लेबैक के बीच वीडियो buffer हो रहा है)। स्पिनर दिखता है और टाइमर रुक जाता है। |
onError | सामग्री लोड नहीं हो पाई। त्रुटि पैनल दिखता है और टाइमर रुक जाता है। |
onDurationReady | media की असली लंबाई मिलीसेकंड में बताएँ, उदाहरण के लिए वीडियो metadata से, ताकि टाइमर उससे मेल खाए। story पर सेट की गई duration इस पर भारी पड़ती है। |
onEnded | media अपने आप ख़त्म हो गया, उदाहरण के लिए वीडियो अंत तक चल गया। प्लेयर टाइमर का इंतज़ार किए बिना अगली story पर चला जाता है। |
rk-image-story-slide (loaded) और (failed) भेजता है, और rk-video-story-slide (playbackStarted), (buffering), (failed), (durationReady) और (finished) भेजता है, इसलिए इन्हें लपेटने वाले स्लाइड टेम्पलेट को बस हर एक को मेल खाते callback तक पहुँचाना होता है।
प्रीलोडर कैशिंग
अंतर्निहित RkImageStorySlideComponent और RkVideoStorySlideComponent अगली story को पृष्ठभूमि में पहले से लोड कर लेते हैं। जब दर्शक पहले से लोड की गई story तक पहुँचता है, तो सामग्री तुरंत दिखती है, बिना किसी लोडिंग स्पिनर के। जो URL एक बार नाकाम हो गया वह भी याद रखा जाता है, इसलिए टूटी हुई story दोबारा खोलने पर फिर से कोशिश करने के बजाय सीधे त्रुटि दिखती है।
कस्टम नेविगेशन
// The four moves arrive as the implicit value, so `let-nav` is all a
// template needs. Supplying this replaces both built-in arrows.
<ng-template rkStoriesNavigation let-nav>
<button type="button" (click)="nav.onPrevGroup()">Previous user</button>
<button type="button" (click)="nav.onPrevStory()">Back</button>
<button type="button" (click)="nav.onNextStory()">Next</button>
<button type="button" (click)="nav.onNextGroup()">Next user</button>
</ng-template>कस्टम प्रोग्रेस बार
// activeIndex and progress are core signals, the same ones the built-in
// canvas bar reads. Bridge them with toAngularSignal to follow the timer.
<ng-template
rkStoriesProgressBar
let-group
let-total="totalStories"
let-activeIndex="activeIndex"
let-progress="progress"
>
<app-my-progress-bar
[totalStories]="total"
[activeIndex]="activeIndex"
[progress]="progress"
/>
</ng-template>कस्टम लोडिंग और त्रुटि
// Both are scoped to the story on screen: the player reports one loading
// state, and these draw only over the slide the viewer is looking at.
<ng-template rkStoriesLoading let-story>
<div class="my-spinner">Loading {{ story.mediaType }}…</div>
</ng-template>
<ng-template rkStoriesError let-story>
<div class="my-error">That story could not be loaded.</div>
</ng-template>कस्टम कैरोसेल कार्ड
// One side card of the desktop carousel. `offset` is negative on the left
// and positive on the right, and 0 while the card slides through the centre.
<ng-template
rkStoriesGroupPreview
let-group
let-story="story"
let-offset="offset"
let-viewedCount="viewedCount"
let-onOpen="onOpen"
>
<button type="button" class="my-card" (click)="onOpen()">
<img [src]="group.author.avatar" [alt]="group.author.name" />
<span>{{ group.author.name }}</span>
<span>{{ viewedCount }} / {{ group.stories.length }} seen</span>
</button>
</ng-template>ट्रांज़िशन
groupTransition तय करता है कि लेखक बदलते समय कौन-सा 3D असर चले। ट्रांज़िशन फ़ंक्शन @reelkit/angular से इंपोर्ट करें:
import { cubeTransition, flipTransition, fadeTransition } from '@reelkit/angular';
@Component({
template: `
<!-- Between users only. Stories inside one user always cross-fade. -->
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
[groupTransition]="flip"
[innerTransitionDuration]="200"
(closed)="isOpen.set(false)"
/>
`,
})
export class TransitionsComponent {
protected readonly flip = flipTransition;
}डबल-टैप और लाइक
डबल टैप पर दिल का एनिमेशन चलता है, जो तुरंत दृश्य प्रतिक्रिया देता है। (doubleTapped) समूह और story का इंडेक्स देता है ताकि आप लाइक अपनी स्थिति में सहेज सकें। प्लेयर खुद लाइक की कोई स्थिति नहीं रखता।
// A double tap plays the heart over the story and reports which story it
// landed on. The heart draws itself; this is only the reporting half.
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
(doubleTapped)="like($event.groupIndex, $event.storyIndex)"
(closed)="isOpen.set(false)"
/>दिल के एनिमेशन को अपनी तरह ढालना
गति --rk-stories-heart-duration टोकन से बदलें (थीमिंग देखें)। रंग, आकार, या दिल को पूरी तरह छिपाने के लिए .rk-stories-heart क्लास पर निशाना लगाएँ। RkHeartAnimationComponent अलग से इस्तेमाल के लिए एक्सपोर्ट किया गया है।
:root {
/* Pop-in and fade-out duration. */
--rk-stories-heart-duration: 500ms;
}
/* Colour and size are plain CSS on the element itself. */
.rk-stories-heart svg {
width: 72px;
height: 72px;
fill: #ff4d6d;
}
/* Or hide it and animate your own from the double-tap event. */
.rk-stories-heart {
display: none;
}अंतर्निहित दिल का एनिमेशन अभी किसी स्लॉट से नहीं बदला जा सकता। इसे CSS से दोबारा स्टाइल करें, या display: none से छिपाकर (doubleTapped) से अपना एनिमेशन चलाएँ। अगर आपको दिल के लिए स्लॉट चाहिए, तो GitHub Issues पर बताएँ।
StoriesApi
इंपेरेटिव नियंत्रण के लिए API (apiReady) से लें। प्लेयर हर बार खुलने पर दोबारा बनता है, इसलिए पिछली बार का रखा हुआ handle पुराना पड़ जाता है — हमेशा सबसे नया इस्तेमाल करें।
import type { StoriesApi } from '@reelkit/angular-stories-player';
@Component({
template: `
<rk-stories-overlay
[isOpen]="isOpen()"
[groups]="groups"
(apiReady)="api = $event"
(closed)="isOpen.set(false)"
/>
<button type="button" (click)="api?.nextGroup()">Skip this user</button>
`,
})
export class DrivenComponent {
// Handed over once the player is ready to take orders, and again on every
// open: the player is rebuilt each time, so keep the newest one.
protected api: StoriesApi | null = null;
}मेथड
| मेथड | टाइप | विवरण |
|---|---|---|
nextStory() | () => void | मौजूदा समूह के भीतर अगली story पर जाएँ |
prevStory() | () => void | मौजूदा समूह के भीतर पिछली story पर लौटें |
nextGroup() | () => void | अगले लेखक के समूह पर जाएँ |
prevGroup() | () => void | पिछले लेखक के समूह पर जाएँ |
goToGroup(index) | (index: number) => void | इंडेक्स से किसी समूह पर सीधे जाएँ |
pause() | () => void | ऑटो-एडवांस और प्रोग्रेस टाइमर रोकें |
resume() | () => void | ऑटो-एडवांस और प्रोग्रेस टाइमर फिर चालू करें |
उप-कंपोनेंट
एक्सपोर्ट किए गए हिस्से, जिन्हें स्लॉट के भीतर जोड़ा जा सकता है या अलग से इस्तेमाल किया जा सकता है।
| कंपोनेंट | यह क्या है |
|---|---|
RkCanvasProgressBarComponent | canvas पर खंडों वाला प्रोग्रेस बार, सीधे टाइमर सिग्नल से बनाया गया। बहुत सारी stories वाले समूह के लिए खिसकती खिड़की संभालता है। [live]="false" के साथ यह बिना animation loop के सिर्फ़ सिग्नल बदलने पर और resize के ब ाद बनता है। आकार और रंग अलग-अलग inputs हैं: gap, barHeight, minSegmentWidth, bgColor, fillColor। |
RkHeartAnimationComponent | डबल-टैप वाला दिल। एनिमेशन खत्म होने पर (completed) भेजता है, ताकि माता-पिता कंपोनेंट उसे हटा सके। |
RkImageStorySlideComponent | object-fit: cover वाली पूरी चौड़ाई की इमेज स्लाइड, जो लोड और त्रुटि की सूचना output से देती है। |
RkStoriesRingComponent | ग्रेडिएंट रिंग वाला गोल अवतार। दो स्थितियाँ: कुछ देखना बाकी हो तो घूमता है, पूरा देख लेने पर सपाट हो जाता है। (clicked) भेजता है। |
RkStoriesRingListComponent | लेखकों के नाम के साथ रिंग की क्षैतिज पंक्ति, हर समूह के लिए एक। समूह के इंडेक्स के साथ (selected) भेजता है। |
RkStoryHeaderComponent | डिफ़ॉल्ट हेडर: अवतार, नाम, सत्यापित ब ैज, सापेक्ष समय, पॉज़, साउंड, बंद करना और लोडिंग स्पिनर। पॉज़ और साउंड बटन तभी दिखते हैं जब [showPauseButton] और [showSoundButton] सेट हों, जैसे प्लेयर अपने हेडर के लिए उन्हें सेट करता है। |
RkVideoStorySlideComponent | एक ही साझा <video> तत्व पर टिकी वीडियो स्लाइड, ताकि iOS पर आवाज़ टूटे नहीं। ऑटोप्ले, पोस्टर फ़्रेम और अवधि की सूचना संभालती है। |
RkStoriesCarouselComponent भी अपने CarouselSlide टाइप के साथ एक्सपोर्ट होता है, लेकिन जिन मानों से वह बनता है वे सब ओवरले के पास हैं — कार्ड का रूप बदलना हो तो कैरोसेल खुद माउंट करने के बजाय rkStoriesGroupPreview स्लॉट इस्तेमाल करें।
import {
RkStoriesRingComponent,
RkStoriesRingListComponent,
RkStoryHeaderComponent,
RkCanvasProgressBarComponent,
RkImageStorySlideComponent,
RkVideoStorySlideComponent,
RkHeartAnimationComponent,
} from '@reelkit/angular-stories-player';
// Each piece the player draws is exported on its own, so a page can build a
// ring row without the overlay, or a slot can rebuild a region out of the
// same parts the player uses.
@Component({
imports: [RkStoriesRingComponent],
template: `
<rk-stories-ring
[author]="author"
[totalStories]="5"
[viewedCount]="2"
[size]="68"
(clicked)="open()"
/>
`,
})
export class RingOnlyComponent {}कस्टम story टाइप
StoryItem को अपने फ़ील्ड से बढ़ाएँ और उसी से समूहों को टाइप करें:
// The player is generic over the story type, so a feed can carry whatever
// extra fields a slide template needs.
interface PromoStory extends StoryItem {
title?: string;
ctaText?: string;
}
const groups: StoriesGroup<PromoStory>[] = [
{
author: { id: 'shop', name: 'Shop', avatar: '/shop.jpg' },
stories: [
{ id: 'p1', mediaType: 'image', src: '/promo.jpg', title: 'Flash Sale', ctaText: 'Shop now' },
],
},
];पुनः एक्सपोर्ट
उपभोक्ता को जो कुछ चाहिए वह सब इसी पैकेज से आता है, इसलिए @reelkit/core से सीधे इंपोर्ट करने की कभी ज़रूरत नहीं पड़ती।
| कहाँ से | पुनः एक्सपोर्ट |
|---|---|
@reelkit/angular | SoundStateService, createOverlayUrlState, urlIndexTwoAxisKey, urlStableIdTwoAxisKey, base64UrlCodec, createViewedStateController, twoAxisViewedTracking, createLocalStorageAdapter, createSessionStorageAdapter, createMemoryStorageAdapter, और टाइप OverlayUrlStateOptions, UrlAdapter, UrlCodec, UrlLocator, UrlKey, UrlStateController, TwoAxisPosition, TwoAxisIdentity, UrlIndexTwoAxisKeyOptions, ViewedStateController, ViewedStateOptions, StorageAdapter |
@reelkit/stories-core | createStoriesViewedStateController, StoriesViewedStateController, StoriesViewedStateControllerConfig, और सामग्री टाइप StoryItem, AuthorInfo, StoriesGroup, MediaType |
attachViewedState भी एक्सपोर्ट होता है, उस कंपोनेंट के लिए जो देखे जाने की स्थिति का controller खुद रखता है और उसे तब तक जुड़ा रखना चाहता है जब तक वह कंपोनेंट जीवित है।
RkStoriesOverlayComponent के इनपुट
rk-stories-overlay
| इनपुट | टाइप | डिफ़ॉल्ट | विवरण |
|---|---|---|---|
ariaLabel | string | 'Stories player' | डायलॉग क्षेत्र का सुलभ नाम; ओवरले खुलने पर स्क्रीन रीडर इसे पढ़ते हैं |
chromePlacement | 'overlay' | 'group' | 'overlay' | प्रोग्रेस बार और हेडर कहाँ रहते हैं। 'overlay' प्लेयर के ऊपर एक कॉपी बनाता है; 'group' हर समूह की slide में एक बनाता है, जिससे दोनों समूह के साथ घूमते हैं। यह टाइप ChromePlacement नाम से export होता है। |
defaultImageDuration | number | 5000 | इमेज story के ऑटो-एडवांस की अवधि, मिलीसेकंड में |
desktopLayout | 'single' | 'carousel' | 'single' | डेस्कटॉप लेआउट। 'carousel' पड़ोसी समूहों को चालू story के बगल में प्रीव्यू कार्ड की तरह दिखाता है और समूहों के बीच स्लाइड करता है। फ़ोन पर हमेशा अकेली story ही दिखती है। यह टाइप DesktopLayout के रूप में एक्सपोर्ट होता है। |
enableKeyboard | boolean | true | कीबोर्ड नेविगेशन चालू करता है (बाएँ/दाएँ तीर, Escape) |
groups | StoriesGroup<T>[] | required | दिखाने के लिए story समूह। प्लेयर खुले रहते जोड़े गए समूह या stories बिना दर्शक की जगह खोए शामिल हो जाते हैं। |
groupTransition | TransitionTransformFn | cubeTransition | लेखकों के बीच ट्रांज़िशन। डेस्कटॉप कैरोसेल दिखते समय इसे अनदेखा किया जाता है, तब स्लाइड होता है। |
hideUIOnPause | boolean | true | देर तक दबाए रखने से पॉज़ रहने पर प्रोग्रेस बार और हेडर छिपा देता है |
initialGroupIndex | number | 0 | जिस समूह से प्लेयर खुलता है उसका इंडेक्स (शून्य से) |
initialStoryIndex | number | जहाँ से आगे, वरना 0 | जिस story से प्लेयर खुलता है उसका इंडेक्स (शून्य से)। साफ़ बताने पर यह याद रखी गई जगह से ऊपर रहता है; न देने पर खुलने वाला समूह भी बाकी समूहों की तरह वहीं से आगे चलता है। |
innerTransitionDuration | number | 200 | एक ही लेखक की stories के बीच फ़ेड की अवधि, मिलीसेकंड में |
isOpen | boolean | required | true रहने तक प्लेयर रेंडर करता है और body का स्क्रॉल रोक देता है |
minSegmentWidth | number | 8 | प्रोग्रेस खंड की सबसे कम चौड़ाई, जिसके बाद बार खंडों की खिड़की खिसकाने लगता है, पिक्सेल में |
resumeStoryIndex | (groupIndex: number) => number | — |