@angular/animations#keyframes TypeScript Examples
The following examples show how to use
@angular/animations#keyframes.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: animations.ts From Angular-Cookbook with MIT License | 6 votes |
cardAnimation = trigger('cardAnimation', [
state(
'active',
style({
color: 'rgb(51, 51, 51)',
backgroundColor: 'white',
})
),
transition('void => *', [
animate(
'1.5s ease',
keyframes([
style({
transform: 'translateX(-200px) scale3d(0.4, 0.4, 0.4)',
offset: 0,
}),
style({
transform: 'translateX(0px) rotate(-90deg) scale3d(0.5, 0.5, 0.5)',
offset: 0.25,
}),
style({
transform:
'translateX(-200px) rotate(90deg) translateY(0) scale3d(0.6, 0.6, 0.6)',
offset: 0.5,
}),
style({
transform:
'translateX(-100px) rotate(135deg) translateY(0) scale3d(0.6, 0.6, 0.6)',
offset: 0.75,
}),
style({
transform: 'translateX(0) rotate(360deg)',
offset: 1,
}),
])
),
]),
])
Example #2
Source File: mat-fab-menu.animations.ts From fab-menu with MIT License | 5 votes |
speedDialFabAnimations = [
trigger('fabToggler', [
state('false', style({
transform: 'rotate(0deg)'
})),
state('true', style({
transform: 'rotate(225deg)'
})),
transition('* <=> *', animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
]),
trigger('fabsStagger', [
transition('* => *', [
query(':enter', style({opacity: 0}), {optional: true}),
query(':enter', stagger('40ms',
[
animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
keyframes(
[
style({opacity: 0, transform: 'translateY(10px)'}),
style({opacity: 1, transform: 'translateY(0)'}),
]
)
)
]
), {optional: true}),
query(':leave',
animate('200ms cubic-bezier(0.4, 0.0, 0.2, 1)',
keyframes([
style({opacity: 1}),
style({opacity: 0}),
])
), {optional: true}
)
])
])
]
Example #3
Source File: banner.component.ts From avid-covider with MIT License | 5 votes |
@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.less'],
animations: [
trigger('openClose', [
state('open', style({ transform: 'translateY(0%)' })),
state('closed', style({ transform: 'translateY(-100%)', display: 'none'})),
transition('open => closed', [
sequence([
animate('0.25s 0s ease-in', keyframes([
style({transform: 'translateY(0%)'}),
style({transform: 'translateY(-100%)'})
])),
style({display: 'none'})
])
]),
transition('closed => open', [
sequence([
style({display: 'flex'}),
animate('0.25s 0s ease-out', keyframes([
style({transform: 'translateY(-100%)'}),
style({transform: 'translateY(0%)'})
]))
])
])
])
]
})
export class BannerComponent implements OnInit {
@Input() message: string = null;
@Input() buttonMessage: string = null;
@Output() result = new EventEmitter<boolean>();
constructor() { }
ngOnInit() {
}
close(value) {
this.message = null;
this.result.emit(value);
}
}
Example #4
Source File: reminder-widget.component.ts From avid-covider with MIT License | 5 votes |
@Component({
selector: 'app-reminder-widget',
templateUrl: './reminder-widget.component.html',
styleUrls: ['./reminder-widget.component.less'],
animations: [
trigger('openClose', [
state('open', style({transform: 'translateY(0%)'})),
state('closed', style({transform: 'translateY(100%)', display: 'none'})),
transition('open => closed', [
sequence([
animate('0.25s 0s ease-in', keyframes([
style({transform: 'translateY(0%)'}),
style({transform: 'translateY(100%)'})
])),
style({display: 'none'})
])
]),
transition('closed => open', [
sequence([
style({display: 'flex'}),
animate('0.25s 0s ease-out', keyframes([
style({transform: 'translateY(100%)'}),
style({transform: 'translateY(0%)'})
]))
])
])
])
]
})
export class ReminderWidgetComponent implements OnInit, OnChanges {
@Output() select = new EventEmitter<string>();
@Input() options: any[] = null;
isOpen = false;
constructor() { }
ngOnInit() {
}
resolve(value?) {
this.select.emit(value);
this.isOpen = false;
}
ngOnChanges() {
this.isOpen = !!this.options && !!this.options.length;
}
}
Example #5
Source File: toaster.component.ts From avid-covider with MIT License | 5 votes |
@Component({
selector: 'app-toaster',
templateUrl: './toaster.component.html',
styleUrls: ['./toaster.component.less'],
animations: [
trigger('openClose', [
state('closed', style({ filter: 'blur(20px)', transform: 'translateY(-35%)', opacity: 0, display: 'none'})),
state('open', style({ filter: 'blur(0px)', transform: 'translateY(0%)', opacity: 1, })),
transition('open => closed', [
sequence([
animate('0.5s', keyframes([
style({ filter: 'blur(0px)', transform: 'translateY(0%)', opacity: 1 }),
style({ filter: 'blur(20px)', transform: 'translateY(-35%)', opacity: 0 })
])),
style({ display: 'none' })
])
]),
transition('closed => open', [
sequence([
style({ display: 'flex' }),
animate('0.5s', keyframes([
style({ filter: 'blur(20px)', transform: 'translateY(-35%)', opacity: 0 }),
style({ filter: 'blur(0px)', transform: 'translateY(0%)', opacity: 1 })
]))
])
])
])
]
})
export class ToasterComponent implements OnInit, OnChanges {
@Input() message: string = null;
constructor() { }
ngOnInit() {
}
ngOnChanges(): void {
if (this.message) {
setTimeout(() => {
this.message = null;
}, 5000);
}
}
}
Example #6
Source File: toast.component.ts From radiopanel with GNU General Public License v3.0 | 5 votes |
@Component({
selector: 'app-toast',
templateUrl: './toast.component.html',
animations: [
trigger('flyInOut', [
state('inactive', style({
opacity: 0,
})),
transition('inactive => active', animate('400ms ease-out', keyframes([
style({
transform: 'translate3d(100%, 0, 0) skewX(-30deg)',
opacity: 0,
}),
style({
transform: 'skewX(20deg)',
opacity: 1,
}),
style({
transform: 'skewX(-5deg)',
opacity: 1,
}),
style({
transform: 'none',
opacity: 1,
}),
]))),
transition('active => removed', animate('400ms ease-out', keyframes([
style({
opacity: 1,
}),
style({
transform: 'translate3d(100%, 0, 0) skewX(30deg)',
opacity: 0,
}),
]))),
]),
],
preserveWhitespaces: false,
})
export class ToastComponent extends Toast {
constructor(
protected toastrService: ToastrService,
public toastPackage: ToastPackage,
) {
super(toastrService, toastPackage);
}
action(event: Event) {
event.stopPropagation();
this.toastPackage.triggerAction();
return false;
}
}
Example #7
Source File: panel.component.ts From ngx-colors with MIT License | 4 votes |
@Component({
selector: "ngx-colors-panel",
templateUrl: "./panel.component.html",
styleUrls: ["./panel.component.scss"],
animations: [
trigger("colorsAnimation", [
transition("void => slide-in", [
// Initially all colors are hidden
query(":enter", style({ opacity: 0 }), { optional: true }),
//slide-in animation
query(
":enter",
stagger("10ms", [
animate(
".3s ease-in",
keyframes([
style({ opacity: 0, transform: "translatex(-50%)", offset: 0 }),
style({
opacity: 0.5,
transform: "translatex(-10px) scale(1.1)",
offset: 0.3,
}),
style({ opacity: 1, transform: "translatex(0)", offset: 1 }),
])
),
]),
{ optional: true }
),
]),
//popup animation
transition("void => popup", [
query(":enter", style({ opacity: 0, transform: "scale(0)" }), {
optional: true,
}),
query(
":enter",
stagger("10ms", [
animate(
"500ms ease-out",
keyframes([
style({ opacity: 0.5, transform: "scale(.5)", offset: 0.3 }),
style({ opacity: 1, transform: "scale(1.1)", offset: 0.8 }),
style({ opacity: 1, transform: "scale(1)", offset: 1 }),
])
),
]),
{ optional: true }
),
]),
]),
],
})
export class PanelComponent implements OnInit {
@HostListener("document:mousedown", ["$event"])
click(event) {
if (this.isOutside(event)) {
this.emitClose("cancel");
}
}
@HostListener("document:scroll")
onScroll() {
this.onScreenMovement();
}
@HostListener("window:resize")
onResize() {
this.onScreenMovement();
}
@HostBinding("style.top.px") public top: number;
@HostBinding("style.left.px") public left: number;
@ViewChild("dialog") panelRef: ElementRef;
constructor(
public service: ConverterService,
private cdr: ChangeDetectorRef
) {}
public color = "#000000";
public previewColor: string = "#000000";
public hsva = new Hsva(0, 1, 1, 1);
public colorsAnimationEffect = "slide-in";
public palette = defaultColors;
public variants = [];
public colorFormats = formats;
public format: ColorFormats = ColorFormats.HEX;
public canChangeFormat: boolean = true;
public menu = 1;
public hideColorPicker: boolean = false;
public hideTextInput: boolean = false;
public acceptLabel: string;
public cancelLabel: string;
public colorPickerControls: "default" | "only-alpha" | "no-alpha" = "default";
private triggerInstance: NgxColorsTriggerDirective;
private TriggerBBox;
public isSelectedColorInPalette: boolean;
public indexSeleccionado;
public positionString;
public temporalColor;
public backupColor;
public ngOnInit() {
this.setPosition();
this.hsva = this.service.stringToHsva(this.color);
this.indexSeleccionado = this.findIndexSelectedColor(this.palette);
}
public ngAfterViewInit() {
this.setPositionY();
}
private onScreenMovement() {
this.setPosition();
this.setPositionY();
if (!this.panelRef.nativeElement.style.transition) {
this.panelRef.nativeElement.style.transition = "transform 0.5s ease-out";
}
}
private findIndexSelectedColor(colors): number {
let resultIndex = undefined;
if (this.color) {
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
if (typeof color == "string") {
if (
this.service.stringToFormat(this.color, ColorFormats.HEX) ==
this.service.stringToFormat(color, ColorFormats.HEX)
) {
resultIndex = i;
}
} else {
if (this.findIndexSelectedColor(color.variants) != undefined) {
resultIndex = i;
}
}
}
}
return resultIndex;
}
public iniciate(
triggerInstance: NgxColorsTriggerDirective,
triggerElementRef,
color,
palette,
animation,
format: string,
hideTextInput: boolean,
hideColorPicker: boolean,
acceptLabel: string,
cancelLabel: string,
colorPickerControls: "default" | "only-alpha" | "no-alpha",
position: "top" | "bottom"
) {
this.colorPickerControls = colorPickerControls;
this.triggerInstance = triggerInstance;
this.TriggerBBox = triggerElementRef;
this.color = color;
this.hideColorPicker = hideColorPicker;
this.hideTextInput = hideTextInput;
this.acceptLabel = acceptLabel;
this.cancelLabel = cancelLabel;
if (format) {
if (formats.includes(format)) {
this.format = formats.indexOf(format.toLowerCase());
this.canChangeFormat = false;
if (
this.service.getFormatByString(this.color) != format.toLowerCase()
) {
this.setColor(this.service.stringToHsva(this.color));
}
} else {
console.error("Format provided is invalid, using HEX");
this.format = ColorFormats.HEX;
}
} else {
this.format = formats.indexOf(this.service.getFormatByString(this.color));
}
this.previewColor = this.color;
this.palette = palette ?? defaultColors;
this.colorsAnimationEffect = animation;
if (position == "top") {
let TriggerBBox = this.TriggerBBox.nativeElement.getBoundingClientRect();
this.positionString =
"transform: translateY(calc( -100% - " + TriggerBBox.height + "px ))";
}
}
public setPosition(): void {
if (this.TriggerBBox) {
const panelWidth = 250;
const viewportOffset = this.TriggerBBox.nativeElement.getBoundingClientRect();
this.top = viewportOffset.top + viewportOffset.height;
if (viewportOffset.left + panelWidth > window.innerWidth) {
this.left = viewportOffset.right < panelWidth ? window.innerWidth / 2 - panelWidth / 2 : viewportOffset.right - panelWidth;
} else {
this.left = viewportOffset.left;
}
}
}
private setPositionY(): void {
const triggerBBox = this.TriggerBBox.nativeElement.getBoundingClientRect();
const panelBBox = this.panelRef.nativeElement.getBoundingClientRect();
const panelHeight = panelBBox.height;
// Check for space below the trigger
if (triggerBBox.bottom + panelHeight > window.innerHeight) {
// there is no space, move panel over the trigger
this.positionString =
triggerBBox.top < panelBBox.height
? 'transform: translateY(-' + triggerBBox.bottom + 'px );'
: 'transform: translateY(calc( -100% - ' + triggerBBox.height + 'px ));';
} else {
this.positionString = '';
}
this.cdr.detectChanges();
}
public hasVariant(color): boolean {
if (!this.previewColor) {
return false;
}
return (
typeof color != "string" &&
color.variants.some(
(v) => v.toUpperCase() == this.previewColor.toUpperCase()
)
);
}
public isSelected(color) {
if (!this.previewColor) {
return false;
}
return (
typeof color == "string" &&
color.toUpperCase() == this.previewColor.toUpperCase()
);
}
public getBackgroundColor(color) {
if (typeof color == "string") {
return { background: color };
} else {
return { background: color?.preview };
}
}
public onAlphaChange(event) {
this.palette = this.ChangeAlphaOnPalette(event, this.palette);
}
private ChangeAlphaOnPalette(
alpha,
colors: Array<string | NgxColor>
): Array<any> {
var result = [];
for (let i = 0; i < colors.length; i++) {
const color = colors[i];
if (typeof color == "string") {
let newColor = this.service.stringToHsva(color);
newColor.onAlphaChange(alpha);
result.push(this.service.toFormat(newColor, this.format));
} else {
let newColor = new NgxColor();
let newColorPreview = this.service.stringToHsva(color.preview);
newColorPreview.onAlphaChange(alpha);
newColor.preview = this.service.toFormat(newColorPreview, this.format);
newColor.variants = this.ChangeAlphaOnPalette(alpha, color.variants);
result.push(newColor);
}
}
return result;
}
/**
* Change color from default colors
* @param string color
*/
public changeColor(color: string): void {
this.setColor(this.service.stringToHsva(color));
// this.triggerInstance.onChange();
this.emitClose("accept");
}
public onChangeColorPicker(event: Hsva) {
this.temporalColor = event;
this.color = this.service.toFormat(event, this.format);
// this.setColor(event);
this.triggerInstance.sliderChange(
this.service.toFormat(event, this.format)
);
}
public changeColorManual(color: string): void {
this.previewColor = color;
this.color = color;
this.hsva = this.service.stringToHsva(color);
this.triggerInstance.setColor(this.color);
// this.triggerInstance.onChange();
}
setColor(value: Hsva) {
this.hsva = value;
this.color = this.service.toFormat(value, this.format);
this.setPreviewColor(value);
this.triggerInstance.setColor(this.color);
}
setPreviewColor(value: Hsva) {
this.previewColor = this.service.hsvaToRgba(value).toString();
}
hsvaToRgba;
onChange() {
// this.triggerInstance.onChange();
}
public onColorClick(color) {
if (typeof color == "string") {
this.changeColor(color);
} else {
this.variants = color.variants;
this.menu = 2;
}
}
public addColor() {
this.menu = 3;
this.backupColor = this.color;
this.color = "#FF0000";
this.temporalColor = this.service.stringToHsva(this.color);
}
public nextFormat() {
if (this.canChangeFormat) {
this.format = (this.format + 1) % this.colorFormats.length;
this.setColor(this.hsva);
}
}
public emitClose(status: "cancel" | "accept") {
if (this.menu == 3) {
if (status == "cancel") {
} else if (status == "accept") {
this.setColor(this.temporalColor);
}
}
this.triggerInstance.close();
}
public onClickBack() {
if (this.menu == 3) {
this.color = this.backupColor;
this.hsva = this.service.stringToHsva(this.color);
}
this.indexSeleccionado = this.findIndexSelectedColor(this.palette);
this.menu = 1;
}
isOutside(event) {
return event.target.classList.contains("ngx-colors-overlay");
}
}
Example #8
Source File: animations.ts From Angular-Cookbook with MIT License | 4 votes |
ROUTE_ANIMATION = trigger('routeAnimation', [
transition('* <=> *', [
style({
position: 'relative',
perspective: '1000px',
}),
query(
':enter, :leave',
[
style({
position: 'absolute',
width: '100%',
}),
],
optional
),
query(
':enter',
[
style({
opacity: 0,
}),
],
optional
),
group([
query(
':leave',
[
animate(
'1s ease-in',
keyframes([
style({
opacity: 1,
offset: 0,
transform: 'rotateY(0) translateX(0) translateZ(0)',
}),
style({
offset: 0.25,
transform:
'rotateY(45deg) translateX(25%) translateZ(100px) translateY(5%)',
}),
style({
offset: 0.5,
transform:
'rotateY(90deg) translateX(75%) translateZ(400px) translateY(10%)',
}),
style({
offset: 0.75,
transform:
'rotateY(135deg) translateX(75%) translateZ(800px) translateY(15%)',
}),
style({
opacity: 0,
offset: 1,
transform:
'rotateY(180deg) translateX(0) translateZ(1200px) translateY(25%)',
}),
])
),
],
optional
),
query(
':enter',
[
animate(
'1s ease-out',
keyframes([
style({
opacity: 0,
offset: 0,
transform: 'rotateY(180deg) translateX(25%) translateZ(1200px)',
}),
style({
offset: 0.25,
transform:
'rotateY(225deg) translateX(-25%) translateZ(1200px)',
}),
style({
offset: 0.5,
transform: 'rotateY(270deg) translateX(-50%) translateZ(400px)',
}),
style({
offset: 0.75,
transform: 'rotateY(315deg) translateX(-50%) translateZ(25px)',
}),
style({
opacity: 1,
offset: 1,
transform: 'rotateY(360deg) translateX(0) translateZ(0)',
}),
])
),
],
optional
),
]),
]),
])
Example #9
Source File: emote.component.ts From App with MIT License | 4 votes |
@Component({
selector: 'app-emote',
templateUrl: './emote.component.html',
styleUrls: ['./emote.component.scss'],
animations: [
trigger('open', [
transition(':enter', [
animate(500, keyframes([
style({ opacity: 0, offset: 0 }),
style({ opacity: 0, offset: .75 }),
style({ opacity: 1, offset: 1 })
]))
])
])
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmoteComponent implements OnInit {
/** The maximum height an emote can be. This tells where the scope text should be placed */
MAX_HEIGHT = 128;
channels = new BehaviorSubject<UserStructure[]>([]);
channelPage = 1;
@Input() emote: EmoteStructure | undefined;
disableNotices = false;
blurred = new BehaviorSubject<boolean>(true);
sizes = new BehaviorSubject<EmoteComponent.SizeResult[]>([]);
audit = new BehaviorSubject<AuditLogEntry[]>([]);
interactError = new Subject<string>().pipe(
mergeMap(x => scheduled([
of(!!x ? 'ERROR: ' + x : ''),
timer(5000).pipe(
takeUntil(this.interactError),
mapTo('')
)
], asyncScheduler).pipe(mergeAll()))
) as Subject<string>;
constructor(
@Inject(DOCUMENT) private document: Document,
private metaService: Meta,
private restService: RestService,
private route: ActivatedRoute,
private router: Router,
private cdr: ChangeDetectorRef,
private dialog: MatDialog,
private appService: AppService,
private emoteListService: EmoteListService,
private dataService: DataService,
public themingService: ThemingService,
public clientService: ClientService
) { }
/**
* Get all sizes of the current emote
*/
getSizes(): Observable<EmoteComponent.SizeResult[]> {
return from([1, 2, 3, 4]).pipe(
map(s => ({
scope: s,
url: this.restService.CDN.Emote(String(this.emote?.getID()), s),
width: this.emote?.width[s - 1],
height: this.emote?.height[s - 1]
} as EmoteComponent.SizeResult)),
toArray()
);
}
/**
* Method called when the client user interacts with a button
*/
onInteract(interaction: ContextMenuComponent.InteractButton): void {
if (typeof interaction.click === 'function' && !!this.emote) {
interaction.click(this.emote).pipe(
switchMap(() => iif(() => interaction.label === 'add to channel' || interaction.label === 'remove from channel',
this.readChannels().pipe(mapTo(undefined)),
of(undefined)
))
).subscribe({
complete: () => this.interactError.next(''),
error: (err: HttpErrorResponse) => this.interactError.next(this.restService.formatError(err))
});
}
}
get interactions(): ContextMenuComponent.InteractButton[] {
return this.emoteListService.interactions;
}
/**
* Bring up a dialog to rename the current emote
*/
rename(): void {
const dialogRef = this.dialog.open(EmoteRenameDialogComponent, {
data: { emote: this.emote, happening: 'Rename' },
});
dialogRef.afterClosed().pipe(
filter(data => !!data && data.name !== null),
switchMap(data => this.emote?.edit({ name: data.name }, data.reason) ?? EMPTY),
).subscribe();
}
report(): void {
const dialogRef = this.dialog.open(EmoteReportDialogComponent, {
data: { emote: this.emote },
maxWidth: 400
});
dialogRef.afterClosed().pipe(
filter(reason => !!reason && typeof reason === 'string'),
switchMap(reason => this.emote?.report(reason) ?? EMPTY)
).subscribe();
}
/**
* Bring up a dialog to define overrides for the current emote
*/
setOverrides(): void {
const dialogRef = this.dialog.open(EmoteOverridesDialogComponent, {
data: { emote: this.emote, happening: 'set-overrides' },
});
dialogRef.afterClosed().pipe(
filter(data => typeof data?.value === 'number'),
switchMap(data => this.emote?.edit({ visibility: data.value }, data.reason) ?? EMPTY),
).subscribe();
}
/**
* Get the channels that this emote is added to
*/
readChannels(): Observable<UserStructure[]> {
if (!this.emote) return of([]);
return this.emote.getChannels().pipe(
take(1),
tap(users => this.channels.next(users))
);
}
readAuditActivity(): Observable<AuditLogEntry[]> {
return this.emote?.getAuditActivity().pipe(
toArray()
) ?? of([]);
}
formatCreationDate(): Observable<string> {
return this.emote?.getCreatedAt().pipe(
map(d => !!d ? format(d, 'Pp') : '')
) ?? of('');
}
hasTags(): Observable<boolean> {
if (!this.emote) return of(false);
return this.emote.getTags().pipe(
map(a => (a ?? []).length > 0)
);
}
isProcessing(): Observable<boolean> {
return this.emote?.getStatus().pipe(
map(status => status === Constants.Emotes.Status.PROCESSING)
) ?? of(true);
}
isPendingOrDisabled(): Observable<boolean> {
return this.emote?.getStatus().pipe(
map(status => status === Constants.Emotes.Status.PENDING || status === Constants.Emotes.Status.DISABLED)
) ?? of(false);
}
getAliasName(): Observable<string> {
return this.emote?.getAlias().pipe(
map(a => a.length > 0 ? `(${a})` : '')
) ?? of('');
}
/**
* Query the emote's channels by page & update the current set
*
* @param page the channels page
*/
queryChannels(page: number): void {
if (!this.emote) {
return undefined;
}
this.channelPage = page;
this.restService.v2.gql.query<{ emote: DataStructure.Emote }>({
query: `
query PaginateEmoteChannels($id: String!, $page: Int) {
emote(id: $id) {
channels(page: $page) {
id,
login,
display_name,
profile_image_url,
role { id, color }
}
}
}
`,
variables: {
id: this.emote.getID(),
page
},
auth: true
}).pipe(
map(res => res?.body?.data.emote.channels ?? []),
map(chans => this.dataService.add('user', ...chans as DataStructure.TwitchUser[]))
).subscribe({
next: channels => this.channels.next(channels)
});
}
getTotalChannelPages(): Observable<number> {
return this.emote?.getChannelCount().pipe(
map(n => n ?? 0),
map(n => Math.floor(n / 20) + 1)
) ?? of(0);
}
ngOnInit(): void {
const emoteGetter = this.route.snapshot.paramMap.has('emote')
? this.restService.awaitAuth().pipe(
switchMap(() => this.restService.v2.GetEmote(this.route.snapshot.paramMap.get('emote') as string, true)),
catchError(err => throwError(this.restService.formatError(err))),
filter(res => res.emote !== null), // Initiate a new emote structure instance
tap(res => this.appService.pageTitleAttr.next([ // Update page title
{ name: 'EmoteName', value: res.emote?.name ?? '' },
{ name: 'OwnerName', value: `by ${res.emote?.owner?.display_name ?? ''}` }
])),
map(res => this.emote = this.dataService.add('emote', res.emote)[0]),
switchMap(emote => this.readChannels().pipe(mapTo(emote)))
)
: !!this.emote ? of(this.emote) : throwError(Error('Unknown Emote'));
// Look up requested emote from route uri
emoteGetter.pipe(
// Update meta
// Show this emote in discord etc!
switchMap(emote => emote.getURL(4).pipe(
tap(url => {
const appURL = this.document.location.host + this.router.serializeUrl(this.router.createUrlTree(['/emotes', String(emote.getID())]));
const emoteData = emote.getSnapshot();
this.metaService.addTags([
// { name: 'og:title', content: this.appService.pageTitle },
// { name: 'og:site_name', content: this.appService.pageTitle },
{ name: 'og:description', content: `uploaded by ${emoteData?.owner?.display_name}` },
{ name: 'og:image', content: url ?? '' },
{ name: 'og:image:type', content: emote.getSnapshot()?.mime ?? 'image/png' },
{ name: 'theme-color', content: this.themingService.primary.hex() }
]);
// Discord OEmbed
// TODO: Make this a proper service so it can be applied to other pages
if (AppComponent.isBrowser.getValue() !== true) {
const link = this.document.createElement('link');
link.setAttribute('type', 'application/json+oembed');
const query = new URLSearchParams();
query.append('object', Buffer.from(JSON.stringify({
title: this.appService.pageTitle ?? 'Unknown Page',
author_name: ''.concat(
`${emoteData?.name} by ${emoteData?.owner?.display_name ?? 'Unknown User'} `,
`${BitField.HasBits(emoteData?.visibility ?? 0, DataStructure.Emote.Visibility.GLOBAL) ? '(Global Emote)' : `(${emote.getSnapshot()?.channel_count ?? 0} Channels)`}`
),
author_url: `https://${appURL}`,
provider_name: `7TV.APP - It's like a third party thing`,
provider_url: 'https://7tv.app',
type: 'photo',
url
})).toString('base64'));
link.setAttribute('href', `https://${environment.origin}/services/oembed?` + query.toString());
this.document.head.appendChild(link);
}
return undefined;
})
)),
// Set emote sizes
switchMap(() => this.getSizes().pipe(
tap(result => this.sizes.next(result))
)),
// Add audit logs
switchMap(() => this.readAuditActivity().pipe(
tap(entries => this.audit.next(entries)),
catchError(err => of(undefined))
)),
tap(() => this.cdr.markForCheck()),
// Show warning dialog for hidden emote?
switchMap(() => (this.emote as EmoteStructure).hasVisibility('HIDDEN').pipe(
switchMap(isHidden => iif(() => !isHidden || this.disableNotices,
of(true),
new Observable<boolean>(observer => {
const dialogRef = this.dialog.open(EmoteWarningDialogComponent, {
maxWidth: 300,
disableClose: true
});
dialogRef.afterClosed().subscribe({
next(ok): void {
observer.next(ok);
observer.complete();
} // Send the result of the user either going ahead or going back
});
})
)),
tap(canShow => this.blurred.next(!canShow))
))
).subscribe({
error: (err: HttpErrorResponse) => {
this.dialog.open(ErrorDialogComponent, {
data: {
errorName: 'Cannot View Emote',
errorMessage: err.error?.error ?? err.error ?? err,
errorCode: String(err.status)
} as ErrorDialogComponent.Data
});
this.router.navigate(['/emotes'], {replaceUrl: true});
}
});
}
}
Example #10
Source File: emote-create.component.ts From App with MIT License | 4 votes |
@Component({
selector: 'app-emote-create',
templateUrl: './emote-create.component.html',
styleUrls: ['./emote-create.component.scss'],
animations: [
trigger('open', [
transition(':enter', [
animate(500, keyframes([
style({ opacity: 0, offset: 0 }),
style({ opacity: 0, offset: .75 }),
style({ opacity: 1, offset: 1 })
]))
])
])
],
changeDetection: ChangeDetectionStrategy.Default
})
export class EmoteCreateComponent implements OnInit {
constructor(
private loggerService: LoggerService,
private dialog: MatDialog,
private localStorageService: LocalStorageService,
public themingService: ThemingService,
public emoteFormService: EmoteFormService
) { }
get form(): FormGroup { return this.emoteFormService.form; }
get emoteControl(): FormControl { return this.emoteFormService.form.get('emote') as FormControl; }
draggingFile = new BehaviorSubject<boolean>(false);
tags = [] as string[];
tagInputSeparationKeys = [ENTER, SPACE, COMMA];
isUploaded(): Observable<boolean> {
return this.emoteFormService.emoteData.asObservable().pipe(
map(data => data === null ? false : true)
);
}
isUploading(): Observable<boolean> { return this.emoteFormService.uploading.asObservable(); }
ngOnInit(): void {
if (this.localStorageService.getItem('agree_tos') !== 'true') {
const dialogRef = this.dialog.open(TOSDialogComponent, {
disableClose: true
});
dialogRef.afterClosed().pipe(
filter(agree => agree === true)
).subscribe({
next: () => {
this.localStorageService.setItem('agree_tos', 'true');
}
});
}
}
addTag(ev: MatChipInputEvent): void {
if (ev.value.length < 3 || this.tags.length > 5) {
ev.chipInput?.clear();
return undefined;
}
this.tags.push(ev.value.toLowerCase());
ev.chipInput?.clear();
}
removeTag(tag: string): void {
const i = this.tags.indexOf(tag);
this.tags.splice(i, 1);
}
onDropFile(ev: DragEvent): void {
ev.preventDefault();
this.draggingFile.next(false);
this.uploadEmoteFile(ev.dataTransfer?.files[0] ?? null);
}
onDragOver(event: Event): void {
event.stopPropagation();
event.preventDefault();
this.draggingFile.pipe(
take(1),
filter(x => x === false),
delay(250),
tap(() => this.draggingFile.next(false))
).subscribe();
this.draggingFile.next(true);
}
getEventTargetFile(target: EventTarget | null): File | null {
return (target as any)?.files[0] ?? null;
}
uploadEmoteFile(file: File | null): void {
if (!(file instanceof File)) return this.loggerService.debug(`Canceled emote upload`), undefined;
const reader = new FileReader();
const control = this.form.get('emote') as FormControl;
if (file.size > 5000000) {
this.emoteFormService.uploadError.next('File must be under 5.0MB');
control.setErrors({ image_size_too_large: true });
return undefined;
} else {
this.emoteFormService.uploadError.next('');
control.setErrors({ image_size_too_large: false });
}
reader.onload = (e: ProgressEvent) => {
this.emoteFormService.uploadedEmote.next(String((e.target as { result?: string; }).result));
if (control) {
control.patchValue(file);
control.setErrors(null);
control.markAsDirty();
}
return undefined;
};
reader.readAsDataURL(file);
return undefined;
}
startUpload(): void {
if (this.tags.length > 0) {
this.emoteFormService.form.patchValue({ tags: this.tags.join(',').toLowerCase() });
}
this.emoteFormService.uploadEmote();
}
}
Example #11
Source File: emote-list.component.ts From App with MIT License | 4 votes |
@Component({
selector: 'app-emote-list',
templateUrl: './emote-list.component.html',
styleUrls: ['./emote-list.component.scss'],
styles: ['.selected-emote-card { opacity: 0 }'],
animations: [
trigger('fadeout', [
state('true', style({ transform: 'translateY(200%)', opacity: 0 })),
transition('* => true', animate('100ms'))
]),
trigger('emotes', [
transition('* => *', [
query('.is-emote-card:enter', [
style({ opacity: 0, transform: 'translateX(0.5em) translateY(-0.5em)', position: 'relative' }),
stagger(-4, [
animate('275ms ease-in-out', keyframes([
style({ opacity: 0, offset: 0.475 }),
style({ opacity: 1, transform: 'none', offset: 1 })
]))
])
], { optional: true }),
group([
query('.is-emote-card:not(.selected-emote-card):leave', [
style({ opacity: 1 }),
stagger(2, [
animate('100ms', style({ transform: 'scale(0)' }))
])
], { optional: true }),
query('.selected-emote-card', [
style({ opacity: 1 }),
animate('550ms', keyframes([
style({ offset: 0, opacity: 1, transform: 'scale(1)' }),
style({ offset: .2, transform: 'scale(.91)' }),
style({ offset: .38, transform: 'scale(.64)' }),
style({ offset: .44, transform: 'scale(.82)' }),
style({ offset: 1, transform: 'scale(12) translateY(25%)', opacity: 0 })
])),
], { optional: true })
])
])
])
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmoteListComponent implements AfterViewInit, OnDestroy {
destroyed = new Subject<any>().pipe(take(1)) as Subject<void>;
selecting = new BehaviorSubject(false).pipe(takeUntil(this.destroyed)) as BehaviorSubject<boolean>;
emotes = new BehaviorSubject<any>([]).pipe(takeUntil(this.destroyed)) as BehaviorSubject<EmoteStructure[]>;
newPage = new Subject();
loading = new Subject();
totalEmotes = new BehaviorSubject<number>(0);
resized = new Subject<[number, number]>();
@ViewChild('emotesContainer') emotesContainer: ElementRef<HTMLDivElement> | undefined;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator | undefined;
@ViewChild('contextMenu') contextMenu: ContextMenuComponent | undefined;
pageOptions: EmoteListComponent.PersistentPageOptions | undefined;
pageSize = new BehaviorSubject<number>(16);
currentSearchOptions: RestV2.GetEmotesOptions | undefined;
currentSearchQuery = '';
skipNextQueryChange = false;
firstPageEvent = true;
constructor(
private restService: RestService,
private renderer: Renderer2,
private router: Router,
private route: ActivatedRoute,
private appService: AppService,
private dataService: DataService,
public themingService: ThemingService
) { }
selectEmote(ev: MouseEvent, el: any, emote: EmoteStructure): void {
if (!emote.getID()) return undefined;
ev.preventDefault();
this.selecting.next(true);
this.renderer.addClass(el, 'selected-emote-card');
this.emotes.next([]);
setTimeout(() => {
this.router.navigate(['emotes', emote.getID()]);
}, 775);
}
private updateQueryParams(replaceUrl: boolean = false): void {
const merged = {
...this.currentSearchOptions,
...{ page: (this.pageOptions?.pageIndex ?? 0) }
};
this.skipNextQueryChange = true;
this.router.navigate(['.'], {
relativeTo: this.route,
queryParams: Object.keys(merged).map(k => ({ [k]: (merged as any)[k as any] })).reduce((a, b) => ({ ...a, ...b })),
queryParamsHandling: 'merge',
replaceUrl
});
}
handleSearchChange(change: Partial<RestV2.GetEmotesOptions>): void {
const queryString = Object.keys(change).map(k => `${k}=${change[k as keyof RestV2.GetEmotesOptions]}`).join('&');
this.appService.pushTitleAttributes({ name: 'SearchOptions', value: `- ${queryString}` });
this.currentSearchOptions = { ...this.currentSearchOptions, ...change as RestV2.GetEmotesOptions };
this.updateQueryParams();
this.goToFirstPage();
this.getEmotes(undefined, this.currentSearchOptions).pipe(
delay(50),
tap(emotes => this.emotes.next(emotes))
).subscribe();
}
getEmotes(page = 0, options?: Partial<RestV2.GetEmotesOptions>): Observable<EmoteStructure[]> {
this.emotes.next([]);
this.newPage.next(page);
const timeout = setTimeout(() => this.loading.next(true), 1000);
const cancelSpinner = () => {
this.loading.next(false);
clearTimeout(timeout);
};
const size = this.calculateSizedRows();
return this.restService.awaitAuth().pipe(
switchMap(() => this.restService.v2.SearchEmotes(
(this.pageOptions?.pageIndex ?? 0) + 1,
Math.max(EmoteListComponent.MINIMUM_EMOTES, size ?? EmoteListComponent.MINIMUM_EMOTES),
options ?? this.currentSearchOptions
)),
takeUntil(this.newPage.pipe(take(1))),
tap(res => this.totalEmotes.next(res?.total_estimated_size ?? 0)),
delay(200),
map(res => res?.emotes ?? []),
mergeAll(),
map(data => this.dataService.add('emote', data)[0]),
toArray(),
defaultIfEmpty([] as EmoteStructure[]),
tap(() => cancelSpinner()),
catchError(() => defer(() => cancelSpinner()))
) as Observable<EmoteStructure[]>;
}
onOpenCardContextMenu(emote: EmoteStructure): void {
if (!this.contextMenu) {
return undefined;
}
this.contextMenu.contextEmote = emote;
emote.getOwner().pipe(
tap(usr => !!this.contextMenu ? this.contextMenu.contextUser = (usr ?? null) : noop())
).subscribe();
}
goToFirstPage(): void {
this.paginator?.page.next({
pageIndex: 0,
pageSize: this.pageOptions?.pageSize ?? 0,
length: this.pageOptions?.length ?? 0,
});
}
/**
* Handle pagination changes
*/
onPageEvent(ev: PageEvent): void {
this.pageOptions = {
...ev
};
if (this.firstPageEvent) {
this.updateQueryParams(true);
this.firstPageEvent = false;
}
else {
this.updateQueryParams();
}
// Save PageIndex title attr
this.appService.pushTitleAttributes({ name: 'PageIndex', value: `- ${ev.pageIndex}/${Number((ev.length / ev.pageSize).toFixed(0))}` });
// Fetch new set of emotes
this.getEmotes(ev.pageIndex).pipe(
tap(emotes => this.emotes.next(emotes))
).subscribe();
}
isEmpty(): Observable<boolean> {
return this.totalEmotes.pipe(
take(1),
map(size => size === 0)
);
}
/**
* Calculate how many rows and columns according to the container's size
*
* @returns the result of rows * columns
*/
calculateSizedRows(): number | null {
if (!this.emotesContainer) {
return null;
}
const marginBuffer = 28; // The margin _in pixels between each card
const cardSize = 137; // The size of the cards in pixels
const width = this.emotesContainer.nativeElement.scrollWidth - 32; // The width of emotes container
const height = this.emotesContainer.nativeElement.clientHeight - 16; // The height of the emotes container
const rows = Math.floor((width / (cardSize + marginBuffer))); // The calculated amount of rows
const columns = Math.floor(height / (cardSize + marginBuffer)); // The calculated amount of columns
// Return the result of rows multiplied by columns
return rows * columns;
}
@HostListener('window:resize', ['$event'])
onWindowResize(ev: Event): void {
const size = this.calculateSizedRows();
this.pageSize.next(size ?? 0);
this.resized.next([0, 0]);
timer(1000).pipe(
takeUntil(this.resized.pipe(take(1))),
switchMap(() => this.getEmotes(this.pageOptions?.pageIndex, {})),
tap(emotes => this.emotes.next(emotes))
).subscribe();
}
ngAfterViewInit(): void {
// Get persisted page options?
this.route.queryParamMap.pipe(
defaultIfEmpty({} as ParamMap),
map(params => {
return {
page: params.has('page') ? Number(params.get('page')) : 0,
search: {
sortBy: params.get('sortBy') ?? 'popularity',
sortOrder: params.get('sortOrder'),
globalState: params.get('globalState'),
query: params.get('query'),
submitter: params.get('submitter'),
channel: params.get('channel')
}
};
}),
tap(() => setTimeout(() => this.skipNextQueryChange = false, 0)),
filter(() => !this.skipNextQueryChange)
).subscribe({
next: opt => {
const d = {
pageIndex: !isNaN(opt.page) ? opt.page : 0,
pageSize: Math.max(EmoteListComponent.MINIMUM_EMOTES, this.calculateSizedRows() ?? 0),
length: 0,
};
this.updateQueryParams(true);
this.currentSearchOptions = opt.search as any;
this.paginator?.page.next(d);
this.pageOptions = d;
}
});
this.pageSize.next(this.calculateSizedRows() ?? 0);
}
ngOnDestroy(): void {
this.loading.complete();
}
}