Merge pull request #97 from NicolasConstant/develop

0.8.0 merge
This commit is contained in:
Nicolas Constant 2019-05-17 22:40:25 -04:00 committed by GitHub
commit 700c9c890f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 463 additions and 39 deletions

View File

@ -28,7 +28,7 @@
<div class="header__download-box--description">
A FLOSS multi-account Mastodon and Pleroma desktop client<br />
Now available in Beta (v0.7.0)<br />
Now available in Beta (v0.8.0)<br />
<br />
</div>
@ -43,9 +43,9 @@
<br />
<h4 class="header__download-box--subtitle">Or download the desktop client:</h4>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-win.exe" class="download-button" title="download client for windows"><i class="fab fa-windows"></i></a>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-mac.dmg" class="download-button" title="download client for mac"><i class="fab fa-apple"></i></a>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.7.0/Sengi-0.7.0-linux.deb" class="download-button" title="download client for debian-based distrib"><i class="fab fa-ubuntu"></i></a>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.8.0/Sengi-0.8.0-win.exe" class="download-button" title="download client for windows"><i class="fab fa-windows"></i></a>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.8.0/Sengi-0.8.0-mac.dmg" class="download-button" title="download client for mac"><i class="fab fa-apple"></i></a>
<a href="https://github.com/NicolasConstant/sengi/releases/download/0.8.0/Sengi-0.8.0-linux.deb" class="download-button" title="download client for debian-based distrib"><i class="fab fa-ubuntu"></i></a>
<a href="https://snapcraft.io/sengi" title="use Snap Store for linux"><img src="images/snap-store-white.png" /></a>
</p>
</div>

View File

