#327413 Add image picker for editable images.

This commit is contained in:
Georgi Prodanov 2017-01-17 18:30:50 +02:00
Родитель 4f903f86f7
Коммит 9d494d4b12
25 изменённых файлов: 301 добавлений и 56 удалений

Просмотреть файл

@ -11,13 +11,15 @@ import {
EventsService, EventsService,
EventRegistrationsService, EventRegistrationsService,
AlertService, AlertService,
GroupsService GroupsService,
ImagePickerService,
FilesService
} from './services'; } from './services';
@Component({ @Component({
selector: 'my-app', selector: 'my-app',
templateUrl: 'app.component.html', templateUrl: 'app.component.html',
providers: [EverliveProvider, UsersService, EventsService, EventRegistrationsService, AlertService, GroupsService] providers: [EverliveProvider, UsersService, EventsService, EventRegistrationsService, AlertService, GroupsService, ImagePickerService, FilesService]
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
loggedIn: boolean = false; loggedIn: boolean = false;

Просмотреть файл

@ -3,7 +3,8 @@ import { RouterExtensions } from 'nativescript-angular/router';
import { EventCreationModalComponent } from '../event-creation-modal/event-creation-modal.component'; import { EventCreationModalComponent } from '../event-creation-modal/event-creation-modal.component';
import { Event, Group } from '../../shared/models'; import { Event, Group } from '../../shared/models';
import { EventsService, AlertService, UsersService, GroupsService } from '../../services'; import { EventsService, AlertService, UsersService, GroupsService, FilesService } from '../../services';
import { utilities } from '../../shared';
@Component({ @Component({
selector: 'add-event', selector: 'add-event',
@ -16,6 +17,7 @@ export class AddEventComponent {
constructor( constructor(
private _routerExtensions: RouterExtensions, private _routerExtensions: RouterExtensions,
private _groupsService: GroupsService, private _groupsService: GroupsService,
private _filesService: FilesService,
private _eventService: EventsService, private _eventService: EventsService,
private _alertService: AlertService, private _alertService: AlertService,
private _usersService: UsersService, private _usersService: UsersService,
@ -35,6 +37,20 @@ export class AddEventComponent {
} }
}) })
.then(() => { .then(() => {
// TODO: move to service
let prm = Promise.resolve<{ Id: string, Uri: string }>();
if (utilities.isLocalUrl(this.newEvent.ImageUrl)) {
// this has been turned into local uri by the picker component
prm = this._filesService.uploadFromUri(this.newEvent.ImageUrl);
}
return prm;
})
.then((uploadResult) => {
if (uploadResult) {
this.newEvent.Image = uploadResult.Id;
}
let creationPromise = this._eventService.create(this.newEvent); let creationPromise = this._eventService.create(this.newEvent);
let groupPromise = this._groupsService.getById(this.newEvent.GroupId); let groupPromise = this._groupsService.getById(this.newEvent.GroupId);
return Promise.all<any>([groupPromise, creationPromise]);; return Promise.all<any>([groupPromise, creationPromise]);;

Просмотреть файл

@ -2,8 +2,9 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { RouterExtensions } from 'nativescript-angular/router'; import { RouterExtensions } from 'nativescript-angular/router';
import { EventsService, AlertService } from '../../services'; import { EventsService, AlertService, FilesService } from '../../services';
import { Event, User } from '../../shared/models'; import { Event, User } from '../../shared/models';
import { utilities } from '../../shared';
@Component({ @Component({
selector: 'edit-event', selector: 'edit-event',
@ -17,6 +18,7 @@ export class EditEventComponent implements OnInit {
private _route: ActivatedRoute, private _route: ActivatedRoute,
private _alertsService: AlertService, private _alertsService: AlertService,
private _routerExtensions: RouterExtensions, private _routerExtensions: RouterExtensions,
private _filesService: FilesService,
private _eventsService: EventsService private _eventsService: EventsService
) {} ) {}
@ -39,6 +41,20 @@ export class EditEventComponent implements OnInit {
this._alertsService.askConfirmation('Save all changes?') this._alertsService.askConfirmation('Save all changes?')
.then(() => { .then(() => {
// TODO: move to service
let prm = Promise.resolve<{ Id: string, Uri: string }>();
if (utilities.isLocalUrl(this.event.ImageUrl)) {
// this has been turned into local uri by the picker component
prm = this._filesService.uploadFromUri(this.event.ImageUrl);
}
return prm;
})
.then((uploadResult) => {
if (uploadResult) {
this.event.Image = uploadResult.Id;
}
return this._eventsService.update(this.event); return this._eventsService.update(this.event);
}) })
.then(() => { .then(() => {
@ -51,7 +67,7 @@ export class EditEventComponent implements OnInit {
if (err) { if (err) {
this._alertsService.showError(err.message); this._alertsService.showError(err.message);
} }
});; });
} }
delete() { delete() {

Просмотреть файл

@ -1,5 +1,5 @@
<StackLayout class="cntnr"> <StackLayout class="cntnr">
<photo-picker [url]="event.ImageUrl" [type]="'event'" [editable]="true"></photo-picker> <photo-picker [(url)]="event.ImageUrl" [type]="'event'" [editable]="true"></photo-picker>
<StackLayout class="form info-cntnr"> <StackLayout class="form info-cntnr">
<StackLayout class="input-field"> <StackLayout class="input-field">

Просмотреть файл

@ -12,13 +12,13 @@
border-width: 0; border-width: 0;
border-bottom-width: 1; border-bottom-width: 1;
border-color: #ccc; border-color: #ccc;
margin: 0 20;
} }
.event-name, .event-name,
.event-date-wrp, .event-date-wrp,
.event-description, .event-description,
.event-organiser, .event-organiser {
.event-list-separator {
margin: 10 20; margin: 10 20;
} }

Просмотреть файл

@ -28,7 +28,7 @@ export class EventsComponent implements OnInit {
) { } ) { }
onAdd() { onAdd() {
this._routerExtensions.navigate(['/events/add']); this._routerExtensions.navigateByUrl('/events/add');
} }
ngOnInit() { ngOnInit() {

Просмотреть файл

@ -3,7 +3,7 @@ import { ActivatedRoute } from '@angular/router';
import { RouterExtensions } from 'nativescript-angular/router'; import { RouterExtensions } from 'nativescript-angular/router';
import { GroupCreationModalComponent } from '../group-creation-modal/group-creation-modal.component'; import { GroupCreationModalComponent } from '../group-creation-modal/group-creation-modal.component';
import { GroupsService, AlertService } from '../../services'; import { GroupsService, AlertService, FilesService } from '../../services';
import { Group } from '../../shared/models'; import { Group } from '../../shared/models';
import { utilities } from '../../shared'; import { utilities } from '../../shared';
@ -17,6 +17,7 @@ export class AddGroupComponent {
constructor( constructor(
private _routerExtensions: RouterExtensions, private _routerExtensions: RouterExtensions,
private _filesService: FilesService,
private _groupsService: GroupsService, private _groupsService: GroupsService,
private _alertService: AlertService, private _alertService: AlertService,
private _vsRef: ViewContainerRef private _vsRef: ViewContainerRef
@ -30,7 +31,18 @@ export class AddGroupComponent {
return this._alertService.showError(errMsg); return this._alertService.showError(errMsg);
} }
let createdId: string; let createdId: string;
this._groupsService.create(this.group) let promise: Promise<{ Id: string, Uri: string }> = Promise.resolve();
if (utilities.isLocalUrl(this.group.ImageUrl)) {
promise = this._filesService.uploadFromUri(this.group.ImageUrl);
}
promise.then((uploadResult) => {
if (uploadResult) {
this.group.Image = uploadResult.Id;
}
return this._groupsService.create(this.group);
})
.then((result) => { .then((result) => {
createdId = result.Id; createdId = result.Id;
let ctx = { groupName: this.group.Name }; let ctx = { groupName: this.group.Name };
@ -40,7 +52,6 @@ export class AddGroupComponent {
if (doInviteMembers) { if (doInviteMembers) {
return this._alertService.showError('Not implemented, yet'); return this._alertService.showError('Not implemented, yet');
} }
}) })
.then(() => { .then(() => {
this._routerExtensions.navigateByUrl(`/groups/${createdId}`); this._routerExtensions.navigateByUrl(`/groups/${createdId}`);

Просмотреть файл

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { RouterExtensions } from 'nativescript-angular/router'; import { RouterExtensions } from 'nativescript-angular/router';
import { GroupsService, AlertService } from '../../services'; import { GroupsService, AlertService, FilesService } from '../../services';
import { Group } from '../../shared/models'; import { Group } from '../../shared/models';
import { utilities } from '../../shared'; import { utilities } from '../../shared';
@ -17,6 +17,7 @@ export class EditGroupComponent implements OnInit {
constructor( constructor(
private _groupsService: GroupsService, private _groupsService: GroupsService,
private _alertsService: AlertService, private _alertsService: AlertService,
private _filesService: FilesService,
private _routerExtensions: RouterExtensions, private _routerExtensions: RouterExtensions,
private _activatedRoute: ActivatedRoute private _activatedRoute: ActivatedRoute
) { } ) { }
@ -40,6 +41,18 @@ export class EditGroupComponent implements OnInit {
this._alertsService.askConfirmation(`Update all fields of "${this.group.Name}"?`) this._alertsService.askConfirmation(`Update all fields of "${this.group.Name}"?`)
.then(() => { .then(() => {
let prm = Promise.resolve<{ Id: string, Uri: string }>();
if (utilities.isLocalUrl(this.group.ImageUrl)) {
prm = this._filesService.uploadFromUri(this.group.ImageUrl);
}
return prm;
})
.then((uploadResult) => {
if (uploadResult) {
this.group.Image = uploadResult.Id;
}
return this._groupsService.update(this.group); return this._groupsService.update(this.group);
}) })
.then(() => { .then(() => {

Просмотреть файл

@ -17,7 +17,7 @@ TextView {
color: #888; color: #888;
} }
.user-image { .user >>> Image {
width: 30; width: 30;
height: 30; height: 30;
margin-right: 10; margin-right: 10;

Просмотреть файл

@ -51,14 +51,6 @@ export class EditableGroupComponent implements OnInit {
}, err => err); // ignore rejection (when user clicks back and closes)); }, err => err); // ignore rejection (when user clicks back and closes));
} }
getUserResizedUrl(rawUrl: string) {
return this._getResizedImageUrl(rawUrl, { width: 60, height: 60 });
}
getGroupResizedUrl(rawUrl: string) {
return this._getResizedImageUrl(rawUrl);
}
private _openGroupTypeModal() { private _openGroupTypeModal() {
let ctx = { let ctx = {
items: this._groupTypeOptions, items: this._groupTypeOptions,

Просмотреть файл

@ -1,5 +1,5 @@
<StackLayout class="cntnr"> <StackLayout class="cntnr">
<Image src="{{getGroupResizedUrl(group.ImageUrl)}}"></Image> <photo-picker [(url)]="group.ImageUrl" [editable]="true" [type]="'group'"></photo-picker>
<StackLayout class="input-field"> <StackLayout class="input-field">
<Label class="label" text="Name"></Label> <Label class="label" text="Name"></Label>
@ -24,7 +24,7 @@
<StackLayout *ngIf="currentUser" class="input-field user"> <StackLayout *ngIf="currentUser" class="input-field user">
<Label class="label" text="Administrator"></Label> <Label class="label" text="Administrator"></Label>
<StackLayout orientation="horizontal"> <StackLayout orientation="horizontal">
<Image class="user-image" src="{{getUserResizedUrl(currentUser.ImageUrl)}}"></Image> <photo-picker [url]="currentUser.ImageUrl" [type]="'user'" [small]="true"></photo-picker>
<Label class="user-label" [text]="currentUser.DisplayName || currentUser.Username"></Label> <Label class="user-label" [text]="currentUser.DisplayName || currentUser.Username"></Label>
</StackLayout> </StackLayout>
</StackLayout> </StackLayout>

Просмотреть файл

@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { EverliveProvider } from './everlive-provider.service';
import { ImagePickerService } from './image-picker.service';
@Injectable()
export class FilesService {
constructor(
private _elProvider: EverliveProvider,
private _imagesService: ImagePickerService
) {}
upload(base64File: string, filename?: string) {
let data = {
Filename: filename || `teamupimg${Date.now()}.png`,
ContentType: 'image/png',
base64: base64File
};
return this._elProvider.get.files.create(data)
.then((res: any) => {
let result = res.result;
return {
Id: result.Id,
Uri: result.Uri
};
});
}
uploadFromUri(uri: string) {
let base64 = this._imagesService.getBase64FromUri(uri);
return this.upload(base64);
}
}

Просмотреть файл

@ -39,6 +39,7 @@ export class GroupsService {
} }
create(group: Group) { create(group: Group) {
group = this._sanitizeGroup(group);
return this._groupsData.create(group).then(res => res.result); return this._groupsData.create(group).then(res => res.result);
} }
@ -138,7 +139,7 @@ export class GroupsService {
} }
update(group: Group) { update(group: Group) {
delete (<any>group).ImageUrl; // sanitize expanded field group = this._sanitizeGroup(group);
return this._groupsData.updateSingle(group).then(r => r.result); return this._groupsData.updateSingle(group).then(r => r.result);
} }
@ -196,4 +197,9 @@ export class GroupsService {
query.expand(this._imageExpandExp); query.expand(this._imageExpandExp);
return this._groupsData.get(query).then(res => res.result); return this._groupsData.get(query).then(res => res.result);
} }
private _sanitizeGroup(group: Group) {
delete group.ImageUrl;
return group;
}
} }

Просмотреть файл

@ -0,0 +1,112 @@
import { Injectable } from '@angular/core';
import * as nsPermissions from 'nativescript-permissions';
import * as nsPicker from 'nativescript-imagepicker';
import * as nsImgSource from 'image-source';
import * as nsPlatform from 'platform';
import * as fs from 'file-system';
import { EverliveProvider } from './';
declare const android: any;
@Injectable()
export class ImagePickerService {
private _imageItems: any[] = [];
constructor(
private _elProvider: EverliveProvider
) {}
pickImage() {
return this._pick('single')
.then(images => images[0]);
}
pickImages() {
return this._pick('multiple');
}
getBase64FromUri(uri: string) {
let imgSrc = nsImgSource.fromFileOrResource(uri);
return imgSrc.toBase64String('png');
}
private _pick(mode: string): Promise<{ name: string, uri: string }[]> {
let ctx = nsPicker.create({ mode });
let authPromise = Promise.resolve<any>();
if (nsPlatform.device.os === 'Android' && (+nsPlatform.device.sdkVersion >= 23)) {
let text = 'We need these permissions to read from storage';
let permType = android.Manifest.permission.READ_EXTERNAL_STORAGE;
authPromise = nsPermissions.requestPermission(permType, text);
}
return authPromise.then(() => this._presentPicker(ctx))
.then((res) => {
return this._processImages(res);
}, (err) => {
console.log('user cancelled or refused to grant permissions');
return Promise.reject(false);
});
}
private _presentPicker(context: any) {
return context.authorize()
.then(() => {
return context.present();
});
}
private _processImages(selection: any[]): Promise<{ name: string, uri: string }[]> {
let processedImgs: Promise<any>[] = selection.map((selectedItem) => {
return selectedItem.getImage().then(imgSource => {
return this._processImage(imgSource);
});
});
return Promise.all(processedImgs);
}
private _processImage(imageSource: any): Promise<{ name: string, uri: string }> {
let name = `teamupimg${Date.now()}.png`;
let folder = fs.knownFolders.documents();
let uri = fs.path.join(folder.path, name);
let saved = imageSource.saveToFile(uri, 'png');
let imgItem = null;
if (saved) {
imgItem = { name, uri };
} else {
console.log('didnt save!!!');
}
return imgItem;
}
// private _sendImage(name, uri) {
// let base64;
// try {
// let imgSrc = nsImgSource.fromFileOrResource(uri);
// base64 = imgSrc.toBase64String('png');
// } catch (ex) {
// console.log('err parsing as base64: ' + JSON.stringify(ex.message));
// return Promise.reject({ message: 'Could not read image' });
// }
// return Promise.resolve();
// }
// private _sendImages(imgData: { name: any, uri: any }[]) {
// let sendPromises: Promise<any>[] = [];
// imgData.forEach(data => {
// if (data) {
// console.log(`id: ${JSON.stringify(data)}`);
// sendPromises.push(this._sendImage(data.name, data.uri));
// }
// });
// return Promise.all(sendPromises);
// }
}

Просмотреть файл

@ -1,6 +1,9 @@
export * from './event-registrations.service';
export * from './event-registrations.service';
export * from './everlive-provider.service'; export * from './everlive-provider.service';
export * from './files.service';
export * from './users.service'; export * from './users.service';
export * from './events.service'; export * from './events.service';
export * from './event-registrations.service';
export * from './alert.service'; export * from './alert.service';
export * from './groups.service'; export * from './groups.service';
export * from './image-picker.service';

Просмотреть файл

@ -82,6 +82,6 @@ export class UsersService {
if (!user) { if (!user) {
return null; return null;
} }
return new User(user.Id, user.Username, user.DisplayName, user.Email, user.ImageUrl, user.Phone); return new User(user.Id, user.Username, user.DisplayName, user.Email, user.ImageUrl, user.Phone, user.Image);
} }
} }

Просмотреть файл

@ -2,7 +2,8 @@ import { ItemModel } from './item.model';
export class Group extends ItemModel { export class Group extends ItemModel {
Name: string; Name: string;
Image?: string; Image: string;
ImageUrl: string;
RequiresApproval: boolean; RequiresApproval: boolean;
IsPublic: boolean; IsPublic: boolean;
Description: string; Description: string;

Просмотреть файл

@ -8,7 +8,8 @@ export class User extends ItemModel {
public DisplayName: string, public DisplayName: string,
public Email: string, public Email: string,
public ImageUrl: string, public ImageUrl: string,
public Phone: string public Phone: string,
public Image: string
) { ) {
super(); super();
} }

Просмотреть файл

@ -0,0 +1,3 @@
.container.large {
width: 100%;
}

Просмотреть файл

@ -1,5 +1,8 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit, Output, EventEmitter, ViewChild } from '@angular/core';
import * as nsImgSource from 'image-source';
import * as nsImage from 'ui/image';
import { ImagePickerService } from '../../services';
import { utilities, constants } from '../'; import { utilities, constants } from '../';
@Component({ @Component({
@ -15,6 +18,12 @@ export class PhotoPickerComponent implements OnInit {
@Input('small') isSmall: boolean; @Input('small') isSmall: boolean;
@Input() editable: boolean; @Input() editable: boolean;
@Output('urlChange') onUpload = new EventEmitter<any>();
constructor(
private _imgPickerService: ImagePickerService
) {}
ngOnInit() { ngOnInit() {
if (!this.rawUrl) { if (!this.rawUrl) {
this.rawUrl = this._decidePlaceholder(); this.rawUrl = this._decidePlaceholder();
@ -25,7 +34,18 @@ export class PhotoPickerComponent implements OnInit {
onEdit(event) { onEdit(event) {
if (this.editable) { if (this.editable) {
console.log('picking...'); this._imgPickerService.pickImage()
.then(obj => {
console.log('picked:' + JSON.stringify(obj));
// this.onUpload.emit(obj);
// this.editableImg.imageSource = nsImgSource.fromFileOrResource(obj.uri);
this.resizedUrl = obj.uri;
this.onUpload.emit(obj.uri);
})
.catch(err => {
console.log('pick err: ' + JSON.stringify(err));
});
} else { } else {
console.log('editable is false...'); console.log('editable is false...');
} }

Просмотреть файл

@ -1,4 +1,6 @@
<StackLayout *ngIf="resizedUrl"> <AbsoluteLayout *ngIf="resizedUrl">
<Image *ngIf="editable" [src]="resizedUrl" (tap)="onEdit()"></Image> <StackLayout class="container" [ngClass]="{ 'large': !isSmall }">
<Image *ngIf="!editable" [src]="resizedUrl"></Image> <Image class="image" *ngIf="editable" [src]="resizedUrl" (tap)="onEdit()"></Image>
<Image class="image" *ngIf="!editable" [src]="resizedUrl"></Image>
</StackLayout> </StackLayout>
</AbsoluteLayout>

Просмотреть файл

@ -33,3 +33,8 @@ export function findIndex<T> (arr: T[], predicate: (item: T) => any): number {
return -1; return -1;
}; };
export function isLocalUrl (url: string) {
let regExp = new RegExp('^\/.*', 'i');
return url && regExp.test(url);
};

Просмотреть файл

@ -36,4 +36,9 @@ export class EditUserComponent implements OnInit {
.then(() => this._routerExtensions.navigateByUrl('user')) .then(() => this._routerExtensions.navigateByUrl('user'))
.catch(err => err && this._alertsService.showError(err)); .catch(err => err && this._alertsService.showError(err));
} }
onImageUpload(createdImage: { Uri: string, Id: string }) {
this.user.Image = createdImage.Id;
this._usersService.updateUser(this.user);
}
} }

Просмотреть файл

@ -12,7 +12,7 @@
<ScrollView> <ScrollView>
<StackLayout *ngIf="user" class="user-info-cnt"> <StackLayout *ngIf="user" class="user-info-cnt">
<photo-picker [url]="user.ImageUrl" [type]="'user'" [editable]="true"></photo-picker> <photo-picker [(url)]="user.ImageUrl" [type]="'user'" [editable]="true" (uplodad)="onImageUpload($event)"></photo-picker>
<StackLayout class="user-info-block"> <StackLayout class="user-info-block">
<Label class="user-label" text="Name"></Label> <Label class="user-label" text="Name"></Label>

Просмотреть файл

@ -24,7 +24,9 @@
"tns-core-modules": "2.4.4", "tns-core-modules": "2.4.4",
"rxjs": "5.0.0-beta.12", "rxjs": "5.0.0-beta.12",
"everlive-sdk": "1.9.1", "everlive-sdk": "1.9.1",
"nativescript-theme-core": "^0.1.3" "nativescript-theme-core": "^0.1.3",
"nativescript-imagepicker": "^2.4.1",
"nativescript-permissions": "^1.2.0"
}, },
"devDependencies": { "devDependencies": {
"babel-traverse": "6.18.0", "babel-traverse": "6.18.0",