...
 
Commits (2)
......@@ -21,7 +21,6 @@ context('Pro Settings', () => {
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
},
logoGuid: '#logo_guid',
}
const hashtags = {
......
......@@ -85,7 +85,6 @@ export interface MindsUser {
pro_published?: boolean;
pro_settings?: {
logo_image: string;
logo_guid: string;
tag_list?: Tag[];
background_image: string;
title: string;
......@@ -97,6 +96,8 @@ export interface MindsUser {
featured_content?: Array<string>;
tile_ratio?: string;
styles?: { [key: string]: string };
has_custom_logo?: boolean;
has_custom_background?: boolean;
};
mode: ChannelMode;
}
......
<div class="m-pro__channel">
<ng-container *ngIf="channel">
<div class="m-proChannel__topbar">
<ng-container *ngIf="!channel.pro_settings.logo_guid; else customLogo">
<ng-container
*ngIf="!channel.pro_settings.has_custom_logo; else customLogo"
>
<minds-avatar
[object]="channel"
[routerLink]="homeRouterLink"
......
import { Injectable } from '@angular/core';
import { Client } from '../../services/api/client';
import { Upload } from '../../services/api/upload';
@Injectable()
export class ProService {
public readonly ratios = ['16:9', '16:10', '4:3', '1:1'];
constructor(protected client: Client) {}
constructor(protected client: Client, protected uploadClient: Upload) {}
async isActive(): Promise<boolean> {
const result: any = await this.client.get('api/v2/pro');
......@@ -66,4 +67,22 @@ export class ProService {
await this.client.post(endpoint.join('/'), settings);
return true;
}
async upload(type: string, file, remoteUser: string | null = null) {
const endpoint = ['api/v2/pro/settings/assets', type];
if (remoteUser) {
endpoint.push(remoteUser);
}
const response = (await this.uploadClient.post(endpoint.join('/'), [
file,
])) as any;
if (!response || response.status !== 'success') {
throw new Error(response.message || 'Invalid server response');
}
return true;
}
}
......@@ -24,6 +24,17 @@
</m-tooltip>
</a>
<a
class="m-topbar--navigation--item"
[class.m-topbar--navigation--item-active]="currentTab === 'assets'"
(click)="currentTab = 'assets'"
>
<span i18n>Assets</span>
<m-tooltip icon="help" i18n>
Upload a custom logo and background.
</m-tooltip>
</a>
<a
class="m-topbar--navigation--item"
[class.m-topbar--navigation--item-active]="currentTab === 'hashtags'"
......@@ -207,15 +218,72 @@
</label>
</ng-container>
</div>
</ng-template>
<!-- Assets -->
<ng-template ngSwitchCase="assets">
<p class="m-proSettings__note" i18n>
Upload a custom logo and background.
</p>
<div class="m-proSettings__field">
<label for="logo_guid" i18n>Logo Asset GUID</label>
<input
type="text"
id="logo_guid"
name="logo_guid"
[(ngModel)]="settings.logo_guid"
/>
<label for="logo" i18n>Logo Image</label>
<label
for="logo"
class="m-proSettingsField__filePreview m-proSettingsField__logoFilePreview"
[ngStyle]="{
backgroundColor: settings.plain_background_color
}"
>
<input
type="file"
id="logo"
name="logo"
accept="image/*"
(change)="onAssetFileSelect('logo', logoField.files)"
#logoField
/>
<img
*ngIf="getPreviewAssetSrc('logo')"
[src]="getPreviewAssetSrc('logo')"
/>
<span class="m-proSettingsFieldFilePreview__overlay">
<i class="material-icons">cloud_upload</i>
</span>
</label>
</div>
<div class="m-proSettings__field">
<label for="background" i18n>Background</label>
<label
for="background"
class="m-proSettingsField__filePreview m-proSettingsField__backgroundFilePreview"
>
<input
type="file"
id="background"
name="background"
accept="image/*"
(change)="
onAssetFileSelect('background', backgroundField.files)
"
#backgroundField
/>
<img
*ngIf="getPreviewAssetSrc('background')"
[src]="getPreviewAssetSrc('background')"
/>
<span class="m-proSettingsFieldFilePreview__overlay">
<i class="material-icons">cloud_upload</i>
</span>
</label>
</div>
</ng-template>
......@@ -381,8 +449,8 @@
</ng-template>
</ng-container>
<div class="m-proSettings__error" *ngIf="!!error">
{{ error }}
<div class="m-proSettings__error" *ngIf="error">
Error: {{ error }}
</div>
<div
......
......@@ -92,6 +92,68 @@
margin-left: auto;
}
}
.m-proSettingsField__filePreview {
position: relative;
overflow: hidden;
display: inline-block;
border-radius: 4px;
cursor: pointer;
margin: 0;
&.m-proSettingsField__logoFilePreview {
padding: 16px;
> img {
max-width: 100%;
max-height: 100px;
object-fit: contain;
}
}
&.m-proSettingsField__backgroundFilePreview {
> img {
width: 480px;
height: 270px;
object-fit: cover;
}
}
input[type='file'] {
position: absolute;
-webkit-appearance: none;
width: 0.1px;
height: 0.1px;
z-index: -1;
opacity: 0.01;
top: -1px;
left: -1px;
}
.m-proSettingsFieldFilePreview__overlay {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5;
background: rgba(0, 0, 0, 0.4);
> i.material-icons {
font-size: 3em;
color: #ffffff;
}
}
&:hover {
.m-proSettingsFieldFilePreview__overlay {
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.m-proSettings__previewBtn {
......@@ -134,10 +196,16 @@
}
.m-proSettings__error {
margin: 16px 0;
font-size: 20px;
margin: 0 0 16px 0;
padding: 8px;
border-radius: 8px;
border: 1px solid;
font-size: 14px;
line-height: 1;
@include m-theme() {
color: themed($m-red);
border-color: themed($m-red);
}
}
}
......@@ -2,8 +2,10 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { ProService } from '../pro.service';
import { Session } from '../../../services/session';
......@@ -11,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { MindsTitle } from '../../../services/ux/title';
import { Subscription } from 'rxjs';
import { SiteService } from '../../../common/services/site.service';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
@Component({
selector: 'm-pro--settings',
......@@ -27,6 +30,7 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
currentTab:
| 'general'
| 'theme'
| 'assets'
| 'hashtags'
| 'footer'
| 'domain'
......@@ -38,6 +42,12 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
protected param$: Subscription;
@ViewChild('logoField', { static: false })
protected logoField: ElementRef<HTMLInputElement>;
@ViewChild('backgroundField', { static: false })
protected backgroundField: ElementRef<HTMLInputElement>;
constructor(
protected service: ProService,
protected session: Session,
......@@ -45,7 +55,8 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
protected route: ActivatedRoute,
protected cd: ChangeDetectorRef,
protected title: MindsTitle,
protected site: SiteService
protected site: SiteService,
protected sanitizer: DomSanitizer
) {}
ngOnInit() {
......@@ -81,12 +92,74 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.detectChanges();
}
onAssetFileSelect(type: string, files: FileList | null) {
if (!files || !files.item(0)) {
this.settings[type] = null;
this.detectChanges();
return;
}
this.settings[type] = files.item(0);
this.detectChanges();
}
protected async uploadAsset(
type: string,
file: File,
htmlInputFileElementRef: ElementRef<HTMLInputElement> | null = null
): Promise<void> {
await this.service.upload(type, file, this.user);
if (htmlInputFileElementRef && htmlInputFileElementRef.nativeElement) {
try {
htmlInputFileElementRef.nativeElement.value = '';
} catch (e) {
console.warn(`Browser prevented ${type} field resetting`);
}
}
}
getPreviewAssetSrc(type: string): string | SafeUrl {
if (this.settings[type]) {
if (!this.settings[type]._mindsBlobUrl) {
this.settings[type]._mindsBlobUrl = URL.createObjectURL(this.settings[
type
] as File);
}
return this.sanitizer.bypassSecurityTrustUrl(
this.settings[type]._mindsBlobUrl
);
}
return this.settings[`${type}_image`];
}
async save() {
this.error = null;
this.inProgress = true;
this.detectChanges();
try {
const { logo, background, ...settings } = this.settings;
const uploads: Promise<any>[] = [];
if (logo) {
uploads.push(this.uploadAsset('logo', logo, this.logoField));
settings.has_custom_logo = true;
}
if (background) {
uploads.push(
this.uploadAsset('background', background, this.backgroundField)
);
settings.has_custom_background = true;
}
await Promise.all(uploads);
this.settings = settings;
await this.service.set(this.settings, this.user);
} catch (e) {
this.error = e.message;
......