@ -28,7 +28,8 @@ function createWindow() {
{ role: "reload" },
{ role: "forcereload" },
{ type: "separator" },
{ role: "close" }
{ role: "close" },
{ role: 'quit' }
]
},
{
@ -68,7 +69,8 @@ function createWindow() {
{ role: "delete" },
{ role: "selectall" },
{ type: "separator" },
{ role: "close" }
{ role: "close" },
{ role: 'quit' }
]
},
{

View File

@ -1,6 +1,6 @@
{
"name": "sengi",
"version": "0.7.2",
"version": "0.8.0",
"license": "AGPL-3.0-or-later",
"main": "main-electron.js",
"description": "A multi-account desktop client for Mastodon and Pleroma",

View File

@ -57,6 +57,7 @@ import { MentionsComponent } from './components/floating-column/manage-account/m
import { NotificationsComponent } from './components/floating-column/manage-account/notifications/notifications.component';
import { SettingsState } from './states/settings.state';
import { AccountEmojiPipe } from './pipes/account-emoji.pipe';
import { CardComponent } from './components/stream/status/card/card.component';
const routes: Routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
@ -102,7 +103,8 @@ const routes: Routes = [
DirectMessagesComponent,
MentionsComponent,
NotificationsComponent,
AccountEmojiPipe
AccountEmojiPipe,
CardComponent
],
imports: [
FontAwesomeModule,

View File

@ -54,7 +54,7 @@ export class AddNewAccountComponent implements OnInit {
return Promise.resolve(instanceApps[0].app);
} else {
const redirect_uri = this.getLocalHostname() + '/register';
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://github.com/NicolasConstant/sengi')
return this.authService.createNewApplication(instance, 'Sengi', redirect_uri, 'read write follow', 'https://nicolasconstant.github.io/sengi/')
.then((appData: AppData) => {
return this.saveNewApp(instance, appData)
.then(() => { return appData; });

View File

@ -1,6 +1,6 @@
<div class="panel">
<h3 class="panel__title">settings</h3>
<p class="version">Sengi version: 0.7.2</p>
<p class="version">Sengi version: 0.8.0</p>
</div>

View File

@ -1,7 +1,16 @@
<div class="media-viewer-canvas" (click)="close()">
<button class="media-viewer-canvas__close" title="close">
<button class="media-viewer-canvas__close media-viewer-canvas__button" title="close">
<fa-icon [icon]="faTimes"></fa-icon>
</button>
<button class="media-viewer-canvas__previous media-viewer-canvas__button" title="previous" (click)="previous($event)" *ngIf="previousAvailable">
<fa-icon [icon]="faAngleLeft"></fa-icon>
</button>
<button class="media-viewer-canvas__next media-viewer-canvas__button" title="next" (click)="next($event)" *ngIf="nextAvailable">
<fa-icon [icon]="faAngleRight"></fa-icon>
</button>
<img class="media-viewer-canvas__image" *ngIf="imageUrl" src="{{imageUrl}}" (click)="blockClick($event)"/>
<video class="media-viewer-canvas__image" *ngIf="gifvUrl" role="application" loop autoplay (click)="blockClick($event)">
<source src="{{ gifvUrl }}" type="video/mp4">
@ -9,4 +18,7 @@
<video class="media-viewer-canvas__image" *ngIf="videoUrl" role="application" loop controls="controls" (click)="blockClick($event)">
<source src="{{ videoUrl }}" type="video/mp4">
</video>
<div #video *ngIf="html" class="media-viewer-canvas__image media-viewer-canvas__iframe" [innerHTML]="html">
</div>
</div>

View File

@ -8,23 +8,76 @@
overflow: hidden;
position: relative;
&__close {
&__button {
@include clearButton;
padding: 5px;
position: absolute;
}
&__close {
top: 15px;
right: 20px;
font-size: 24px;
color: $font-link-primary;
color: $font-link-primary;
&:hover {
color: $font-link-primary-hover;
}
}
$browsing-icon-size: 30px;
$browsing-icon-top-position: calc(50vh - #{$browsing-icon-size/2});
$browsing-icon-bottom-position-mini: 0px;
$screen-break: 800px;
&__previous {
@media screen and (min-width: $screen-break) {
top: $browsing-icon-top-position;
left: 5px;
}
@media screen and (max-width: $screen-break) {
bottom: $browsing-icon-bottom-position-mini;
left: 40vw;
}
font-size: $browsing-icon-size;
color: whitesmoke;
padding: 10px;
}
&__next {
@media screen and (min-width: 800px) {
top: $browsing-icon-top-position;
right: 5px;
}
@media screen and (max-width: 800px) {
bottom: $browsing-icon-bottom-position-mini;
right: 40vw;
}
font-size: $browsing-icon-size;
color: whitesmoke;
padding: 10px;
}
&__image {
max-width: 95%;
@media screen and (min-width: $screen-break) {
max-width: 85%;
}
@media screen and (max-width: $screen-break) {
max-width: 95%;
}
max-height: calc(100% - 120px);
margin-top: 50vh;
margin-left: 50vw;
transform: translate(-50%, -50%);
}
&__iframe {
height: 60vw;
width: 95vw;
max-height: 600px;
max-width: 950px;
}
}

View File

@ -1,10 +1,12 @@
import { Component, OnInit, Input, Output } from '@angular/core';
import { faChevronLeft, faChevronRight, faTimes } from "@fortawesome/free-solid-svg-icons";
import { Component, OnInit, Input, Output, ElementRef, ViewChild, HostListener } from '@angular/core';
import { SafeHtml } from '@angular/platform-browser';
import { faTimes, faAngleLeft, faAngleRight } from "@fortawesome/free-solid-svg-icons";
import { Subject } from 'rxjs';
import { OpenMediaEvent } from '../../models/common.model';
import { Attachment } from '../../services/models/mastodon.interfaces';
@Component({
selector: 'app-media-viewer',
templateUrl: './media-viewer.component.html',
@ -12,27 +14,51 @@ import { Attachment } from '../../services/models/mastodon.interfaces';
})
export class MediaViewerComponent implements OnInit {
private _mediaEvent: OpenMediaEvent;
faChevronLeft = faChevronLeft;
faChevronRight = faChevronRight;
faTimes = faTimes;
faAngleLeft = faAngleLeft;
faAngleRight = faAngleRight;
imageUrl: string;
gifvUrl: string;
videoUrl: string;
html: SafeHtml;
previousAvailable: boolean;
nextAvailable: boolean;
private currentIndex: number;
@Input('openedMediaEvent')
set openedMediaEvent(value: OpenMediaEvent) {
this._mediaEvent = value;
const attachment = value.attachments[value.selectedIndex];
this.loadAttachment(attachment);
if (value.iframe) {
this.html = value.iframe;
this.autoplayIframe();
} else {
const attachment = value.attachments[value.selectedIndex];
this.currentIndex = value.selectedIndex;
this.loadAttachment(attachment);
this.setBrowsing();
}
}
get openedMediaEvent(): OpenMediaEvent {
return this._mediaEvent;
}
@Output() closeSubject = new Subject<boolean>();
@ViewChild('video') myVideo: ElementRef;
@HostListener('document:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
event.stopPropagation();
event.preventDefault();
if (event.key === 'ArrowRight') {
this.next(event);
} else if (event.key === 'ArrowLeft') {
this.previous(event);
}
}
constructor() { }
@ -42,20 +68,72 @@ export class MediaViewerComponent implements OnInit {
private loadAttachment(attachment: Attachment) {
if (attachment.type === 'image') {
this.imageUrl = attachment.url;
} else if (attachment.type === 'gifv'){
} else if (attachment.type === 'gifv') {
this.gifvUrl = attachment.url;
} else if (attachment.type === 'video'){
} else if (attachment.type === 'video') {
this.videoUrl = attachment.url;
}
}
private setBrowsing() {
var index = this.currentIndex;
var attachments = this.openedMediaEvent.attachments;
console.log(`index ${index}`);
console.log(`attachments.length ${attachments.length}`);
if (index < attachments.length - 1) {
this.nextAvailable = true;
} else {
this.nextAvailable = false;
}
if (index > 0) {
this.previousAvailable = true;
} else {
this.previousAvailable = false;
}
}
private autoplayIframe(): any {
setTimeout(() => {
if (this.myVideo.nativeElement.childNodes[0].src.includes('?')) {
this.myVideo.nativeElement.childNodes[0].src += '&autoplay=1&auto_play=1';
} else {
this.myVideo.nativeElement.childNodes[0].src += '?autoplay=1&auto_play=1';
}
}, 500);
}
close(): boolean {
this.closeSubject.next(true);
return false;
}
blockClick(event: any): boolean{
blockClick(event: any): boolean {
event.stopPropagation();
return false;
}
previous(event): boolean {
event.stopPropagation();
if (this.currentIndex <= 0) return false;
this.currentIndex--;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing();
return false;
}
next(event): boolean {
event.stopPropagation();
if (this.currentIndex >= this.openedMediaEvent.attachments.length - 1) return false;
this.currentIndex++;
this.imageUrl = this.openedMediaEvent.attachments[this.currentIndex].url;
this.setBrowsing();
return false;
}
}

View File

@ -51,7 +51,7 @@ export class AttachementsComponent implements OnInit {
}
attachmentSelected(index: number): boolean {
let openMediaEvent = new OpenMediaEvent(index, this.attachments);
let openMediaEvent = new OpenMediaEvent(index, this.attachments, null);
this.navigationService.openMedia(openMediaEvent);
return false;
}

View File

@ -0,0 +1,38 @@
<div class="card" *ngIf="card.type === 'link' || card.type === 'video'">
<a *ngIf="card.type === 'link'" class="card__link" href="{{ card.url }}" target="_blank" title="{{ card.title }}">
<img *ngIf="card.image" class="card__link--image" src="{{ card.image }}" alt="" />
<div *ngIf="!card.image" class="card__link--image">
<fa-icon class="card__link--image--logo" [icon]="faFileAlt"></fa-icon>
</div>
<h3 class="card__link--title">{{ card.title }}</h3>
<span class="card__link--host">{{ host }}</span>
</a>
<div *ngIf="card.type === 'photo'" class="card__photo">
</div>
<div *ngIf="card.type === 'video'" class="card__video">
<div *ngIf="!showHtml" class="card__video--preview">
<div class="card__video--preview--controls">
<a href class="card__video--preview--link" (click)="play()" title="play">
<fa-icon [icon]="faPlay"></fa-icon>
</a>
<a href class="card__video--preview--link" (click)="expand()" title="open">
<fa-icon [icon]="faExpand"></fa-icon>
</a>
<a href="{{ card.url }}" class="card__video--preview--link" target="_blank" title="browse">
<fa-icon [icon]="faExternalLinkAlt"></fa-icon>
</a>
</div>
<img src="{{ card.image }}" class="card__video--preview--image" />
</div>
<div #video *ngIf="showHtml" class="card__video--content" [innerHTML]="html">
</div>
<!-- {{ card.html }} -->
<!-- <div [innerHTML]="html">
</div> -->
</div>
<div *ngIf="card.type === 'rich'" class="card__rich" [innerHTML]="html"></div>
</div>

View File

@ -0,0 +1,111 @@
@import "variables";
// @import "mixins";
.card {
border: 1px solid $card-border-color;
background-color: $column-background;
border-radius: 3px;
transition: all .2s;
white-space: nowrap;
overflow: hidden;
&:hover {
background-color: lighten($column-background, 5);
}
&__link {
display: block;
text-decoration: none;
color: whitesmoke;
&--image {
width: 55px;
height: 55px;
float: left;
object-fit: cover;
border-right: 1px solid $card-border-color;
margin-right: 10px;
&--logo {
display: block;
font-size: 24px;
color: lighten($card-border-color, 5);
margin: 9px 0 0 18px;
}
}
&--title {
margin: 7px 0 0 0;
padding-right: 5px;
font-size: 1em;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
&--host {
margin: 11px 0 0 0px;
font-size: 0.8em;
opacity: .7;
}
}
&__photo {
}
&__video {
// border-radius: 3px;
&--preview {
position: relative;
&--controls {
position: absolute;
top: 50px;
left: 65px;
// outline: 2px solid greenyellow;
width: 100px;
height: 50px;
z-index: 10;
border-radius: 5px;
background-color: rgba($color: #000000, $alpha: .85);
padding-left: 12px;
}
&--link {
transition: all .2s;
display: block;
color: whitesmoke;
font-size: 16px;
margin: 12px 5px 0 5px;
float: left;
&:hover {
opacity: 0.7;
}
}
&--image {
width: 100%;
height: 150px;
// border: 1px solid salmon;
object-fit: cover;
}
}
&--content {
//width: 300px;
width: 100%;
height: 150px;
}
}
&__rich {
}
}

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { CardComponent } from './card.component';
describe('CardComponent', () => {
let component: CardComponent;
let fixture: ComponentFixture<CardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ CardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,68 @@
import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { faPlay, faExpand, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons";
import { faFileAlt } from "@fortawesome/free-regular-svg-icons";
import { Card } from '../../../../services/models/mastodon.interfaces';
import { NavigationService } from '../../../../services/navigation.service';
import { OpenMediaEvent } from '../../../../models/common.model';
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss']
})
export class CardComponent implements OnInit {
faPlay = faPlay;
faExpand = faExpand;
faFileAlt = faFileAlt;
faExternalLinkAlt = faExternalLinkAlt;
@Input() card: Card;
@ViewChild('video') myVideo: ElementRef;
host: string;
html: SafeHtml;
showHtml: boolean;
constructor(
private readonly navigationService: NavigationService,
private readonly sanitizer: DomSanitizer) { }
ngOnInit() {
this.host = this.card.url.replace('https://', '').replace('http://', '').split('/')[0];
}
private sanitize(card: Card): SafeHtml{
if(!card.html) return null;
let html = card.html.replace(`width="${card.width}"`, '');
html = html.replace(`height="${card.height}"`, '');
html = html.replace(`<iframe `, '<iframe allow="autoplay" style="width:100%; height:100%;"');
return this.sanitizer.bypassSecurityTrustHtml(html);
}
play(): boolean {
this.html = this.sanitize(this.card);
this.showHtml = true;
setTimeout(() => {
if(this.myVideo.nativeElement.childNodes[0].src.includes('?')){
this.myVideo.nativeElement.childNodes[0].src+='&autoplay=1&auto_play=1';
} else {
this.myVideo.nativeElement.childNodes[0].src+='?autoplay=1&auto_play=1';
}
}, 500);
return false;
}
expand(): boolean {
this.html = this.sanitize(this.card);
const openMedia = new OpenMediaEvent(null, null, this.html);
this.navigationService.openMedia(openMedia);
return false;
}
}

View File

@ -1,12 +1,15 @@
<div class="reblog" *ngIf="reblog">
<a class="reblog__profile-link" href (click)="openAccount(status.account)"><span innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar" src="{{ status.account.avatar }}" /></a> boosted
<a class="reblog__profile-link" href (click)="openAccount(status.account)"><span
innerHTML="{{ status.account | accountEmoji }}"></span> <img *ngIf="reblog" class="reblog__avatar"
src="{{ status.account.avatar }}" /></a> boosted
</div>
<div *ngIf="notificationType === 'favourite'">
<div class="notification--icon">
<fa-icon class="favorite" [icon]="faStar"></fa-icon>
</div>
<div class="notification--label">
<a href class="notification--link" (click)="openAccount(notificationAccount)" innerHTML="{{ notificationAccount | accountEmoji }}"></a> favorited your status
<a href class="notification--link" (click)="openAccount(notificationAccount)"
innerHTML="{{ notificationAccount | accountEmoji }}"></a> favorited your status
</div>
</div>
<div *ngIf="notificationType === 'reblog'">
@ -14,7 +17,8 @@
<fa-icon class="boost" [icon]="faRetweet"></fa-icon>
</div>
<div class="notification--label">
<a href class="notification--link" (click)="openAccount(notificationAccount)" innerHTML="{{ notificationAccount | accountEmoji }}"></a> boosted your status
<a href class="notification--link" (click)="openAccount(notificationAccount)"
innerHTML="{{ notificationAccount | accountEmoji }}"></a> boosted your status
</div>
</div>
<div class="status">
@ -32,7 +36,7 @@
</span>
</a>
<div class="status__created-at" title="{{ displayedStatus.created_at | date: 'full' }}">
<a class="status__created-at--link" href="{{ displayedStatus.url }}" target="_blank">
<a href class="status__created-at--link" (click)="textSelected()" (auxclick)="openUrl()">
{{ status.created_at | timeAgo | async }}
</a>
</div>
@ -47,7 +51,7 @@
<div class="status__labels--label status__labels--thread" title="thread" *ngIf="isThread">
thread
</div>
<div class="status__labels--label status__labels--discuss" title="this status has a discution"
<div class="status__labels--label status__labels--discuss" title="this status has a discussion"
*ngIf="hasReply">
replies
</div>
@ -62,6 +66,9 @@
<app-databinded-text class="status__content" *ngIf="!isContentWarned" [text]="statusContent"
(accountSelected)="accountSelected($event)" (hashtagSelected)="hashtagSelected($event)"
(textSelected)="textSelected()"></app-databinded-text>
<app-card class="status__card" *ngIf="displayedStatus.card && !hasAttachments" [card]="displayedStatus.card"></app-card>
<app-attachements *ngIf="!isContentWarned && hasAttachments" class="attachments"
[attachments]="displayedStatus.media_attachments">
</app-attachements>
@ -69,6 +76,6 @@
<app-action-bar #appActionBar [statusWrapper]="statusWrapper" (cwIsActiveEvent)="changeCw($event)"
(replyEvent)="openReply()"></app-action-bar>
</div>
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper"
(onClose)="closeReply()"></app-create-status>
<app-create-status *ngIf="replyingToStatus" [statusReplyingToWrapper]="statusWrapper" (onClose)="closeReply()">
</app-create-status>
</div>

View File

@ -111,6 +111,11 @@
margin: 0 10px 0 $avatar-column-space;
display: block;
}
&__card {
word-wrap: break-word;
margin: 10px 10px 0 $avatar-column-space;
display: block;
}
&__content-warning {
min-height: 80px;
display: block; // border: 1px solid greenyellow;

View File

@ -47,6 +47,7 @@ export class StatusComponent implements OnInit {
@Input('statusWrapper')
set statusWrapper(value: StatusWrapper) {
this._statusWrapper = value;
// console.warn(value.status);
this.status = value.status;
if (this.status.reblog) {
@ -148,7 +149,7 @@ export class StatusComponent implements OnInit {
this.browseHashtagEvent.next(hashtag);
}
textSelected(): void {
textSelected(): boolean {
const status = this._statusWrapper.status;
const accountInfo = this._statusWrapper.provider;
@ -160,5 +161,12 @@ export class StatusComponent implements OnInit {
}
this.browseThreadEvent.next(openThread);
return false;
}
openUrl(): boolean {
event.preventDefault();
window.open(this.displayedStatus.url, "_blank");
return false;
}
}

View File

@ -23,10 +23,10 @@ $header-height: 160px;
}
&__avatar {
position: absolute;
top: 15px;
left: 15px;
top: 12px;
left: 12px;
width: 80px;
border-radius: 50%;
border-radius: 3px;
}
&__display-name {
position: absolute;

View File

@ -1,10 +1,14 @@
import { SafeHtml } from '@angular/platform-browser';
import { Attachment, Status } from "../services/models/mastodon.interfaces";
import { AccountInfo } from '../states/accounts.state';
export class OpenMediaEvent {
constructor(
public selectedIndex: number,
public attachments: Attachment[]) {
public attachments: Attachment[],
public iframe: SafeHtml) {
}
}

View File

@ -83,6 +83,14 @@ export interface Card {
title: string;
description: string;
image: string;
type: 'link' | 'photo' | 'video' | 'rich';
author_name: string;
author_url: string;
provider_name: string;
provider_url: string;
html: any;
width: number;
height: number;
}
export interface Context {
@ -165,6 +173,7 @@ export interface Status {
emojis: Emoji[];
language: string;
pinned: boolean;
card: Card;
pleroma: PleromaStatusInfo;
}

View File

@ -50,4 +50,6 @@ $button-background-color-hover: lighten($color-primary, 20);
$column-background: #0f111a;
$column-background: #0f111a;
$card-border-color: #1e2435;