feat: adds primeng and novaloop design
This commit is contained in:
@@ -1,61 +1,11 @@
|
||||
<ng-progress [spinner]="false" [debounceTime]="300" [min]="20" [color]="'#83ebd6'" [thick]="true"></ng-progress>
|
||||
<nb-layout center>
|
||||
<nb-layout-header subheader>
|
||||
<div class="navbar-container">
|
||||
<div id="logo" style="padding-right: 20px;">
|
||||
<a [routerLink]="['/']">
|
||||
<img height="40px" src="assets/images/novaloop.png" alt="Novaloop favicon">
|
||||
</a>
|
||||
<h2>{{appName}}</h2>
|
||||
</div>
|
||||
<!--add class button-responsive to the button-->
|
||||
<button nbButton ghost class="button-responsive" [nbContextMenu]="navigationItems">
|
||||
<nb-icon icon="menu-outline"></nb-icon>
|
||||
</button>
|
||||
<!--add class menu-responsive to nb-actions-->
|
||||
<nb-actions class="left menu-responsive">
|
||||
<nb-action *ngFor="let item of navigationItems"
|
||||
[routerLink]="item.link"
|
||||
[title]="item.title">
|
||||
{{ item.title }}
|
||||
</nb-action>
|
||||
</nb-actions>
|
||||
</div>
|
||||
</nb-layout-header>
|
||||
|
||||
<nb-layout-column>
|
||||
<div [ngClass]="{'dark': themeService.isDarkThemeEnabled()}" class="flex flex-col h-screen justify-between bg.">
|
||||
<app-header></app-header>
|
||||
<div class="flex-grow bg-white dark:bg-denim-darker">
|
||||
<router-outlet></router-outlet>
|
||||
</nb-layout-column>
|
||||
</div>
|
||||
<app-footer></app-footer>
|
||||
</div>
|
||||
|
||||
<nb-layout-footer>
|
||||
<div class="footer-container">
|
||||
<div class="col col-left">
|
||||
Novaloop AG<br/>
|
||||
Niederdorfstrasse 88<br/>
|
||||
8001 Zürich<br/>
|
||||
</div>
|
||||
<div class="col col-center">
|
||||
<div class="mastodon-link">
|
||||
<img src="assets/images/mastodon.svg" alt="Mastodon logo" class="mastodon-logo">
|
||||
<a href="https://novaloop.social/@magbeat" target="_blank" rel="noreferrer">@magbeat@novaloop.social</a><br>
|
||||
</div>
|
||||
<div class="mastodon-link">
|
||||
<img src="assets/images/mastodon.svg" alt="Mastodon logo" class="mastodon-logo">
|
||||
<a href="https://novaloop.social/@langhard" target="_blank" rel="noreferrer">@langhard@novaloop.social</a><br>
|
||||
</div>
|
||||
<div class="mastodon-link">
|
||||
<img src="assets/images/mastodon.svg" alt="Mastodon logo" class="mastodon-logo">
|
||||
<a href="https://novaloop.social/@snowping" target="_blank" rel="noreferrer">@snowping@novaloop.social</a><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-right">
|
||||
<a href="tel:+41 44 500 54 60">+41 44 500 54 60</a><br/>
|
||||
<a href="https://www.novaloop.ch" target="_blank" rel="noreferrer">www.novaloop.ch</a><br/>
|
||||
<a href="mailto:mail@novaloop.ch">mail@novaloop.ch</a><br/>
|
||||
<a href="https://git.novaloop.ch/novaloop-oss/mastodon-apps" target="_blank" rel="noreferrer">Source Code (v{{version}})</a><br>
|
||||
<a href="https://git.novaloop.ch/novaloop-oss/mastodon-apps/issues" target="_blank" rel="noreferrer">Issues</a>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
</nb-layout-footer>
|
||||
</nb-layout>
|
||||
<p-toast position="bottom-right"></p-toast>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
.button-responsive {
|
||||
display: none
|
||||
}
|
||||
|
||||
#logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
a {
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
img.mastodon-logo {
|
||||
width: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
.col-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mastodon-link {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.col-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
.footer-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.button-responsive {
|
||||
display: inline-block
|
||||
}
|
||||
.menu-responsive {
|
||||
display: none
|
||||
}
|
||||
.col {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
.col-center {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.col-right {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
nb-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -2,28 +2,25 @@ import {Component, isDevMode} from '@angular/core';
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {fromApplication, fromAuthorize, selectIsLoggedIn} from "./shared/state/store";
|
||||
import {environment} from "../environments/environment";
|
||||
|
||||
// @ts-ignore
|
||||
import packageJson from '../../../../package.json';
|
||||
import {ThemeService} from "./shared/services/theme.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss']
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent {
|
||||
appName = environment.appName;
|
||||
version = packageJson.version;
|
||||
|
||||
navigationItems = [
|
||||
{title: 'Authorize', link: '/authorize'},
|
||||
{title: 'Stats', link: '/sync'},
|
||||
{title: 'Edit Lists', link: '/lists'},
|
||||
{title: 'List view', link: '/followings/list'},
|
||||
{title: 'Matrix View', link: '/followings/matrix'},
|
||||
{title: 'Table View', link: '/followings/table'},
|
||||
];
|
||||
|
||||
constructor(private store: Store) {
|
||||
constructor(private store: Store, public themeService: ThemeService) {
|
||||
const darkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (darkMode) {
|
||||
this.themeService.setTheme('dark');
|
||||
}
|
||||
this.store.dispatch(fromAuthorize.loadLocalStorage());
|
||||
this.store.pipe(
|
||||
select(selectIsLoggedIn),
|
||||
|
||||
@@ -4,10 +4,8 @@ import {BrowserModule} from '@angular/platform-browser';
|
||||
import {AppRoutingModule} from './app-routing.module';
|
||||
import {AppComponent} from './app.component';
|
||||
import {MastodonApiModule} from "projects/mastodon-api/src/public-api";
|
||||
import {NbActionsModule, NbContextMenuModule, NbDialogModule, NbGlobalPhysicalPosition, NbLayoutModule, NbMenuModule, NbThemeModule, NbToastrModule} from "@nebular/theme";
|
||||
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||
import {SharedModule} from './shared/shared.module';
|
||||
import {NbEvaIconsModule} from "@nebular/eva-icons";
|
||||
import {ActionReducer, StoreModule} from '@ngrx/store';
|
||||
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
|
||||
import {applicationStateReducer, authenticationStateReducer} from "./shared/state/store/reducers";
|
||||
@@ -16,13 +14,12 @@ import {NgProgressModule} from "ngx-progressbar";
|
||||
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
||||
import {AuthorizationModule} from "./authorization/authorization.module";
|
||||
import {ApplicationStateEffects, AuthorizationStateEffects, fromAuthorize} from "./shared/state/store";
|
||||
import {HeaderComponent} from './layout/header/header.component';
|
||||
import {FooterComponent} from './layout/footer/footer.component';
|
||||
import {ToastModule} from "primeng/toast";
|
||||
import {InputSwitchModule} from "primeng/inputswitch";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
|
||||
const toastrConfig = {
|
||||
duration: 3000,
|
||||
position: NbGlobalPhysicalPosition.BOTTOM_RIGHT,
|
||||
preventDuplicates: true,
|
||||
destroyByClick: true,
|
||||
};
|
||||
|
||||
export function handlePersistentState(reducer: ActionReducer<any>): ActionReducer<any> {
|
||||
return function (state, action) {
|
||||
@@ -50,6 +47,8 @@ export const metaReducers = [handlePersistentState];
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HeaderComponent,
|
||||
FooterComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -76,15 +75,9 @@ export const metaReducers = [handlePersistentState];
|
||||
SharedModule,
|
||||
NgProgressModule,
|
||||
NgProgressHttpModule,
|
||||
// Nebula
|
||||
NbContextMenuModule,
|
||||
NbMenuModule.forRoot(),
|
||||
NbDialogModule.forRoot(),
|
||||
NbEvaIconsModule,
|
||||
NbActionsModule,
|
||||
NbThemeModule.forRoot({name: 'dark'}),
|
||||
NbToastrModule.forRoot(toastrConfig),
|
||||
NbLayoutModule,
|
||||
ToastModule,
|
||||
InputSwitchModule,
|
||||
FormsModule,
|
||||
],
|
||||
providers: []
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import {AuthorizationRoutingModule} from "./authorization-routing.module";
|
||||
import {AuthorizeComponent} from './authorize/authorize.component';
|
||||
import {ReactiveFormsModule} from '@angular/forms';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {NbSpinnerModule} from "@nebular/theme";
|
||||
import {AuthGuard} from "./guards/auth.guard";
|
||||
|
||||
@NgModule({
|
||||
@@ -16,8 +15,6 @@ import {AuthGuard} from "./guards/auth.guard";
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
ReactiveFormsModule,
|
||||
// Nebula
|
||||
NbSpinnerModule,
|
||||
]
|
||||
})
|
||||
export class AuthorizationModule {
|
||||
|
||||
@@ -1,30 +1,53 @@
|
||||
<app-page>
|
||||
<div title>Authorize with your instance</div>
|
||||
<div body>
|
||||
|
||||
<div [formGroup]="serverForm" class="flex flex-col md:flex-row gap-4">
|
||||
<input
|
||||
id="serverUrl"
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="mastodon.social"
|
||||
[formControlName]="'instance'" (keydown.enter)="registerApplication()"
|
||||
>
|
||||
<button *ngIf="(loggedIn$ | async) === true"
|
||||
pButton
|
||||
type="button"
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async)"
|
||||
(click)="logout()"
|
||||
label="Logout"
|
||||
></button>
|
||||
<button pButton
|
||||
type="button"
|
||||
[disabled]="serverForm.invalid || (applicationRegistered$ | async) === true || (registeringApplication$ | async)"
|
||||
[loading]="(registeringApplication$ | async)!"
|
||||
(click)="registerApplication()"
|
||||
label="Register Application"
|
||||
></button>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async) || (loggedIn$ | async)"
|
||||
[loading]="(authorizingUser$ | async)!"
|
||||
(click)="authorizeUser()"
|
||||
label="Authorize User"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 py-6 sm:px-0">
|
||||
<div *ngIf="(loggedIn$ | async) && (instanceName$ | async) as instanceName">
|
||||
<p>User is authorized with {{instanceName}}</p>
|
||||
<p>Next up: Wait for <a routerLink="/sync">Sync</a> and manage</p>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
<!--
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<h1>Authorize with your instance</h1>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<div [formGroup]="serverForm">
|
||||
<input nbInput id="serverUrl" type="text" placeholder="mastodon.social" [formControlName]="'instance'" (keydown.enter)="registerApplication()">
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(registeringApplication$ | async)!"
|
||||
[disabled]="serverForm.invalid || (applicationRegistered$ | async) === true || (registeringApplication$ | async)"
|
||||
(click)="registerApplication()">Register Application
|
||||
</button>
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(authorizingUser$ | async)!"
|
||||
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async) || (loggedIn$ | async)"
|
||||
(click)="authorizeUser()">Authorize User
|
||||
</button>
|
||||
<button class="button" nbButton type="button"
|
||||
[nbSpinner]="(authorizingUser$ | async)!"
|
||||
[disabled]="(instanceName$ | async) === undefined || !(applicationRegistered$ | async)"
|
||||
(click)="logout()">Logout
|
||||
</button>
|
||||
</div>
|
||||
<div *ngIf="(loggedIn$ | async) && (instanceName$ | async) as instanceName">
|
||||
<p>User is authorized with {{instanceName}}</p>
|
||||
<p>Next up: Wait for <a routerLink="/sync">Sync</a> and manage</p>
|
||||
</div>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
-->
|
||||
|
||||
@@ -19,6 +19,10 @@ import {
|
||||
styleUrls: ['./authorize.component.scss']
|
||||
})
|
||||
export class AuthorizeComponent implements OnInit, OnDestroy {
|
||||
steps = [
|
||||
{label: 'Register Instance', description: 'Register the instance with the application', isValid: false},
|
||||
{label: 'Authorize User', description: 'Authorize User with instance', isValid: false},
|
||||
];
|
||||
registeringApplication$: Observable<boolean>;
|
||||
applicationRegistered$: Observable<boolean>;
|
||||
authorizingUser$: Observable<boolean>;
|
||||
|
||||
@@ -2,10 +2,13 @@ import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {FollowingsRoutingModule} from "./followings-list-routing.module";
|
||||
import {NbBadgeModule, NbCheckboxModule, NbListModule, NbPopoverModule, NbToggleModule, NbUserModule} from "@nebular/theme";
|
||||
import {ListComponent} from "./list/list.component";
|
||||
import {TableComponent} from "./table/table.component";
|
||||
import {MatrixComponent} from "./matrix/matrix.component";
|
||||
import {MultiSelectModule} from "primeng/multiselect";
|
||||
import {CheckboxModule} from "primeng/checkbox";
|
||||
import {OverlayPanelModule} from "primeng/overlaypanel";
|
||||
import {FormsModule} from "@angular/forms";
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -18,13 +21,10 @@ import {MatrixComponent} from "./matrix/matrix.component";
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
FollowingsRoutingModule,
|
||||
// Nebula
|
||||
NbCheckboxModule,
|
||||
NbListModule,
|
||||
NbToggleModule,
|
||||
NbBadgeModule,
|
||||
NbPopoverModule,
|
||||
NbUserModule,
|
||||
MultiSelectModule,
|
||||
CheckboxModule,
|
||||
OverlayPanelModule,
|
||||
FormsModule,
|
||||
]
|
||||
})
|
||||
export class FollowingsModule {
|
||||
|
||||
@@ -1,36 +1,43 @@
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<h1>Followings</h1>
|
||||
<div class="divider"></div>
|
||||
<app-filters></app-filters>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<nb-list>
|
||||
<nb-list-item *ngFor="let following of followings$ | async">
|
||||
<app-page>
|
||||
<div title>Followings (List View)</div>
|
||||
<div body>
|
||||
|
||||
<div class="pb-6">
|
||||
<app-filters></app-filters>
|
||||
</div>
|
||||
<div class="pt-6 grid gap-4 md:grid-cols lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<div *ngFor="let following of followings$ | async" class="w-full overflow-y-visible overflow-x-hidden">
|
||||
<app-account [account]="following">
|
||||
<div body>
|
||||
<div *ngIf="following.lists.length > 0">
|
||||
<h4>Lists</h4>
|
||||
<div *ngIf="following.lists.length > 0" class="pt-6">
|
||||
<ul>
|
||||
<li *ngFor="let list of following.lists">
|
||||
<a [routerLink]="['/lists']" [queryParams]="{listId: list.id}">{{list.title}}</a>
|
||||
<nb-icon icon="trash-2-outline" class="nb-icon-remove"
|
||||
(click)="removeAccountFromList(following.id, list.id)"></nb-icon>
|
||||
<i class="pi pi-list pr-2 align-middle"></i>
|
||||
<a [routerLink]="['/lists']" [queryParams]="{listId: list.id}" class="align-middle">{{list.title}}</a>
|
||||
<i class="pl-2 pi pi-times-circle align-middle cursor-pointer text-red-600" (click)="removeAccountFromList(following.id, list.id)"></i>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div footer>
|
||||
<select class="list-select" #listSelect>
|
||||
<option *ngFor="let list of lists$ | async" [value]="list.id">{{list.title}}</option>
|
||||
</select>
|
||||
<button class="add-to-list-button" nbButton
|
||||
status="basic"
|
||||
(click)="addAccountToSelectedList(following.id, listSelect)">Add to list
|
||||
</button>
|
||||
<div footer class="md:pt-6 flex flex-col space-y-6">
|
||||
<p-multiSelect
|
||||
[options]="(lists$ | async)!"
|
||||
appendTo="body"
|
||||
optionLabel="title"
|
||||
optionValue="id"
|
||||
#listSelect
|
||||
></p-multiSelect>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
class="p-button-sm"
|
||||
(click)="addAccountToSelectedList(following.id, listSelect)"
|
||||
label="Add to list(s)"
|
||||
></button>
|
||||
</div>
|
||||
</app-account>
|
||||
</nb-list-item>
|
||||
</nb-list>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
select.list-select {
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
nb-icon.nb-icon-remove {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-to-list-button {
|
||||
margin-left: 20px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
.add-to-list-button {
|
||||
margin-top: 20px;
|
||||
margin-left: 0;
|
||||
:host {
|
||||
::ng-deep .p-multiselect {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
import {MultiSelect} from "primeng/multiselect";
|
||||
|
||||
@Component({
|
||||
selector: 'app-list',
|
||||
@@ -11,15 +12,17 @@ import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "
|
||||
})
|
||||
export class ListComponent {
|
||||
followings$: Observable<ReadonlyArray<Account>>;
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
lists$: Observable<List[]>;
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
}
|
||||
|
||||
addAccountToSelectedList(accountId: string, select: HTMLSelectElement) {
|
||||
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId: select.value}));
|
||||
addAccountToSelectedList(accountId: string, select: MultiSelect) {
|
||||
select.value.forEach((listId: string) => {
|
||||
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId}));
|
||||
});
|
||||
}
|
||||
|
||||
removeAccountFromList(accountId: string, listId: string) {
|
||||
|
||||
@@ -1,65 +1,55 @@
|
||||
<nb-card>
|
||||
<nb-card-header style="position: relative;">
|
||||
<h1>Matrix View for Followings</h1>
|
||||
<div class="divider"></div>
|
||||
<nb-badge text="experimental" position="top right" status="success"></nb-badge>
|
||||
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||
<app-filters></app-filters>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="vertical-align: bottom;">
|
||||
Hover over a contact or click on it to see more info about the following
|
||||
</th>
|
||||
<th *ngFor="let list of lists$ | async" class="list-title">
|
||||
{{list.title}}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let account of followings$ | async">
|
||||
<th>
|
||||
<ng-template #templateRef>
|
||||
<div style="padding: 20px;">
|
||||
<p [innerHTML]="account.note"></p>
|
||||
<h4 *ngIf="account.fields.length > 0">Custom fields</h4>
|
||||
<div class="table">
|
||||
<div class="row" *ngFor="let field of account.fields">
|
||||
<div class="column first">{{ field.name }}</div>
|
||||
<div class="column" [innerHTML]="field.value"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
<span (click)="openMoreInfo(account)"
|
||||
[nbPopover]="templateRef"
|
||||
nbPopoverTrigger="hover"
|
||||
nbPopoverPlacement="bottom"
|
||||
>
|
||||
{{account.displayName}} <span class="username">@{{account.acct}}</span>
|
||||
</span>
|
||||
</th>
|
||||
<td *ngFor="let list of lists$ | async">
|
||||
<nb-checkbox style="cursor: pointer;"
|
||||
[checked]="isChecked(account.lists, list.id)"
|
||||
(checkedChange)="onCheckedChange($event, account.id, list.id)">
|
||||
</nb-checkbox>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
|
||||
<ng-template #dialog let-ref="dialogRef">
|
||||
<div class="container" *ngIf="selectedAccount">
|
||||
<app-account [account]="selectedAccount">
|
||||
<div footer>
|
||||
<button nbButton style="cursor: pointer;" (click)="ref.close()">Close Infos</button>
|
||||
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||
<app-page>
|
||||
<div title>Followings (Matrix View)</div>
|
||||
<div body>
|
||||
<div class="px-4 py-6 sm:px-0 flex flex-col divide-y">
|
||||
<div class="pb-6">
|
||||
<app-filters></app-filters>
|
||||
</div>
|
||||
</app-account>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="pt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="min-w-[350px] sticky top-0 bg-gray-100 dark:bg-denim-dark"></th>
|
||||
<th *ngFor="let list of lists$ | async" class="sticky top-0 bg-gray-100 dark:bg-denim-dark whitespace-nowrap h-[140px] align-bottom min-w-[50px]">
|
||||
<div class="w-[30px] [rotate:280deg] [translate: 25px, 51px] ">
|
||||
<span class="pl-4 pb-0">
|
||||
{{list.title}}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let account of followings$ | async">
|
||||
<th class="border-solid border-0 border-gray-300 border-b">
|
||||
<p-overlayPanel #info [dismissable]="true" [showCloseIcon]="true">
|
||||
<ng-template pTemplate="">
|
||||
<p [innerHTML]="account.note"></p>
|
||||
<h4 *ngIf="account.fields.length > 0">Custom fields</h4>
|
||||
<div class="table">
|
||||
<div class="row" *ngFor="let field of account.fields">
|
||||
<div class="column first">{{ field.name }}</div>
|
||||
<div class="column" [innerHTML]="field.value"></div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</p-overlayPanel>
|
||||
|
||||
<span class="font-bold">{{account.displayName}}</span> <span class="text-gray-400 pl-2">@{{account.acct}}</span> <i class="pl-2 pi pi-info-circle"
|
||||
(click)="info.toggle($event)"></i>
|
||||
</th>
|
||||
<td *ngFor="let list of lists$ | async" class="border-solid border-0 border-gray-300 border-b border-l text-center h-[50px]">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isChecked(account.lists, list.id)"
|
||||
(change)="onCheckedChange($event, account.id, list.id)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
span.username {
|
||||
font-weight: 100;
|
||||
margin-left: 10px;
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
table {
|
||||
display: block;
|
||||
height: max(600px, calc(100vh - 300px));
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
|
||||
td {
|
||||
border: none;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #222b45;
|
||||
z-index: 2;
|
||||
border: none;
|
||||
}
|
||||
|
||||
thead th.list-title {
|
||||
writing-mode: vertical-lr;
|
||||
transform: rotate(180deg);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tbody th {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background-color: #222b45;
|
||||
border: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
line-height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import {Component, TemplateRef, ViewChild} from '@angular/core';
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {NbDialogService} from "@nebular/theme";
|
||||
import {fromMatrixViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
|
||||
@@ -16,22 +15,23 @@ export class MatrixComponent {
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
selectedAccount: Account | undefined;
|
||||
|
||||
constructor(private store: Store, private dialogService: NbDialogService) {
|
||||
constructor(private store: Store) {
|
||||
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
}
|
||||
|
||||
openMoreInfo(account: Account) {
|
||||
this.selectedAccount = account;
|
||||
this.dialogService.open(this.dialog!);
|
||||
// TODO
|
||||
// this.dialogService.open(this.dialog!);
|
||||
}
|
||||
|
||||
isChecked(lists: List[], id: string): boolean {
|
||||
return lists.some(list => list.id === id);
|
||||
}
|
||||
|
||||
onCheckedChange($event: boolean, accountId: string, listId: string) {
|
||||
if ($event) {
|
||||
onCheckedChange($event: any, accountId: string, listId: string) {
|
||||
if ($event.currentTarget.checked) {
|
||||
this.store.dispatch(fromMatrixViewPage.addAccountToList({accountId, listId}));
|
||||
} else {
|
||||
this.store.dispatch(fromMatrixViewPage.removeAccountFromList({accountId, listId}));
|
||||
|
||||
@@ -1,58 +1,78 @@
|
||||
<nb-card>
|
||||
<nb-card-header style="position: relative;">
|
||||
<h1>Table View for Followings</h1>
|
||||
<div class="divider"></div>
|
||||
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||
<app-filters></app-filters>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Notes</th>
|
||||
<th>Fields</th>
|
||||
<th>Lists</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
<tr *ngFor="let row of rows$ |async">
|
||||
<td class="col small">
|
||||
<a [href]="row.url">
|
||||
<nb-user
|
||||
size="medium"
|
||||
shape="semi-round"
|
||||
[name]="row.displayName"
|
||||
[title]="row.username"
|
||||
[picture]="row.avatar"
|
||||
>
|
||||
</nb-user>
|
||||
</a>
|
||||
</td>
|
||||
<td class="col large">
|
||||
<span [innerHTML]="row.note"></span>
|
||||
</td>
|
||||
<td class="col large">
|
||||
<span [innerHTML]="row.fields"></span>
|
||||
</td>
|
||||
<td class="col medium">
|
||||
<div *ngFor="let list of row.lists">
|
||||
{{list.title}}
|
||||
<nb-icon icon="person-remove-outline" (click)="removeAccountFromList(row.id, list.id)" style="cursor: pointer;">
|
||||
Remove Account from list
|
||||
</nb-icon>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col medium">
|
||||
<div class="actions">
|
||||
<select class="list-select" #listSelect>
|
||||
<option *ngFor="let list of lists$ | async" [value]="list.id">{{list.title}}</option>
|
||||
</select>
|
||||
<nb-icon icon="person-add-outline" (click)="addAccountToSelectedList(row.id, listSelect.value)" style="cursor: pointer;">
|
||||
Add Account to list
|
||||
</nb-icon>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||
<app-page>
|
||||
<div title>Followings (Table View)</div>
|
||||
<div body>
|
||||
|
||||
<div class="px-4 py-6 sm:px-0 flex flex-col divide-y">
|
||||
<div class="pb-6">
|
||||
<app-filters></app-filters>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<p-table [value]="(rows$ | async)!" styleClass="p-datatable-striped">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th>
|
||||
Username
|
||||
</th>
|
||||
<th>
|
||||
Notes
|
||||
</th>
|
||||
<th>
|
||||
Fields
|
||||
</th>
|
||||
<th>
|
||||
Lists
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-row>
|
||||
<tr>
|
||||
<td class="small">
|
||||
<div class="flex items-center space-x-4 pb-6">
|
||||
<img class="w-10 h-10 rounded-full" [src]="row.avatar" alt="">
|
||||
<div class="font-medium">
|
||||
<div>{{row.displayName}}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||
<a [href]="row.url" rel="noreferrer" target="_blank">
|
||||
{{row.username}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="medium">
|
||||
<span [innerHTML]="row.note"></span>
|
||||
</td>
|
||||
<td class="medium">
|
||||
<span [innerHTML]="row.fields"></span>
|
||||
</td>
|
||||
<td class="medium">
|
||||
<div *ngFor="let list of row.lists">
|
||||
<a [routerLink]="['/lists']" [queryParams]="{listId: list.id}" class="align-middle">{{list.title}}</a>
|
||||
<i class="pl-2 pi pi-times-circle align-middle cursor-pointer text-red-600" (click)="removeAccountFromList(row.id, list.id)"></i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="large">
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-y-0 md:space-x-4">
|
||||
<p-multiSelect
|
||||
[options]="(lists$ | async)!"
|
||||
optionLabel="title"
|
||||
optionValue="id"
|
||||
#listSelect
|
||||
></p-multiSelect>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
(click)="addAccountToSelectedList(row.id, listSelect)"
|
||||
label="Add to list(s)"
|
||||
></button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
@@ -1,40 +1,13 @@
|
||||
table {
|
||||
display: block;
|
||||
height: max(600px, calc(100vh - 300px));
|
||||
::ng-deep .p-datatable {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
overflow-x: auto;
|
||||
|
||||
th {
|
||||
border: 1pt solid #101426;
|
||||
td.medium {
|
||||
max-width: calc(100vw / 6);
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1pt solid #101426;
|
||||
}
|
||||
|
||||
td.col {
|
||||
&.small {
|
||||
min-width: 180px;
|
||||
}
|
||||
&.medium {
|
||||
min-width: 300px;
|
||||
}
|
||||
&.large {
|
||||
min-width: 400px;
|
||||
}
|
||||
max-width: calc(100vw / 4);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
div.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
select {
|
||||
margin-right: 5px;
|
||||
td.large {
|
||||
min-width: 320px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {map, Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {fromTableViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
import {fromListViewPage, fromTableViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
import {MultiSelect} from "primeng/multiselect";
|
||||
import {FilterService} from "primeng/api";
|
||||
|
||||
interface DataGridRow {
|
||||
id: string;
|
||||
@@ -22,9 +24,23 @@ interface DataGridRow {
|
||||
})
|
||||
export class TableComponent {
|
||||
rows$: Observable<DataGridRow[]>;
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
lists$: Observable<List[]>;
|
||||
filterByListName = 'filter-by-lists';
|
||||
|
||||
constructor(private store: Store, private filterService: FilterService) {
|
||||
|
||||
this.filterService.register(this.filterByListName, (value: List[], filter: string[]): boolean => {
|
||||
console.log(filter);
|
||||
if (!filter || filter.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (filter && filter.length === 1 && filter[0] === '0') {
|
||||
console.log('here');
|
||||
return value.length === 0;
|
||||
}
|
||||
return value.some(list => filter.includes(list.id));
|
||||
});
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.rows$ = this.store
|
||||
.pipe(
|
||||
select(selectFilteredFollowingsWithLists),
|
||||
@@ -46,10 +62,13 @@ export class TableComponent {
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
}
|
||||
|
||||
addAccountToSelectedList(accountId: string, listId: string) {
|
||||
this.store.dispatch(fromTableViewPage.addAccountToList({accountId, listId}));
|
||||
addAccountToSelectedList(accountId: string, select: MultiSelect) {
|
||||
select.value.forEach((listId: string) => {
|
||||
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
removeAccountFromList(accountId: string, listId: string) {
|
||||
this.store.dispatch(fromTableViewPage.removeAccountFromList({accountId, listId}));
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<h1>Welcome to Mastolists</h1>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<app-page>
|
||||
<div title>Welcome to Mastolists</div>
|
||||
<div body>
|
||||
<p>Organize your Followings into lists.</p>
|
||||
|
||||
<ul>
|
||||
<li><a [routerLink]="['/authorize']">Authorize</a> with your instance</li>
|
||||
<li><a [routerLink]="['/sync']">Sync</a> your lists and followings to local storage</li>
|
||||
<li>Select the <a [routerLink]="['/followings/list']">List view</a> to add and remove users from lists</li>
|
||||
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||
</ul>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<footer class="p-4 bg-denim text-white dark:bg-denim-dark dark:text-white md:flex md:items-center md:justify-between md:p-6">
|
||||
<div class="text-sm sm:text-left flex flex-col">
|
||||
<a href="https://wwww.novaloop.ch/" class="hover:underline">Novaloop Ltd</a>
|
||||
Niederdorfstrasse 88<br>
|
||||
8001 Zurich<br>
|
||||
<a href="tel:+41 44 500 54 60">+41 44 500 54 60</a>
|
||||
<a href="mailto:mail@novaloop.ch">mail@novaloop.ch</a><br/>
|
||||
</div>
|
||||
<div class="text-sm sm:text-right flex flex-col">
|
||||
<a href="https://novaloop.social/@magbeat" target="_blank" rel="noreferrer">@magbeat@novaloop.social</a>
|
||||
<a href="https://novaloop.social/@snowping" target="_blank" rel="noreferrer">@snowping@novaloop.social</a>
|
||||
<a href="https://novaloop.social/@langhard" target="_blank" rel="noreferrer">@langhard@novaloop.social</a>
|
||||
<a href="https://git.novaloop.ch/novaloop-oss/mastodon-apps" target="_blank" rel="noreferrer">Source Code (v{{version}})</a>
|
||||
<a href="https://git.novaloop.ch/novaloop-oss/mastodon-apps/issues" target="_blank" rel="noreferrer">Issues</a>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,7 @@
|
||||
a {
|
||||
@apply text-olympic-light;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
@apply text-olympic;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ FooterComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import {Component} from '@angular/core';
|
||||
import packageJson from "../../../../../../package.json";
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss']
|
||||
})
|
||||
export class FooterComponent {
|
||||
version = packageJson.version;
|
||||
}
|
||||
111
projects/mastolists/src/app/layout/header/header.component.html
Normal file
111
projects/mastolists/src/app/layout/header/header.component.html
Normal file
@@ -0,0 +1,111 @@
|
||||
<nav class="bg-denim dark:bg-denim-dark">
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<a [routerLink]="'/home'">
|
||||
<img class="h-8 w-8" src="assets/images/novaloop.png" alt="Novaloop AG">
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex-shrink-0 pl-4">
|
||||
<a [routerLink]="'/home'">
|
||||
<h1 class="text-white text-xl">Mastolists</h1>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="ml-4 flex items-center md:ml-6">
|
||||
<div class="hidden md:block">
|
||||
<div class="ml-10 flex items-baseline space-x-4">
|
||||
<a href="#" *ngFor="let item of navigationItems"
|
||||
[routerLink]="item.link"
|
||||
[title]="item.title"
|
||||
class="text-gray-300 hover:bg-denim-darker hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
[routerLinkActive]="'bg-denim-darker dark:bg-gray-700 text-white'"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
|
||||
<!--
|
||||
<a href="#" class="bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium" aria-current="page">Dashboard</a>
|
||||
|
||||
<a href="#" class="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Team</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative ml-3 flex flex-row space-x-4">
|
||||
<div>
|
||||
<a href="#" [routerLink]="'/authorize'">
|
||||
<img class="h-8 w-8 rounded-full"
|
||||
*ngIf="currentAccount$ | async as account; else noAccount"
|
||||
[src]="account.avatar"
|
||||
alt=""
|
||||
>
|
||||
<ng-template #noAccount>
|
||||
<i class="pi pi-user text-white pt-2"></i>
|
||||
</ng-template>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<p-inputSwitch class="input-switch" [(ngModel)]="isDarkThemeEnabled" (onChange)="toggleTheme($event)"></p-inputSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="-mr-2 flex md:hidden">
|
||||
<!-- Mobile menu button -->
|
||||
<button type="button"
|
||||
class="inline-flex items-center justify-center rounded-md bg-gray-800 p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-0 focus:ring-offset-gray-800"
|
||||
(click)="navOpen = !navOpen"
|
||||
aria-controls="mobile-menu" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<!--
|
||||
Heroicon name: outline/bars-3
|
||||
|
||||
Menu open: "hidden", Menu closed: "block"
|
||||
-->
|
||||
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
|
||||
</svg>
|
||||
<!--
|
||||
Heroicon name: outline/x-mark
|
||||
|
||||
Menu open: "block", Menu closed: "hidden"
|
||||
-->
|
||||
<svg class="hidden h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu, show/hide based on menu state. -->
|
||||
<div *ngIf="navOpen" class="md:hidden" id="mobile-menu">
|
||||
<div class="space-y-1 px-2 pt-2 pb-3 sm:px-3 flex flex-col">
|
||||
<a href="#" *ngFor="let item of navigationItems"
|
||||
[routerLink]="item.link"
|
||||
[title]="item.title"
|
||||
(click)="navOpen = false"
|
||||
class="text-gray-300 hover:bg-denim-darker hover:text-white px-3 py-2 rounded-md text-sm font-medium"
|
||||
[routerLinkActive]="'bg-denim-darker text-white'"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="border-t border-gray-700 pt-4 pb-3" *ngIf="currentAccount$ | async as account">
|
||||
<div class="flex items-center px-5">
|
||||
<div class="flex-shrink-0">
|
||||
<img class="h-10 w-10 rounded-full" [src]="account.avatar" alt="">
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<div class="text-base font-medium leading-none text-white">{{account.displayName}}</div>
|
||||
<div class="text-sm font-medium leading-none text-gray-400">@{{account.acct}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-3 pl-2 pb-4">
|
||||
<p-inputSwitch class="input-switch" [(ngModel)]="isDarkThemeEnabled" (onChange)="toggleTheme($event)"></p-inputSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host ::ng-deep .p-inputswitch-slider:before {
|
||||
content: "\263C" !important;
|
||||
}
|
||||
|
||||
:host ::ng-deep .p-inputswitch.p-inputswitch-checked .p-inputswitch-slider:before {
|
||||
color: black;
|
||||
content: "\263D" !important;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ HeaderComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {Account} from 'projects/mastodon-api/src/public-api';
|
||||
import {Observable} from "rxjs";
|
||||
import {selectCurrentAccount} from "../../shared/state/store";
|
||||
import {ThemeService} from "../../shared/services/theme.service";
|
||||
import {InputSwitchOnChangeEvent} from "primeng/inputswitch";
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss']
|
||||
})
|
||||
export class HeaderComponent {
|
||||
navOpen = false;
|
||||
navigationItems = [
|
||||
{title: 'Authorize', link: '/authorize'},
|
||||
{title: 'Stats', link: '/sync'},
|
||||
{title: 'Edit Lists', link: '/lists'},
|
||||
{title: 'List view', link: '/followings/list'},
|
||||
{title: 'Matrix View', link: '/followings/matrix'},
|
||||
{title: 'Table View', link: '/followings/table'},
|
||||
];
|
||||
|
||||
currentAccount$: Observable<Account | undefined>;
|
||||
|
||||
isDarkThemeEnabled: boolean = this.themeService.isDarkThemeEnabled();
|
||||
|
||||
constructor(private store: Store, private themeService: ThemeService) {
|
||||
this.currentAccount$ = this.store.pipe(select(selectCurrentAccount));
|
||||
}
|
||||
|
||||
toggleTheme(event: InputSwitchOnChangeEvent) {
|
||||
this.themeService.setTheme(event.checked ? 'dark' : 'light')
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import {CommonModule} from '@angular/common';
|
||||
import {ListsComponent} from './lists/lists.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {ListsRoutingModule} from "./lists-routing.module";
|
||||
import {NbSpinnerModule} from "@nebular/theme";
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {RippleModule} from "primeng/ripple";
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -16,8 +16,8 @@ import {ReactiveFormsModule} from "@angular/forms";
|
||||
SharedModule,
|
||||
ListsRoutingModule,
|
||||
ReactiveFormsModule,
|
||||
// Nebular
|
||||
NbSpinnerModule,
|
||||
FormsModule,
|
||||
RippleModule,
|
||||
]
|
||||
})
|
||||
export class ListsModule {
|
||||
|
||||
@@ -1,31 +1,83 @@
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<h1>Lists</h1>
|
||||
<div class="divider"></div>
|
||||
<div [formGroup]="newListForm">
|
||||
<input nbInput id="title" type="text" [formControlName]="'title'" style="margin-right: 20px;">
|
||||
<button class="new-list-button" nbButton type="button"
|
||||
[nbSpinner]="creatingList"
|
||||
(click)="createList()">Create List
|
||||
</button>
|
||||
<app-page>
|
||||
<div title>Edit Lists</div>
|
||||
<div body>
|
||||
<div [formGroup]="newListForm" class="flex flex-col md:flex-row gap-6">
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
pInputText
|
||||
[formControlName]="'title'"
|
||||
>
|
||||
<button
|
||||
pButton
|
||||
type="button"
|
||||
[loading]="creatingList"
|
||||
[disabled]="creatingList"
|
||||
(click)="createList()"
|
||||
label="Create List"
|
||||
></button>
|
||||
<button pButton
|
||||
type="button"
|
||||
class="p-button-help"
|
||||
[loading]="(loading$ | async)!"
|
||||
[disabled]="(loading$ | async)!"
|
||||
(click)="loadLists()"
|
||||
label="Sync Lists"
|
||||
></button>
|
||||
</div>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<table *ngIf="(instanceName$ | async) as instanceName">
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Title</th>
|
||||
</tr>
|
||||
<tr *ngFor="let list of lists$ | async">
|
||||
<td>
|
||||
<a [href]="'https://' + instanceName + '/lists/' + list.id" target="_blank" rel="noopener">
|
||||
{{list.id}}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<input nbInput id="serverUrl" type="text" [value]="list.title" (change)="listNameChanged(list.id, $event)">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
<div class="pt-6">
|
||||
<p-table [value]="(lists$ | async)!"
|
||||
styleClass="p-datatable-striped"
|
||||
dataKey="id"
|
||||
editMode="row"
|
||||
(sortFunction)="customSort($event)"
|
||||
[customSort]="true"
|
||||
>
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="id" style="width: 80px">
|
||||
Id
|
||||
<p-sortIcon field="id"></p-sortIcon>
|
||||
</th>
|
||||
<th pSortableColumn="title">
|
||||
Title
|
||||
<p-sortIcon field="title"></p-sortIcon>
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-list let-editing="editing">
|
||||
<tr [pEditableRow]="list">
|
||||
<td>{{list.id}}</td>
|
||||
<td>
|
||||
<p-cellEditor>
|
||||
<ng-template pTemplate="input">
|
||||
<input pInputText type="text" [(ngModel)]="editModels[list.id].title" #titleInput>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="output">
|
||||
{{list.title}}
|
||||
</ng-template>
|
||||
</p-cellEditor>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex align-items-center justify-content-center gap-2">
|
||||
<button *ngIf="!editing" pButton pRipple type="button" pInitEditableRow icon="pi pi-pencil" (click)="onRowEditInit(list)"
|
||||
class="p-button-rounded p-button-text"></button>
|
||||
<button *ngIf="editing" pButton pRipple type="button" pSaveEditableRow icon="pi pi-check" (click)="onRowEditSave(list)"
|
||||
class="p-button-rounded p-button-text p-button-success mr-2"></button>
|
||||
<button *ngIf="editing" pButton pRipple type="button" pCancelEditableRow icon="pi pi-times" (click)="onRowEditCancel(list)"
|
||||
class="p-button-rounded p-button-text p-button-danger"></button>
|
||||
<div *ngIf="instanceName$ | async as instanceName" class="pt-4">
|
||||
<a [href]="'https://' + instanceName + '/lists/' + list.id" target="_blank" rel="noopener">
|
||||
<i class="pi pi-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
@@ -2,8 +2,9 @@ import {Component} from '@angular/core';
|
||||
import {List} from 'projects/mastodon-api/src/public-api';
|
||||
import {Observable} from "rxjs";
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {fromListsPage, selectCurrentInstance, selectLists} from "../../shared/state/store";
|
||||
import {fromListsPage, fromSyncPage, selectCurrentInstance, selectLists, selectLoading} from "../../shared/state/store";
|
||||
import {FormControl, FormGroup} from "@angular/forms";
|
||||
import {SortEvent} from "primeng/api";
|
||||
|
||||
@Component({
|
||||
selector: 'app-lists',
|
||||
@@ -11,10 +12,11 @@ import {FormControl, FormGroup} from "@angular/forms";
|
||||
styleUrls: ['./lists.component.scss']
|
||||
})
|
||||
export class ListsComponent {
|
||||
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
loading$: Observable<boolean>;
|
||||
lists$: Observable<List[]>;
|
||||
instanceName$: Observable<string | undefined>;
|
||||
creatingList: boolean = false;
|
||||
editModels: { [s: string]: List; } = {}
|
||||
|
||||
newListForm = new FormGroup({
|
||||
title: new FormControl(''),
|
||||
@@ -23,12 +25,39 @@ export class ListsComponent {
|
||||
constructor(private store: Store) {
|
||||
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
this.loading$ = this.store.pipe(select(selectLoading));
|
||||
}
|
||||
|
||||
listNameChanged(id: string, event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const newTitle = target.value;
|
||||
this.store.dispatch(fromListsPage.updateList({listId: id, newTitle}));
|
||||
customSort(event: SortEvent) {
|
||||
console.log(event);
|
||||
if (event.field == 'id') {
|
||||
return event.data!.sort((a, b) => {
|
||||
if (event.order == 1) {
|
||||
return parseInt(a['id']) >= parseInt(b['id']) ? 1 : -1;
|
||||
}
|
||||
return parseInt(a['id']) < parseInt(b['id']) ? 1 : -1;
|
||||
});
|
||||
}
|
||||
return event.data!.sort((a, b) => {
|
||||
if (event.order == 1) {
|
||||
return a[event.field!] >= b[event.field!] ? 1 : -1;
|
||||
}
|
||||
return a[event.field!] < b[event.field!] ? 1 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
onRowEditInit(list: List) {
|
||||
this.editModels[list.id] = {...list};
|
||||
}
|
||||
|
||||
onRowEditSave(list: List) {
|
||||
const newTitle = this.editModels[list.id].title;
|
||||
this.store.dispatch(fromListsPage.updateList({listId: list.id, newTitle}));
|
||||
delete this.editModels[list.id];
|
||||
}
|
||||
|
||||
onRowEditCancel(list: List) {
|
||||
delete this.editModels[list.id];
|
||||
}
|
||||
|
||||
createList() {
|
||||
@@ -38,4 +67,8 @@ export class ListsComponent {
|
||||
this.newListForm.reset();
|
||||
}
|
||||
}
|
||||
|
||||
loadLists() {
|
||||
this.store.dispatch(fromSyncPage.loadLists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
<nb-card accent="info" class="account-card">
|
||||
<nb-card-header>
|
||||
<a [href]="account.url">
|
||||
<nb-user
|
||||
size="medium"
|
||||
shape="semi-round"
|
||||
[name]="account.displayName"
|
||||
[title]="'@' + account.acct"
|
||||
[picture]="account.avatar"
|
||||
>
|
||||
</nb-user>
|
||||
</a>
|
||||
<ng-content select="'[header]'"></ng-content>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<p [innerHTML]="account.note"></p>
|
||||
<h4 *ngIf="account.fields.length > 0">Custom fields</h4>
|
||||
<div class="table">
|
||||
<div class="row" *ngFor="let field of account.fields">
|
||||
<div class="column first">{{ field.name }}</div>
|
||||
<div class="column" [innerHTML]="field.value"></div>
|
||||
<div class="p-6 bg-white dark:bg-denim-darker rounded-lg h-full">
|
||||
<div class="flex flex-col h-full divide-y justify-between">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:space-x-4 md:pb-6">
|
||||
<img class="w-10 h-10 rounded-full" [src]="account.avatar" alt="">
|
||||
<div class="font-medium">
|
||||
<div>{{account.displayName}}</div>
|
||||
<div class="text-sm text-gray-500 dark:text-white">
|
||||
<a [href]="account.url" rel="noreferrer" target="_blank">
|
||||
@{{account.acct}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content select="'[body]'"></ng-content>
|
||||
</nb-card-body>
|
||||
<nb-card-footer>
|
||||
<ng-content select="'[footer]'"></ng-content>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
||||
<div class="flex-grow pb-6">
|
||||
<div *ngIf="account.note" class="pt-6">
|
||||
<span [innerHTML]="account.note"></span>
|
||||
</div>
|
||||
<div *ngIf="account.fields.length > 0" class="flex flex-col md:pt-6 space-y-4">
|
||||
<div class="flex flex-col md:flex-row md:space-x-4" *ngFor="let field of account.fields">
|
||||
<div class="min-w-[80px]">{{ field.name }}</div>
|
||||
<div [innerHTML]="field.value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content select="'[body]'"></ng-content>
|
||||
</div>
|
||||
<div>
|
||||
<ng-content select="'[footer]'"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 30px;
|
||||
|
||||
.first {
|
||||
min-width: 160px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,52 @@
|
||||
<div class="form flex flex-row space-x-4">
|
||||
<form [formGroup]="filtersForm" class="w-full">
|
||||
<div class="full flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||
<input
|
||||
class="w-full md:w-[200px]"
|
||||
id="text"
|
||||
type="text"
|
||||
pInputText
|
||||
placeholder="@username or free text"
|
||||
[formControlName]="'text'"
|
||||
>
|
||||
<p-multiSelect
|
||||
[options]="(lists$ | async)!"
|
||||
optionLabel="title"
|
||||
optionValue="id"
|
||||
placeholder="Multiple Select"
|
||||
[formControlName]="'lists'"
|
||||
></p-multiSelect>
|
||||
<div class="flex flex-row space-x-2 pt-3">
|
||||
<p-inputSwitch [formControlName]="'unlisted'"></p-inputSwitch>
|
||||
<span>Unlisted only</span>
|
||||
</div>
|
||||
<button pButton
|
||||
type="button"
|
||||
(click)="clearFilter()"
|
||||
label="Clear Filter"
|
||||
></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<input nbInput id="text" type="text"
|
||||
placeholder="@username or free text"
|
||||
[formControlName]="'text'">
|
||||
<nb-select multiple placeholder="Multiple Select" [formControlName]="'lists'">
|
||||
<nb-option *ngFor="let list of lists$ | async" [value]="list.id">{{list.title}}</nb-option>
|
||||
</nb-select>
|
||||
</form>
|
||||
<div class="nb-toggle-container">
|
||||
<nb-toggle
|
||||
[checked]="(filters$ | async)?.unlisted"
|
||||
(checkedChange)="toggleUnlisted($event)"
|
||||
>Unlisted only
|
||||
</nb-toggle>
|
||||
</div>
|
||||
<button nbButton (click)="clearFilter()">Clear Filter</button>
|
||||
-->
|
||||
<!--
|
||||
<div class="container">
|
||||
<div class="form">
|
||||
<form [formGroup]="filtersForm">
|
||||
@@ -18,3 +67,4 @@
|
||||
<button nbButton (click)="clearFilter()">Clear Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
|
||||
@@ -1,46 +1,6 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
|
||||
input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
nb-select {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nb-toggle-container {
|
||||
padding-top: 2px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
.form {
|
||||
flex-direction: column;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-top: 10px;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
nb-select {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.nb-toggle-container {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
::ng-deep .p-multiselect {
|
||||
width: 100%;
|
||||
@screen md {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,29 +20,49 @@ export class FiltersComponent implements OnDestroy {
|
||||
filtersForm = new FormGroup({
|
||||
text: new FormControl(),
|
||||
lists: new FormControl([] as string[]),
|
||||
unlisted: new FormControl(false),
|
||||
});
|
||||
currentFilter: { fullText: string | undefined; lists: string[]; unlisted: boolean; } = {fullText: undefined, lists: [], unlisted: false};
|
||||
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
lists$: Observable<List[]>;
|
||||
filters$: Observable<FiltersForForm>;
|
||||
formSubscription: Subscription;
|
||||
filterSubscription: Subscription;
|
||||
|
||||
constructor(private store: Store) {
|
||||
this.lists$ = this.store.pipe(select(selectLists));
|
||||
this.filters$ = this.store.pipe(
|
||||
select(selectFiltersForForm),
|
||||
tap(filters => {
|
||||
this.currentFilter = {...filters, lists: [...filters.lists]};
|
||||
this.filtersForm.patchValue({
|
||||
text: filters.fullText,
|
||||
lists: filters.lists as string[],
|
||||
unlisted: filters.unlisted,
|
||||
}, {emitEvent: false, onlySelf: true});
|
||||
}),
|
||||
);
|
||||
this.filterSubscription = this.filters$.subscribe();
|
||||
this.formSubscription = this.filtersForm.valueChanges
|
||||
.pipe(debounceTime(500))
|
||||
.subscribe(value => {
|
||||
if (value['unlisted'] && !this.currentFilter.unlisted) {
|
||||
if (this.currentFilter.lists && this.currentFilter.lists.length > 0) {
|
||||
this.filtersForm.patchValue({
|
||||
lists: [],
|
||||
}, {emitEvent: false, onlySelf: true});
|
||||
value['lists'] = [];
|
||||
this.store.dispatch(fromFilters.setLists({lists: []}));
|
||||
}
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: true}));
|
||||
} else if (this.currentFilter.unlisted) {
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: false}));
|
||||
}
|
||||
if (value['lists'] && value['lists'].length > 0) {
|
||||
this.store.dispatch(fromFilters.setLists({lists: value.lists}));
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: false}));
|
||||
if (this.currentFilter.unlisted) {
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: false}));
|
||||
}
|
||||
}
|
||||
if (!value['lists'] || value['lists'].length === 0) {
|
||||
this.store.dispatch(fromFilters.setLists({lists: []}));
|
||||
@@ -63,22 +83,20 @@ export class FiltersComponent implements OnDestroy {
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||
}
|
||||
} else {
|
||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||
if (this.currentFilter.fullText && this.currentFilter.fullText.length > 0) {
|
||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.formSubscription.unsubscribe();
|
||||
this.filterSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
clearFilter() {
|
||||
this.store.dispatch(fromFilters.clearFilters());
|
||||
|
||||
}
|
||||
|
||||
toggleUnlisted($event: boolean) {
|
||||
this.store.dispatch(fromFilters.setUnlisted({unlisted: $event}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="only-visible-on-mobile">
|
||||
|
||||
<div class="only-visible-on-mobile m-4 p-4 rounded text-xl bg-gray-100 dark:bg-denim-dark">
|
||||
<p>This view is not optimized for mobile devices</p>
|
||||
<p>For a better experience on mobile visit the <a [routerLink]="['/followings', 'list']">list view</a></p>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.only-visible-on-mobile {
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 573px) {
|
||||
@screen lg {
|
||||
.only-visible-on-mobile {
|
||||
display: block;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="m-4 md:m-10 rounded bg-gray-100 dark:bg-denim-dark">
|
||||
<header>
|
||||
<div class="mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<ng-content select="'[title]'"></ng-content>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="mx-auto p-4 sm:p-6 lg:p-8">
|
||||
<ng-content select="'[body]'"></ng-content>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PageComponent } from './page.component';
|
||||
|
||||
describe('PageComponent', () => {
|
||||
let component: PageComponent;
|
||||
let fixture: ComponentFixture<PageComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ PageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page',
|
||||
templateUrl: './page.component.html',
|
||||
styleUrls: ['./page.component.scss']
|
||||
})
|
||||
export class PageComponent {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {MessageService} from 'primeng/api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NlNotificationService {
|
||||
|
||||
constructor(private messageService: MessageService) {
|
||||
}
|
||||
|
||||
danger(content: string, style: "warning" | "error" = "error") {
|
||||
this.messageService.add({detail: content, severity: style})
|
||||
}
|
||||
|
||||
success(content: string, style: "success" | "info" = "info") {
|
||||
this.messageService.add({detail: content, severity: style})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ThemeService } from './theme.service';
|
||||
|
||||
describe('ThemeService', () => {
|
||||
let service: ThemeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ThemeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
28
projects/mastolists/src/app/shared/services/theme.service.ts
Normal file
28
projects/mastolists/src/app/shared/services/theme.service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import {Inject, Injectable} from '@angular/core';
|
||||
import {DOCUMENT} from "@angular/common";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeService {
|
||||
|
||||
private currentTheme = 'light';
|
||||
|
||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
||||
}
|
||||
|
||||
setTheme(theme: string) {
|
||||
let themeLink = this.document.getElementById('app-theme') as HTMLLinkElement;
|
||||
|
||||
if (this.currentTheme != theme) {
|
||||
this.currentTheme = theme;
|
||||
if (themeLink) {
|
||||
themeLink.href = this.currentTheme + '.css';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isDarkThemeEnabled() {
|
||||
return this.currentTheme == 'dark';
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {AccountComponent} from './components/account/account.component';
|
||||
import {
|
||||
NbActionsModule,
|
||||
NbButtonModule,
|
||||
NbCardModule,
|
||||
NbIconModule,
|
||||
NbInputModule,
|
||||
NbSelectModule,
|
||||
NbSpinnerModule,
|
||||
NbToggleModule,
|
||||
NbUserModule
|
||||
} from "@nebular/theme";
|
||||
import {FiltersComponent} from './components/filters/filters.component';
|
||||
import {ReactiveFormsModule} from "@angular/forms";
|
||||
import {NotOptimizedForMobileDevicesComponent} from './components/not-optimized-for-mobile-devices/not-optimized-for-mobile-devices.component';
|
||||
import {RouterLink} from "@angular/router";
|
||||
import {ToastModule} from 'primeng/toast';
|
||||
import {MessageService} from "primeng/api";
|
||||
import {ButtonModule} from "primeng/button";
|
||||
import {InputTextModule} from "primeng/inputtext";
|
||||
import {TableModule} from "primeng/table";
|
||||
import {MultiSelectModule} from "primeng/multiselect";
|
||||
import {InputSwitchModule} from "primeng/inputswitch";
|
||||
import {PageComponent} from './components/page/page.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -23,36 +20,32 @@ import {RouterLink} from "@angular/router";
|
||||
AccountComponent,
|
||||
FiltersComponent,
|
||||
NotOptimizedForMobileDevicesComponent,
|
||||
PageComponent,
|
||||
],
|
||||
providers: [
|
||||
MessageService
|
||||
],
|
||||
providers: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
// Nebula import only
|
||||
NbInputModule,
|
||||
NbButtonModule,
|
||||
NbSpinnerModule,
|
||||
NbSelectModule,
|
||||
NbToggleModule,
|
||||
// Nebula
|
||||
NbCardModule,
|
||||
NbUserModule,
|
||||
NbActionsModule,
|
||||
NbIconModule,
|
||||
NbButtonModule,
|
||||
NbInputModule,
|
||||
RouterLink,
|
||||
ToastModule,
|
||||
//PrimeNG
|
||||
ButtonModule,
|
||||
InputTextModule,
|
||||
TableModule,
|
||||
MultiSelectModule,
|
||||
InputSwitchModule,
|
||||
],
|
||||
exports: [
|
||||
AccountComponent,
|
||||
FiltersComponent,
|
||||
// Nebula
|
||||
NbCardModule,
|
||||
NbActionsModule,
|
||||
NbIconModule,
|
||||
NbButtonModule,
|
||||
NbInputModule,
|
||||
NotOptimizedForMobileDevicesComponent,
|
||||
PageComponent,
|
||||
//PrimeNG
|
||||
ButtonModule,
|
||||
InputTextModule,
|
||||
TableModule,
|
||||
]
|
||||
})
|
||||
export class SharedModule {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||
import {Account} from "projects/mastodon-api/src/public-api";
|
||||
|
||||
export const fromAuthorize = createActionGroup({
|
||||
source: 'Authorize',
|
||||
@@ -9,7 +10,7 @@ export const fromAuthorize = createActionGroup({
|
||||
'Register Application Error': props<{ error: any }>(),
|
||||
'Authorize User': props<{ instanceName: string, clientId: string, redirectUrl: string }>(),
|
||||
'Get Access Token': props<{ code: string, instanceName: string, clientId: string, clientSecret: string, redirectUrl: string }>(),
|
||||
'Get Access Token Success': props<{ accessToken: string, accountId: string }>(),
|
||||
'Get Access Token Success': props<{ accessToken: string, account: Account }>(),
|
||||
'Get Access Token Error': emptyProps(),
|
||||
'Logout': emptyProps(),
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import {Account} from "projects/mastodon-api/src/public-api";
|
||||
|
||||
export interface AuthenticationState {
|
||||
registeringApplication: boolean;
|
||||
applicationRegistered: boolean;
|
||||
authorizingUser: boolean;
|
||||
instanceName?: string;
|
||||
accessToken?: string;
|
||||
accountId?: string;
|
||||
account?: Account;
|
||||
id?: string;
|
||||
appName?: string;
|
||||
website?: string;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||
import {catchError, exhaustMap, map, of, switchMap, tap, zip} from "rxjs";
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {List} from "projects/mastodon-api/src/public-api";
|
||||
import {fromApplication, fromListsPage, fromListViewPage, fromMastodonApi, fromMatrixViewPage, fromSyncPage, fromTableViewPage} from "../actions";
|
||||
import {ListService} from "../../../services/list.service";
|
||||
import {AccountService} from "../../../services/account.service";
|
||||
import {NlNotificationService} from "../../../services/nl-notification.service";
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationStateEffects {
|
||||
@@ -36,13 +36,13 @@ export class ApplicationStateEffects {
|
||||
updateListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.updateListSuccess),
|
||||
map(() => this.toastService.success('List updated successfully', 'Success')),
|
||||
map(() => this.notificationService.success('List updated successfully', 'success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
updateListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.updateListError),
|
||||
map(() => this.toastService.danger('Could not update the list', 'Error')),
|
||||
map(() => this.notificationService.danger('Could not update the list', 'error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
@@ -62,13 +62,13 @@ export class ApplicationStateEffects {
|
||||
createListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.createListSuccess),
|
||||
map(() => this.toastService.success('List created successfully', 'Success')),
|
||||
map(() => this.notificationService.success('List created successfully', 'success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
createListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.createListError),
|
||||
map(() => this.toastService.danger('Could not create the list', 'Error')),
|
||||
map(() => this.notificationService.danger('Could not create the list', 'error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
@@ -76,7 +76,7 @@ export class ApplicationStateEffects {
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.listsLoadedSuccess),
|
||||
switchMap((action) => {
|
||||
this.toastService.success('Lists loaded successfully', 'Success');
|
||||
this.notificationService.success('Lists loaded successfully', 'success');
|
||||
if (action.lists.length === 0) return of(fromMastodonApi.accountIdsForListsLoadedSuccess({mappings: {}}));
|
||||
const mappingsArr = action.lists.map((list) => this.listService.loadAccountsIdsForList(list.id));
|
||||
return zip(mappingsArr)
|
||||
@@ -110,14 +110,14 @@ export class ApplicationStateEffects {
|
||||
loadFollowingsSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.followingsLoadedSuccess),
|
||||
tap(() => this.toastService.success('Followings loaded successfully', 'Success')),
|
||||
tap(() => this.notificationService.success('Followings loaded successfully', 'success')),
|
||||
)
|
||||
, {dispatch: false});
|
||||
|
||||
loadFollowingsError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.followingsLoadedError),
|
||||
tap(() => this.toastService.danger('Followings could not be loaded', 'Error')),
|
||||
tap(() => this.notificationService.danger('Followings could not be loaded', 'error')),
|
||||
)
|
||||
, {dispatch: false});
|
||||
|
||||
@@ -135,14 +135,14 @@ export class ApplicationStateEffects {
|
||||
addAccountToListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.addAccountToListSuccess),
|
||||
map(() => this.toastService.success('Account added to list', 'Success')),
|
||||
map(() => this.notificationService.success('Account added to list', 'success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
addCountsToListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.addAccountToListError),
|
||||
map(() => this.toastService.danger('Could not add account to list, please reload', 'Error')),
|
||||
map(() => this.notificationService.danger('Could not add account to list, please reload', 'error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
@@ -159,14 +159,14 @@ export class ApplicationStateEffects {
|
||||
removeAccountFromListSuccess$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.removeAccountFromListSuccess),
|
||||
map(() => this.toastService.success('Account removed from list', 'Success')),
|
||||
map(() => this.notificationService.success('Account removed from list', 'success')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
removeAccountFromListError$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(fromMastodonApi.removeAccountFromListError),
|
||||
map(() => this.toastService.danger('Could not remove account from list, please reload', 'Error')),
|
||||
map(() => this.notificationService.danger('Could not remove account from list, please reload', 'error')),
|
||||
), {dispatch: false}
|
||||
);
|
||||
|
||||
@@ -174,7 +174,7 @@ export class ApplicationStateEffects {
|
||||
private actions$: Actions,
|
||||
private listService: ListService,
|
||||
private accountService: AccountService,
|
||||
private toastService: NbToastrService,
|
||||
private notificationService: NlNotificationService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import {Injectable} from "@angular/core";
|
||||
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||
import {fromAuthorize} from "../actions";
|
||||
import {catchError, exhaustMap, map, of, switchMap} from "rxjs";
|
||||
import {NbToastrService} from "@nebular/theme";
|
||||
import {MastodonApiAuthenticationService, RegisteredApp} from "projects/mastodon-api/src/public-api";
|
||||
import {Account, MastodonApiAuthenticationService, RegisteredApp} from "projects/mastodon-api/src/public-api";
|
||||
import {Store} from "@ngrx/store";
|
||||
import {environment} from "../../../../../environments/environment";
|
||||
import {NlNotificationService} from "../../../services/nl-notification.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthorizationStateEffects {
|
||||
@@ -17,7 +17,7 @@ export class AuthorizationStateEffects {
|
||||
.createApp(action.instanceName, environment.appName, action.redirectUrl, environment.appWebsite)
|
||||
.pipe(
|
||||
map((registeredApp: RegisteredApp) => {
|
||||
this.toastService.success('Successfully registered the application with the instance', 'Success');
|
||||
this.notificationService.success('Successfully registered the application with the instance', 'success');
|
||||
return fromAuthorize.registerApplicationSuccess({
|
||||
id: registeredApp.id,
|
||||
appName: registeredApp.name,
|
||||
@@ -27,7 +27,7 @@ export class AuthorizationStateEffects {
|
||||
})
|
||||
}),
|
||||
catchError((error) => {
|
||||
this.toastService.danger('Could not register application. Please check the instance name.', 'Error');
|
||||
this.notificationService.danger('Could not register application. Please check the instance name.', 'warning');
|
||||
return of(fromAuthorize.registerApplicationError({error}));
|
||||
}),
|
||||
)
|
||||
@@ -51,18 +51,18 @@ export class AuthorizationStateEffects {
|
||||
this.mastodonApiAuthService
|
||||
.verifyCredentials(result.action.instanceName, result.accessToken)
|
||||
.pipe(
|
||||
map((accountId) => {
|
||||
return {accessToken: result.accessToken, accountId};
|
||||
map((account: Account) => {
|
||||
return {accessToken: result.accessToken, account};
|
||||
})
|
||||
)
|
||||
),
|
||||
map((result) => {
|
||||
this.toastService.success('User successfully authenticated', 'Success');
|
||||
return fromAuthorize.getAccessTokenSuccess({accessToken: result.accessToken, accountId: result.accountId});
|
||||
this.notificationService.success('User successfully authenticated', 'success');
|
||||
return fromAuthorize.getAccessTokenSuccess({accessToken: result.accessToken, account: result.account});
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error(error);
|
||||
this.toastService.danger('Could not get access token', 'Error');
|
||||
this.notificationService.danger('Could not get access token', 'error');
|
||||
return of(fromAuthorize.getAccessTokenError());
|
||||
}),
|
||||
)
|
||||
@@ -80,7 +80,7 @@ export class AuthorizationStateEffects {
|
||||
private store: Store,
|
||||
private actions$: Actions,
|
||||
private mastodonApiAuthService: MastodonApiAuthenticationService,
|
||||
private toastService: NbToastrService,
|
||||
private notificationService: NlNotificationService,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,11 @@ export const authenticationStateReducer = createReducer(
|
||||
authorizingUser: true,
|
||||
}
|
||||
}),
|
||||
on(fromAuthorize.getAccessTokenSuccess, (_state, {accessToken, accountId}) => {
|
||||
on(fromAuthorize.getAccessTokenSuccess, (_state, {accessToken, account}) => {
|
||||
return {
|
||||
..._state,
|
||||
accessToken,
|
||||
accountId,
|
||||
account,
|
||||
authorizingUser: false,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,7 @@ export const authenticationStateFeature = createFeatureSelector<AuthenticationSt
|
||||
|
||||
export const selectIsLoggedIn = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.instanceName !== undefined && state.accessToken !== undefined && state.accountId !== undefined
|
||||
(state: AuthenticationState) => state.instanceName !== undefined && state.accessToken !== undefined && state.account !== undefined
|
||||
)
|
||||
|
||||
export const selectDataForAuthorizedRequest = createSelector(
|
||||
@@ -14,7 +14,7 @@ export const selectDataForAuthorizedRequest = createSelector(
|
||||
return {
|
||||
instanceName: state.instanceName,
|
||||
accessToken: state.accessToken,
|
||||
accountId: state.accountId,
|
||||
accountId: state.account?.id,
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -34,6 +34,10 @@ export const selectCurrentInstance = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.instanceName
|
||||
)
|
||||
export const selectCurrentAccount = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => state.account
|
||||
)
|
||||
export const selectCurrentInstanceWithApplicationRegisteredState = createSelector(
|
||||
authenticationStateFeature,
|
||||
(state: AuthenticationState) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ import {CommonModule} from '@angular/common';
|
||||
import {SyncComponent} from './sync/sync.component';
|
||||
import {SharedModule} from "../shared/shared.module";
|
||||
import {SyncRoutingModule} from "./sync-routing.module";
|
||||
import {NbCardModule, NbSpinnerModule} from "@nebular/theme";
|
||||
|
||||
|
||||
@NgModule({
|
||||
@@ -14,9 +13,6 @@ import {NbCardModule, NbSpinnerModule} from "@nebular/theme";
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
SyncRoutingModule,
|
||||
// Nebula
|
||||
NbCardModule,
|
||||
NbSpinnerModule,
|
||||
]
|
||||
})
|
||||
export class SyncModule {
|
||||
|
||||
@@ -1,45 +1,61 @@
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
<h1>Sync Remote Data with your Local Storage</h1>
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<nb-card>
|
||||
<nb-card-header>
|
||||
Currently in Local Store
|
||||
</nb-card-header>
|
||||
<nb-card-body>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th colspan="2">Followings</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Number of followings</td>
|
||||
<td>{{(followings$ | async)?.length}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="divider"></div>
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th colspan="2">Number of Accounts in List</th>
|
||||
</tr>
|
||||
<tr *ngFor="let list of lists$ | async">
|
||||
<td>{{list?.title}}</td>
|
||||
<td>{{list?.accounts?.length}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</nb-card-body>
|
||||
<nb-card-footer>
|
||||
<app-page>
|
||||
<div title>Sync Remote Data with your Local Storage</div>
|
||||
<div body>
|
||||
<div class="mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden bg-white dark:bg-denim-darker shadow sm:rounded-lg">
|
||||
<div class="px-4 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Followings in Local Store</h3>
|
||||
</div>
|
||||
<div class="px-4 sm:px-6">
|
||||
<p>Followings {{(followings$ | async)?.length}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="overflow-hidden bg-white dark:bg-denim-darker shadow sm:rounded-lg">
|
||||
<div class="px-4 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Number of Accounts in Lists</h3>
|
||||
</div>
|
||||
<div class="m-10">
|
||||
<p-table [value]="(lists$ | async)!" styleClass="p-datatable-striped">
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th pSortableColumn="title">
|
||||
List
|
||||
<p-sortIcon field="title"></p-sortIcon>
|
||||
<p-columnFilter field="title" type="text" display="menu"></p-columnFilter>
|
||||
</th>
|
||||
<th pSortableColumn="accounts.length">
|
||||
Accounts
|
||||
<p-sortIcon field="accounts.length"></p-sortIcon>
|
||||
</th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
<ng-template pTemplate="body" let-list>
|
||||
<tr>
|
||||
<td>{{list?.title}}</td>
|
||||
<td>{{list?.accounts?.length}}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<div class="flex flex-col">
|
||||
<div>
|
||||
<button nbButton type="button"
|
||||
[nbSpinner]="(loading$ | async)!"
|
||||
<button pButton
|
||||
type="button"
|
||||
[loading]="(loading$ | async)!"
|
||||
[disabled]="(loading$ | async)!"
|
||||
(click)="loadListsAndAccounts()">Get Lists and Accounts
|
||||
</button>
|
||||
(click)="loadListsAndAccounts()"
|
||||
label="Get Lists and Accounts"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div style="display:flex; flex-direction: column;" *ngIf="(instanceName$ | async) as instanceName">
|
||||
<div class="flex flex-col pt-6" *ngIf="(instanceName$ | async) as instanceName">
|
||||
<a [href]="'https://' + instanceName + '/settings/export'" rel="noreferrer" target="_blank">
|
||||
Export lists using your account settings page
|
||||
</a>
|
||||
@@ -48,7 +64,7 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div *ngIf="(lists$ | async) && (followings$ | async)">
|
||||
<div class="pt-6" *ngIf="(lists$ | async) && (followings$ | async)">
|
||||
<p>Now you can:</p>
|
||||
<ul>
|
||||
<li>Edit the lists <a [routerLink]="['/lists']">Lists</a></li>
|
||||
@@ -56,7 +72,7 @@
|
||||
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nb-card-footer>
|
||||
</nb-card>
|
||||
</nb-card-body>
|
||||
</nb-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-page>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {select, Store} from "@ngrx/store";
|
||||
import {selectCurrentInstance, fromSyncPage, selectFollowings, selectListsWithAccounts, selectLoading} from "../../shared/state/store";
|
||||
import {fromSyncPage, selectCurrentInstance, selectFollowings, selectListsWithAccounts, selectLoading} from "../../shared/state/store";
|
||||
import {Observable} from "rxjs";
|
||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
|
||||
@@ -12,7 +12,7 @@ import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||
export class SyncComponent {
|
||||
loading$: Observable<boolean>;
|
||||
followings$: Observable<ReadonlyArray<Account>>;
|
||||
lists$: Observable<ReadonlyArray<List>>;
|
||||
lists$: Observable<List[]>;
|
||||
instanceName$: Observable<string | undefined>;
|
||||
|
||||
constructor(private store: Store) {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mastolists</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<meta charset="utf-8">
|
||||
<title>Mastolists</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link id="app-theme" rel="stylesheet" type="text/css" href="light.css">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
/// <reference types="@angular/localize" />
|
||||
|
||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||
|
||||
import {AppModule} from './app/app.module';
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// this is our just created themes.scss file, make sure the path to the file is correct
|
||||
@use 'styles/themes' as *;
|
||||
@tailwind base;
|
||||
|
||||
// framework component styles
|
||||
@use '@nebular/theme/styles/globals' as *;
|
||||
|
||||
// install the framework styles
|
||||
@include nb-install() {
|
||||
@include nb-theme-global();
|
||||
@layer base {
|
||||
button {
|
||||
border: 0;
|
||||
}
|
||||
body {
|
||||
margin: 0px;
|
||||
@apply dark:bg-denim-darker;
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
@apply text-olympic-dark;
|
||||
@apply dark:text-olympic-light;
|
||||
}
|
||||
a:hover {
|
||||
@apply dark:text-olympic;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding: 10px;
|
||||
}
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
18
projects/mastolists/src/styles/designer/_colors.scss
Normal file
18
projects/mastolists/src/styles/designer/_colors.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
:root {
|
||||
@if variable-exists(colors) {
|
||||
@each $name, $color in $colors {
|
||||
@for $i from 0 through 5 {
|
||||
@if ($i == 0) {
|
||||
--#{$name}-50:#{tint($color, (5 - $i) * 19%)};
|
||||
}
|
||||
@else {
|
||||
--#{$name}-#{$i * 100}:#{tint($color, (5 - $i) * 19%)};
|
||||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 4 {
|
||||
--#{$name}-#{($i + 5) * 100}:#{shade($color, $i * 15%)};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
projects/mastolists/src/styles/designer/_common.scss
Normal file
68
projects/mastolists/src/styles/designer/_common.scss
Normal file
@@ -0,0 +1,68 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.p-component {
|
||||
font-family: $fontFamily;
|
||||
font-size: $fontSize;
|
||||
font-weight: $fontWeight;
|
||||
}
|
||||
|
||||
.p-component-overlay {
|
||||
background-color: $maskBg;
|
||||
transition-duration: $transitionDuration;
|
||||
}
|
||||
|
||||
.p-disabled, .p-component:disabled {
|
||||
opacity: $disabledOpacity;
|
||||
}
|
||||
|
||||
.p-error {
|
||||
color: $errorColor;
|
||||
}
|
||||
|
||||
.p-text-secondary {
|
||||
color: $textSecondaryColor;
|
||||
}
|
||||
|
||||
.pi {
|
||||
font-size: $primeIconFontSize;
|
||||
}
|
||||
|
||||
.p-link {
|
||||
font-size: $fontSize;
|
||||
font-family: $fontFamily;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
&:focus {
|
||||
@include focused();
|
||||
}
|
||||
}
|
||||
|
||||
.p-component-overlay-enter {
|
||||
animation: p-component-overlay-enter-animation 150ms forwards;
|
||||
}
|
||||
|
||||
.p-component-overlay-leave {
|
||||
animation: p-component-overlay-leave-animation 150ms forwards;
|
||||
}
|
||||
|
||||
.p-component-overlay {
|
||||
@keyframes p-component-overlay-enter-animation {
|
||||
from {
|
||||
background-color: transparent;
|
||||
}
|
||||
to {
|
||||
background-color: var(--maskbg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes p-component-overlay-leave-animation {
|
||||
from {
|
||||
background-color: var(--maskbg);
|
||||
}
|
||||
to {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
projects/mastolists/src/styles/designer/_components.scss
Normal file
102
projects/mastolists/src/styles/designer/_components.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
@import '_mixins';
|
||||
@import '_common';
|
||||
@import '_colors';
|
||||
|
||||
//Input
|
||||
@import './components/input/_autocomplete';
|
||||
@import './components/input/_calendar';
|
||||
@import './components/input/_cascadeselect';
|
||||
@import './components/input/_checkbox';
|
||||
@import './components/input/_chips';
|
||||
@import './components/input/_colorpicker';
|
||||
@import './components/input/_dropdown';
|
||||
@import './components/input/_editor';
|
||||
@import './components/input/_inputgroup';
|
||||
@import './components/input/_inputmask';
|
||||
@import './components/input/_inputnumber';
|
||||
@import './components/input/_inputswitch';
|
||||
@import './components/input/_inputtext';
|
||||
@import './components/input/_listbox';
|
||||
@import './components/input/_multiselect';
|
||||
@import './components/input/_password';
|
||||
@import './components/input/_radiobutton';
|
||||
@import './components/input/_rating';
|
||||
@import './components/input/_selectbutton';
|
||||
@import './components/input/_slider';
|
||||
@import './components/input/_togglebutton';
|
||||
@import './components/input/_treeselect';
|
||||
|
||||
//Button
|
||||
@import './components/button/_button';
|
||||
@import './components/button/_speeddial';
|
||||
@import './components/button/_splitbutton';
|
||||
|
||||
//Data
|
||||
@import './components/data/_carousel';
|
||||
@import './components/data/_datatable';
|
||||
@import './components/data/_dataview';
|
||||
@import './components/data/_filter';
|
||||
@import './components/data/_fullcalendar';
|
||||
@import './components/data/_orderlist';
|
||||
@import './components/data/_organizationchart';
|
||||
@import './components/data/_paginator';
|
||||
@import './components/data/_picklist';
|
||||
@import './components/data/_timeline';
|
||||
@import './components/data/_tree';
|
||||
@import './components/data/_treetable';
|
||||
@import './components/data/_virtualscroller';
|
||||
|
||||
//Panel
|
||||
@import './components/panel/_accordion';
|
||||
@import './components/panel/_card';
|
||||
@import './components/panel/_divider';
|
||||
@import './components/panel/_fieldset';
|
||||
@import './components/panel/_panel';
|
||||
@import './components/panel/_scrollpanel';
|
||||
@import './components/panel/_splitter';
|
||||
@import './components/panel/_tabview';
|
||||
@import './components/panel/_toolbar';
|
||||
|
||||
//Overlay
|
||||
@import './components/overlay/_confirmpopup';
|
||||
@import './components/overlay/_dialog';
|
||||
@import './components/overlay/_overlaypanel';
|
||||
@import './components/overlay/_sidebar';
|
||||
@import './components/overlay/_tooltip';
|
||||
|
||||
//File
|
||||
@import './components/file/_fileupload';
|
||||
|
||||
//Menu
|
||||
@import './components/menu/_breadcrumb';
|
||||
@import './components/menu/_contextmenu';
|
||||
@import './components/menu/_dock';
|
||||
@import './components/menu/_megamenu';
|
||||
@import './components/menu/_menu';
|
||||
@import './components/menu/_menubar';
|
||||
@import './components/menu/_panelmenu';
|
||||
@import './components/menu/_slidemenu';
|
||||
@import './components/menu/_steps';
|
||||
@import './components/menu/_tabmenu';
|
||||
@import './components/menu/_tieredmenu';
|
||||
|
||||
//Messages
|
||||
@import './components/messages/_inlinemessage';
|
||||
@import './components/messages/_message';
|
||||
@import 'components/messages/toast';
|
||||
|
||||
//MultiMedia
|
||||
@import './components/multimedia/_galleria';
|
||||
@import './components/multimedia/_image';
|
||||
|
||||
//Misc
|
||||
@import './components/misc/_avatar';
|
||||
@import './components/misc/_badge';
|
||||
@import './components/misc/_blockui';
|
||||
@import './components/misc/_chip';
|
||||
@import './components/misc/_inplace';
|
||||
@import './components/misc/_progressbar';
|
||||
@import './components/misc/_scrolltop';
|
||||
@import './components/misc/_skeleton';
|
||||
@import './components/misc/_tag';
|
||||
@import './components/misc/_terminal';
|
||||
203
projects/mastolists/src/styles/designer/_mixins.scss
Normal file
203
projects/mastolists/src/styles/designer/_mixins.scss
Normal file
@@ -0,0 +1,203 @@
|
||||
@mixin icon-override($icon) {
|
||||
&:before {
|
||||
content: $icon;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin focused() {
|
||||
outline: $focusOutline;
|
||||
outline-offset: $focusOutlineOffset;
|
||||
box-shadow: $focusShadow;
|
||||
}
|
||||
|
||||
@mixin focused-inset() {
|
||||
outline: $focusOutline;
|
||||
outline-offset: $focusOutlineOffset;
|
||||
box-shadow: inset $focusShadow;
|
||||
}
|
||||
|
||||
@mixin focused-input() {
|
||||
@include focused();
|
||||
border-color: $inputFocusBorderColor;
|
||||
}
|
||||
|
||||
@mixin focused-listitem() {
|
||||
outline: $focusOutline;
|
||||
outline-offset: $focusOutlineOffset;
|
||||
box-shadow: $inputListItemFocusShadow;
|
||||
}
|
||||
|
||||
@mixin invalid-input() {
|
||||
border-color: $inputErrorBorderColor;
|
||||
}
|
||||
|
||||
@mixin menuitem-link {
|
||||
padding: $menuitemPadding;
|
||||
color: $menuitemTextColor;
|
||||
border-radius: $menuitemBorderRadius;
|
||||
transition: $listItemTransition;
|
||||
user-select: none;
|
||||
|
||||
.p-menuitem-text {
|
||||
color: $menuitemTextColor;
|
||||
}
|
||||
|
||||
.p-menuitem-icon {
|
||||
color: $menuitemIconColor;
|
||||
margin-right: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-submenu-icon {
|
||||
color: $menuitemIconColor;
|
||||
}
|
||||
|
||||
&:not(.p-disabled):hover {
|
||||
background: $menuitemHoverBg;
|
||||
|
||||
.p-menuitem-text {
|
||||
color: $menuitemTextHoverColor;
|
||||
}
|
||||
|
||||
.p-menuitem-icon {
|
||||
color: $menuitemIconHoverColor;
|
||||
}
|
||||
|
||||
.p-submenu-icon {
|
||||
color: $menuitemIconHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include focused-listitem();
|
||||
}
|
||||
}
|
||||
|
||||
@mixin horizontal-rootmenuitem-link {
|
||||
padding: $horizontalMenuRootMenuitemPadding;
|
||||
color: $horizontalMenuRootMenuitemTextColor;
|
||||
border-radius: $horizontalMenuRootMenuitemBorderRadius;
|
||||
transition: $listItemTransition;
|
||||
user-select: none;
|
||||
|
||||
.p-menuitem-text {
|
||||
color: $horizontalMenuRootMenuitemTextColor;
|
||||
}
|
||||
|
||||
.p-menuitem-icon {
|
||||
color: $horizontalMenuRootMenuitemIconColor;
|
||||
margin-right: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-submenu-icon {
|
||||
color: $horizontalMenuRootMenuitemIconColor;
|
||||
margin-left: $inlineSpacing;
|
||||
}
|
||||
|
||||
&:not(.p-disabled):hover {
|
||||
background: $horizontalMenuRootMenuitemHoverBg;
|
||||
|
||||
.p-menuitem-text {
|
||||
color: $horizontalMenuRootMenuitemTextHoverColor;
|
||||
}
|
||||
|
||||
.p-menuitem-icon {
|
||||
color: $horizontalMenuRootMenuitemIconHoverColor;
|
||||
}
|
||||
|
||||
.p-submenu-icon {
|
||||
color: $horizontalMenuRootMenuitemIconHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include focused-listitem();
|
||||
}
|
||||
}
|
||||
|
||||
@mixin placeholder {
|
||||
::-webkit-input-placeholder {
|
||||
@content
|
||||
}
|
||||
:-moz-placeholder {
|
||||
@content
|
||||
}
|
||||
::-moz-placeholder {
|
||||
@content
|
||||
}
|
||||
:-ms-input-placeholder {
|
||||
@content
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scaledPadding($val, $scale) {
|
||||
padding: nth($val, 1) * $scale nth($val, 2) * $scale;
|
||||
}
|
||||
|
||||
@mixin scaledFontSize($val, $scale) {
|
||||
font-size: $val * $scale;
|
||||
}
|
||||
|
||||
@mixin nested-submenu-indents($val, $index, $length) {
|
||||
ul {
|
||||
li {
|
||||
a {
|
||||
padding-left: $val * ($index + 1);
|
||||
}
|
||||
|
||||
@if $index < $length {
|
||||
@include nested-submenu-indents($val, $index + 2, $length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin action-icon($enabled: true) {
|
||||
width: $actionIconWidth;
|
||||
height: $actionIconHeight;
|
||||
color: $actionIconColor;
|
||||
border: $actionIconBorder;
|
||||
background: $actionIconBg;
|
||||
border-radius: $actionIconBorderRadius;
|
||||
transition: $actionIconTransition;
|
||||
|
||||
&:enabled:hover {
|
||||
color: $actionIconHoverColor;
|
||||
border-color: $actionIconHoverBorderColor;
|
||||
background: $actionIconHoverBg;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include focused();
|
||||
}
|
||||
}
|
||||
|
||||
@function tint($color, $percentage) {
|
||||
@return mix(#fff, $color, $percentage);
|
||||
}
|
||||
|
||||
@function shade($color, $percentage) {
|
||||
@return mix(#000, $color, $percentage);
|
||||
}
|
||||
|
||||
@mixin button-states {
|
||||
// <button> and <a> tags support :enabled selector.
|
||||
|
||||
&:enabled,
|
||||
&:not(button):not(a):not(.p-disabled) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin menuitem-badge {
|
||||
background: $badgeBg;
|
||||
color: $badgeTextColor;
|
||||
font-size: $badgeFontSize;
|
||||
font-weight: $badgeFontWeight;
|
||||
min-width: $badgeMinWidth;
|
||||
height: $badgeHeight;
|
||||
line-height: $badgeHeight;
|
||||
border-radius: $borderRadius;
|
||||
margin-left: $inlineSpacing;
|
||||
padding-left: $inlineSpacing;
|
||||
padding-right: $inlineSpacing;
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
.p-button {
|
||||
color: $buttonTextColor;
|
||||
background: $buttonBg;
|
||||
border: $buttonBorder;
|
||||
padding: $buttonPadding;
|
||||
font-size: $fontSize;
|
||||
transition: $formElementTransition;
|
||||
border-radius: $borderRadius;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $buttonHoverBg;
|
||||
color: $buttonTextHoverColor;
|
||||
border-color: $buttonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $buttonActiveBg;
|
||||
color: $buttonTextActiveColor;
|
||||
border-color: $buttonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $buttonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($buttonBg, $textButtonHoverBgOpacity);
|
||||
color: $buttonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($buttonBg, $textButtonActiveBgOpacity);
|
||||
color: $buttonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&.p-button-plain {
|
||||
color: $plainButtonTextColor;
|
||||
border-color: $plainButtonTextColor;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $plainButtonHoverBgColor;
|
||||
color: $plainButtonTextColor;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $plainButtonActiveBgColor;
|
||||
color: $plainButtonTextColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $buttonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($buttonBg, $textButtonHoverBgOpacity);
|
||||
color: $buttonBg;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($buttonBg, $textButtonActiveBgOpacity);
|
||||
color: $buttonBg;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.p-button-plain {
|
||||
color: $plainButtonTextColor;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $plainButtonHoverBgColor;
|
||||
color: $plainButtonTextColor;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $plainButtonActiveBgColor;
|
||||
color: $plainButtonTextColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
@include focused();
|
||||
}
|
||||
|
||||
.p-button-label {
|
||||
transition-duration: $transitionDuration;
|
||||
}
|
||||
|
||||
.p-button-icon-left {
|
||||
margin-right: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-button-icon-right {
|
||||
margin-left: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-button-icon-bottom {
|
||||
margin-top: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-button-icon-top {
|
||||
margin-bottom: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-badge {
|
||||
margin-left: $inlineSpacing;
|
||||
min-width: $fontSize;
|
||||
height: $fontSize;
|
||||
line-height: $fontSize;
|
||||
color: $buttonBg;
|
||||
background-color: $buttonTextColor;
|
||||
}
|
||||
|
||||
&.p-button-raised {
|
||||
box-shadow: $raisedButtonShadow;
|
||||
}
|
||||
|
||||
&.p-button-rounded {
|
||||
border-radius: $roundedButtonBorderRadius;
|
||||
}
|
||||
|
||||
&.p-button-icon-only {
|
||||
width: $buttonIconOnlyWidth;
|
||||
padding: $buttonIconOnlyPadding;
|
||||
|
||||
.p-button-icon-left,
|
||||
.p-button-icon-right {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.p-button-rounded {
|
||||
border-radius: 50%;
|
||||
height: $buttonIconOnlyWidth;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-sm {
|
||||
@include scaledFontSize($fontSize, $scaleSM);
|
||||
@include scaledPadding($buttonPadding, $scaleSM);
|
||||
|
||||
.p-button-icon {
|
||||
@include scaledFontSize($primeIconFontSize, $scaleSM);
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-lg {
|
||||
@include scaledFontSize($fontSize, $scaleLG);
|
||||
@include scaledPadding($buttonPadding, $scaleLG);
|
||||
|
||||
.p-button-icon {
|
||||
@include scaledFontSize($primeIconFontSize, $scaleLG);
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-loading-label-only {
|
||||
.p-button-label {
|
||||
margin-left: $inlineSpacing;
|
||||
}
|
||||
|
||||
.p-button-loading-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-fluid {
|
||||
.p-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.p-button-icon-only {
|
||||
width: $buttonIconOnlyWidth;
|
||||
}
|
||||
|
||||
.p-buttonset {
|
||||
display: flex;
|
||||
|
||||
.p-button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-secondary, .p-buttonset.p-button-secondary > .p-button, .p-splitbutton.p-button-secondary > .p-button {
|
||||
color: $secondaryButtonTextColor;
|
||||
background: $secondaryButtonBg;
|
||||
border: $secondaryButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $secondaryButtonHoverBg;
|
||||
color: $secondaryButtonTextHoverColor;
|
||||
border-color: $secondaryButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $secondaryButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $secondaryButtonActiveBg;
|
||||
color: $secondaryButtonTextActiveColor;
|
||||
border-color: $secondaryButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $secondaryButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($secondaryButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $secondaryButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($secondaryButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $secondaryButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $secondaryButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($secondaryButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $secondaryButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($secondaryButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $secondaryButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-info, .p-buttonset.p-button-info > .p-button, .p-splitbutton.p-button-info > .p-button {
|
||||
color: $infoButtonTextColor;
|
||||
background: $infoButtonBg;
|
||||
border: $infoButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $infoButtonHoverBg;
|
||||
color: $infoButtonTextHoverColor;
|
||||
border-color: $infoButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $infoButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $infoButtonActiveBg;
|
||||
color: $infoButtonTextActiveColor;
|
||||
border-color: $infoButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $infoButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($infoButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $infoButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($infoButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $infoButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $infoButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($infoButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $infoButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($infoButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $infoButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-success, .p-buttonset.p-button-success > .p-button, .p-splitbutton.p-button-success > .p-button {
|
||||
color: $successButtonTextColor;
|
||||
background: $successButtonBg;
|
||||
border: $successButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $successButtonHoverBg;
|
||||
color: $successButtonTextHoverColor;
|
||||
border-color: $successButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $successButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $successButtonActiveBg;
|
||||
color: $successButtonTextActiveColor;
|
||||
border-color: $successButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $successButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($successButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $successButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($successButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $successButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $successButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($successButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $successButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($successButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $successButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-warning, .p-buttonset.p-button-warning > .p-button, .p-splitbutton.p-button-warning > .p-button {
|
||||
color: $warningButtonTextColor;
|
||||
background: $warningButtonBg;
|
||||
border: $warningButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $warningButtonHoverBg;
|
||||
color: $warningButtonTextHoverColor;
|
||||
border-color: $warningButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $warningButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $warningButtonActiveBg;
|
||||
color: $warningButtonTextActiveColor;
|
||||
border-color: $warningButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $warningButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($warningButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $warningButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($warningButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $warningButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $warningButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($warningButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $warningButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($warningButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $warningButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-help, .p-buttonset.p-button-help > .p-button, .p-splitbutton.p-button-help > .p-button {
|
||||
color: $helpButtonTextColor;
|
||||
background: $helpButtonBg;
|
||||
border: $helpButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $helpButtonHoverBg;
|
||||
color: $helpButtonTextHoverColor;
|
||||
border-color: $helpButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $helpButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $helpButtonActiveBg;
|
||||
color: $helpButtonTextActiveColor;
|
||||
border-color: $helpButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $helpButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($helpButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $helpButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($helpButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $helpButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $helpButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($helpButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $helpButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($helpButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $helpButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-danger, .p-buttonset.p-button-danger > .p-button, .p-splitbutton.p-button-danger > .p-button {
|
||||
color: $dangerButtonTextColor;
|
||||
background: $dangerButtonBg;
|
||||
border: $dangerButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: $dangerButtonHoverBg;
|
||||
color: $dangerButtonTextHoverColor;
|
||||
border-color: $dangerButtonHoverBorderColor;
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
box-shadow: $dangerButtonFocusShadow;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: $dangerButtonActiveBg;
|
||||
color: $dangerButtonTextActiveColor;
|
||||
border-color: $dangerButtonActiveBorderColor;
|
||||
}
|
||||
|
||||
&.p-button-outlined {
|
||||
background-color: transparent;
|
||||
color: $dangerButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($dangerButtonBg, $textButtonHoverBgOpacity);
|
||||
color: $dangerButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($dangerButtonBg, $textButtonActiveBgOpacity);
|
||||
color: $dangerButtonBg;
|
||||
border: $outlinedButtonBorder;
|
||||
}
|
||||
}
|
||||
|
||||
&.p-button-text {
|
||||
background-color: transparent;
|
||||
color: $dangerButtonBg;
|
||||
border-color: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: rgba($dangerButtonBg, $textButtonHoverBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $dangerButtonBg;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: rgba($dangerButtonBg, $textButtonActiveBgOpacity);
|
||||
border-color: transparent;
|
||||
color: $dangerButtonBg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-link {
|
||||
color: $linkButtonColor;
|
||||
background: transparent;
|
||||
border: transparent;
|
||||
|
||||
&:enabled:hover {
|
||||
background: transparent;
|
||||
color: $linkButtonHoverColor;
|
||||
border-color: transparent;
|
||||
|
||||
.p-button-label {
|
||||
text-decoration: $linkButtonTextHoverDecoration;
|
||||
}
|
||||
}
|
||||
|
||||
&:enabled:focus {
|
||||
background: transparent;
|
||||
box-shadow: $linkButtonFocusShadow;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&:enabled:active {
|
||||
background: transparent;
|
||||
color: $linkButtonColor;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user