feat: adds primeng and novaloop design
This commit is contained in:
18
angular.json
18
angular.json
@@ -30,7 +30,20 @@
|
|||||||
"projects/mastolists/src/assets"
|
"projects/mastolists/src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
"projects/mastolists/src/styles.scss"
|
"node_modules/primeng/resources/primeng.css",
|
||||||
|
"node_modules/primeicons/primeicons.css",
|
||||||
|
"projects/mastolists/src/styles.scss",
|
||||||
|
{
|
||||||
|
|
||||||
|
"input": "projects/mastolists/src/styles/novaloop-light/theme.css",
|
||||||
|
"bundleName": "light",
|
||||||
|
"inject": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "projects/mastolists/src/styles/novaloop-dark/theme.css",
|
||||||
|
"bundleName": "dark",
|
||||||
|
"inject": false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
},
|
},
|
||||||
@@ -120,6 +133,9 @@
|
|||||||
"projects/mastolists/src/assets"
|
"projects/mastolists/src/assets"
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
{
|
||||||
|
"input": "node_modules/@progress/kendo-theme-default/dist/all.css"
|
||||||
|
},
|
||||||
"projects/mastolists/src/styles.scss"
|
"projects/mastolists/src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
|
|||||||
1
kendo-ui-license.txt
Normal file
1
kendo-ui-license.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
eyJhbGciOiJSUzI1NiIsInR5cCI6IkxJQyJ9.eyJwcm9kdWN0cyI6W3sidHJpYWwiOnRydWUsImNvZGUiOiJLRU5ET1VJQU5HVUxBUiIsImxpY2Vuc2VFeHBpcmF0aW9uRGF0ZSI6MTY3ODI0MzkxOH1dLCJpbnRlZ3JpdHkiOiJrNVc5MEp4VkRXb3JtVUJKSlpDcU1Wd2FrQm89IiwibGljZW5zZUhvbGRlciI6Im1hcmt1cy5odWdnbGVyQG5vdmFsb29wLmNoIiwiaWF0IjoxNjc1NjkxNTk4LCJhdWQiOiJtYXJrdXMuaHVnZ2xlckBub3ZhbG9vcC5jaCIsInVzZXJJZCI6IjNiZTM4N2M0LWFmNTQtNDQ1NS05NDM3LWVkNThlYzZjYzY3YSJ9.vqQ372HPP6IHfJHfWqRQB1aSXTp0IQ6YWA_Ns4fDrd0Mn4epysyHonWvRpLvM8N8mTKJbvw4NSxAIVCyBIAlxv1gYvIvX5xprV0KLs_xXD2VdoBaEIBWzHkZrY9htTqM3Rng9LFDu4jeYKtVxo7YXDWTuLwP-n3sgBKnZYuasyY6MxdOVCw9YRnqUUURPxI7KIlOODbiNtazl2wrsXQZvRjG41XvgIByxoQ6EAHndVA7yYQOOFewbRBOkxcq_YAfeak4NE6nKsU7A3ZaooFqBdZbdsbK-KYaaf_zC_oXI-t9NMr53BOI04EGIOmQ8PZtCAhL8F0LBiuT68TKxo-ujQ
|
||||||
38375
package-lock.json
generated
38375
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -12,41 +12,41 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^15.0.0",
|
"@angular/animations": "^15.1.4",
|
||||||
"@angular/common": "^15.0.0",
|
"@angular/common": "^15.1.4",
|
||||||
"@angular/compiler": "^15.0.0",
|
"@angular/compiler": "^15.1.4",
|
||||||
"@angular/core": "^15.0.0",
|
"@angular/core": "^15.1.4",
|
||||||
"@angular/forms": "^15.0.0",
|
"@angular/forms": "^15.1.4",
|
||||||
"@angular/platform-browser": "^15.0.0",
|
"@angular/localize": "^15.1.4",
|
||||||
"@angular/platform-browser-dynamic": "^15.0.0",
|
"@angular/platform-browser": "^15.1.4",
|
||||||
"@angular/router": "^15.0.0",
|
"@angular/platform-browser-dynamic": "^15.1.4",
|
||||||
"@nebular/auth": "10.0.0",
|
"@angular/router": "^15.1.4",
|
||||||
"@nebular/eva-icons": "^10.0.0",
|
"@ngrx/effects": "^15.2.1",
|
||||||
"@nebular/security": "10.0.0",
|
"@ngrx/entity": "^15.2.1",
|
||||||
"@nebular/theme": "10.0.0",
|
"@ngrx/store": "^15.2.1",
|
||||||
"@ngrx/effects": "^15.0.0",
|
"@ngrx/store-devtools": "^15.2.1",
|
||||||
"@ngrx/entity": "^15.0.0",
|
|
||||||
"@ngrx/store": "^15.0.0",
|
|
||||||
"@ngrx/store-devtools": "^15.0.0",
|
|
||||||
"axios": "^1.2.1",
|
"axios": "^1.2.1",
|
||||||
"eva-icons": "^1.1.3",
|
"eva-icons": "^1.1.3",
|
||||||
"ngx-progressbar": "^9.0.0",
|
"ngx-progressbar": "^9.0.0",
|
||||||
"oauth": "^0.10.0",
|
"oauth": "^0.10.0",
|
||||||
"object-assign-deep": "^0.4.0",
|
"object-assign-deep": "^0.4.0",
|
||||||
"parse-link-header": "^2.0.0",
|
"parse-link-header": "^2.0.0",
|
||||||
|
"primeicons": "^6.0.1",
|
||||||
|
"primeng": "^15.2.0",
|
||||||
"rxjs": "~7.5.0",
|
"rxjs": "~7.5.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.12.0"
|
"zone.js": "~0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^15.0.0-beta.0",
|
"@angular-builders/custom-webpack": "^15.0.0-beta.0",
|
||||||
"@angular-devkit/build-angular": "^15.0.3",
|
"@angular-devkit/build-angular": "^15.1.5",
|
||||||
"@angular/cli": "~15.0.2",
|
"@angular/cli": "~15.1.5",
|
||||||
"@angular/compiler-cli": "^15.0.0",
|
"@angular/compiler-cli": "^15.0.0",
|
||||||
"@types/jasmine": "~4.3.0",
|
"@types/jasmine": "~4.3.0",
|
||||||
"@types/oauth": "^0.9.1",
|
"@types/oauth": "^0.9.1",
|
||||||
"@types/object-assign-deep": "^0.4.0",
|
"@types/object-assign-deep": "^0.4.0",
|
||||||
"@types/parse-link-header": "^2.0.0",
|
"@types/parse-link-header": "^2.0.0",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
"jasmine-core": "~4.5.0",
|
"jasmine-core": "~4.5.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.1.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
@@ -54,28 +54,9 @@
|
|||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.0.0",
|
"karma-jasmine-html-reporter": "~2.0.0",
|
||||||
"ng-packagr": "^15.0.0",
|
"ng-packagr": "^15.0.0",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"tailwindcss": "^3.2.6",
|
||||||
"typescript": "~4.8.2"
|
"typescript": "~4.8.2"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {}
|
||||||
"@nebular/auth@10.0.0": {
|
|
||||||
"@angular/animations": "15.0.3",
|
|
||||||
"@angular/cdk": "15.0.0",
|
|
||||||
"@angular/common": "15.0.3",
|
|
||||||
"@angular/core": "15.0.3",
|
|
||||||
"@angular/forms": "15.0.3",
|
|
||||||
"@angular/router": "15.0.3"
|
|
||||||
},
|
|
||||||
"@nebular/theme@10.0.0": {
|
|
||||||
"@angular/animations": "15.0.3",
|
|
||||||
"@angular/cdk": "15.0.0",
|
|
||||||
"@angular/common": "15.0.3",
|
|
||||||
"@angular/core": "15.0.3",
|
|
||||||
"@angular/router": "15.0.3"
|
|
||||||
},
|
|
||||||
"@nebular/security@10.0.0": {
|
|
||||||
"@angular/common": "15.0.3",
|
|
||||||
"@angular/core": "15.0.3",
|
|
||||||
"@angular/router": "15.0.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {catchError, map, Observable, throwError} from "rxjs";
|
import {catchError, map, Observable, tap, throwError} from "rxjs";
|
||||||
import {RegisterAppResponse} from "../interfaces/responses/register_app_response";
|
import {RegisterAppResponse} from "../interfaces/responses/register_app_response";
|
||||||
import {GetAccessTokenResponse} from "../interfaces/responses/get_access_token_response";
|
import {GetAccessTokenResponse} from "../interfaces/responses/get_access_token_response";
|
||||||
import {VerifyCredentialsResponse} from "../interfaces/responses/verify_credentials_response";
|
import {VerifyCredentialsResponse} from "../interfaces/responses/verify_credentials_response";
|
||||||
import {MastodonApiService} from "./mastodon-api.service";
|
import {MastodonApiService} from "./mastodon-api.service";
|
||||||
import {RegisteredApp} from "../interfaces/public/registered_app";
|
import {RegisteredApp} from "../interfaces/public/registered_app";
|
||||||
|
import {Account} from "../interfaces/public/account";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@@ -57,11 +58,13 @@ export class MastodonApiAuthenticationService {
|
|||||||
window.location.href = `https://${instance}/oauth/authorize?${parameters}`;
|
window.location.href = `https://${instance}/oauth/authorize?${parameters}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyCredentials(instance: string, accessToken: string): Observable<string> {
|
verifyCredentials(instance: string, accessToken: string): Observable<Account> {
|
||||||
const url = `https://${instance}/api/v1/accounts/verify_credentials`;
|
const url = `https://${instance}/api/v1/accounts/verify_credentials`;
|
||||||
return this.mastodonApiService
|
return this.mastodonApiService
|
||||||
.getAuthenticated<VerifyCredentialsResponse>(url, accessToken)
|
.getAuthenticated<VerifyCredentialsResponse>(url, accessToken)
|
||||||
.pipe(map((response) => response.id));
|
.pipe(
|
||||||
|
map((response) => response as Account)
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,11 @@
|
|||||||
<ng-progress [spinner]="false" [debounceTime]="300" [min]="20" [color]="'#83ebd6'" [thick]="true"></ng-progress>
|
<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>
|
<router-outlet></router-outlet>
|
||||||
</nb-layout-column>
|
</div>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
<nb-layout-footer>
|
<p-toast position="bottom-right"></p-toast>
|
||||||
<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>
|
|
||||||
|
|||||||
@@ -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 {select, Store} from "@ngrx/store";
|
||||||
import {fromApplication, fromAuthorize, selectIsLoggedIn} from "./shared/state/store";
|
import {fromApplication, fromAuthorize, selectIsLoggedIn} from "./shared/state/store";
|
||||||
import {environment} from "../environments/environment";
|
import {environment} from "../environments/environment";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import packageJson from '../../../../package.json';
|
import packageJson from '../../../../package.json';
|
||||||
|
import {ThemeService} from "./shared/services/theme.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss'],
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
appName = environment.appName;
|
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.dispatch(fromAuthorize.loadLocalStorage());
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(selectIsLoggedIn),
|
select(selectIsLoggedIn),
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import {BrowserModule} from '@angular/platform-browser';
|
|||||||
import {AppRoutingModule} from './app-routing.module';
|
import {AppRoutingModule} from './app-routing.module';
|
||||||
import {AppComponent} from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
import {MastodonApiModule} from "projects/mastodon-api/src/public-api";
|
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 {BrowserAnimationsModule} from "@angular/platform-browser/animations";
|
||||||
import {SharedModule} from './shared/shared.module';
|
import {SharedModule} from './shared/shared.module';
|
||||||
import {NbEvaIconsModule} from "@nebular/eva-icons";
|
|
||||||
import {ActionReducer, StoreModule} from '@ngrx/store';
|
import {ActionReducer, StoreModule} from '@ngrx/store';
|
||||||
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
|
import {StoreDevtoolsModule} from "@ngrx/store-devtools";
|
||||||
import {applicationStateReducer, authenticationStateReducer} from "./shared/state/store/reducers";
|
import {applicationStateReducer, authenticationStateReducer} from "./shared/state/store/reducers";
|
||||||
@@ -16,13 +14,12 @@ import {NgProgressModule} from "ngx-progressbar";
|
|||||||
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
import {NgProgressHttpModule} from "ngx-progressbar/http";
|
||||||
import {AuthorizationModule} from "./authorization/authorization.module";
|
import {AuthorizationModule} from "./authorization/authorization.module";
|
||||||
import {ApplicationStateEffects, AuthorizationStateEffects, fromAuthorize} from "./shared/state/store";
|
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> {
|
export function handlePersistentState(reducer: ActionReducer<any>): ActionReducer<any> {
|
||||||
return function (state, action) {
|
return function (state, action) {
|
||||||
@@ -50,6 +47,8 @@ export const metaReducers = [handlePersistentState];
|
|||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent,
|
AppComponent,
|
||||||
|
HeaderComponent,
|
||||||
|
FooterComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@@ -76,15 +75,9 @@ export const metaReducers = [handlePersistentState];
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
NgProgressModule,
|
NgProgressModule,
|
||||||
NgProgressHttpModule,
|
NgProgressHttpModule,
|
||||||
// Nebula
|
ToastModule,
|
||||||
NbContextMenuModule,
|
InputSwitchModule,
|
||||||
NbMenuModule.forRoot(),
|
FormsModule,
|
||||||
NbDialogModule.forRoot(),
|
|
||||||
NbEvaIconsModule,
|
|
||||||
NbActionsModule,
|
|
||||||
NbThemeModule.forRoot({name: 'dark'}),
|
|
||||||
NbToastrModule.forRoot(toastrConfig),
|
|
||||||
NbLayoutModule,
|
|
||||||
],
|
],
|
||||||
providers: []
|
providers: []
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {AuthorizationRoutingModule} from "./authorization-routing.module";
|
|||||||
import {AuthorizeComponent} from './authorize/authorize.component';
|
import {AuthorizeComponent} from './authorize/authorize.component';
|
||||||
import {ReactiveFormsModule} from '@angular/forms';
|
import {ReactiveFormsModule} from '@angular/forms';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {NbSpinnerModule} from "@nebular/theme";
|
|
||||||
import {AuthGuard} from "./guards/auth.guard";
|
import {AuthGuard} from "./guards/auth.guard";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -16,8 +15,6 @@ import {AuthGuard} from "./guards/auth.guard";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
// Nebula
|
|
||||||
NbSpinnerModule,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AuthorizationModule {
|
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>
|
||||||
<nb-card-header>
|
<nb-card-header>
|
||||||
<h1>Authorize with your instance</h1>
|
<h1>Authorize with your instance</h1>
|
||||||
</nb-card-header>
|
</nb-card-header>
|
||||||
<nb-card-body>
|
<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-body>
|
||||||
</nb-card>
|
</nb-card>
|
||||||
|
-->
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ import {
|
|||||||
styleUrls: ['./authorize.component.scss']
|
styleUrls: ['./authorize.component.scss']
|
||||||
})
|
})
|
||||||
export class AuthorizeComponent implements OnInit, OnDestroy {
|
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>;
|
registeringApplication$: Observable<boolean>;
|
||||||
applicationRegistered$: Observable<boolean>;
|
applicationRegistered$: Observable<boolean>;
|
||||||
authorizingUser$: Observable<boolean>;
|
authorizingUser$: Observable<boolean>;
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import {NgModule} from '@angular/core';
|
|||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {FollowingsRoutingModule} from "./followings-list-routing.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 {ListComponent} from "./list/list.component";
|
||||||
import {TableComponent} from "./table/table.component";
|
import {TableComponent} from "./table/table.component";
|
||||||
import {MatrixComponent} from "./matrix/matrix.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({
|
@NgModule({
|
||||||
@@ -18,13 +21,10 @@ import {MatrixComponent} from "./matrix/matrix.component";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
FollowingsRoutingModule,
|
FollowingsRoutingModule,
|
||||||
// Nebula
|
MultiSelectModule,
|
||||||
NbCheckboxModule,
|
CheckboxModule,
|
||||||
NbListModule,
|
OverlayPanelModule,
|
||||||
NbToggleModule,
|
FormsModule,
|
||||||
NbBadgeModule,
|
|
||||||
NbPopoverModule,
|
|
||||||
NbUserModule,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class FollowingsModule {
|
export class FollowingsModule {
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
<nb-card>
|
<app-page>
|
||||||
<nb-card-header>
|
<div title>Followings (List View)</div>
|
||||||
<h1>Followings</h1>
|
<div body>
|
||||||
<div class="divider"></div>
|
|
||||||
<app-filters></app-filters>
|
<div class="pb-6">
|
||||||
</nb-card-header>
|
<app-filters></app-filters>
|
||||||
<nb-card-body>
|
</div>
|
||||||
<nb-list>
|
<div class="pt-6 grid gap-4 md:grid-cols lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<nb-list-item *ngFor="let following of followings$ | async">
|
<div *ngFor="let following of followings$ | async" class="w-full overflow-y-visible overflow-x-hidden">
|
||||||
<app-account [account]="following">
|
<app-account [account]="following">
|
||||||
<div body>
|
<div body>
|
||||||
<div *ngIf="following.lists.length > 0">
|
<div *ngIf="following.lists.length > 0" class="pt-6">
|
||||||
<h4>Lists</h4>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li *ngFor="let list of following.lists">
|
<li *ngFor="let list of following.lists">
|
||||||
<a [routerLink]="['/lists']" [queryParams]="{listId: list.id}">{{list.title}}</a>
|
<i class="pi pi-list pr-2 align-middle"></i>
|
||||||
<nb-icon icon="trash-2-outline" class="nb-icon-remove"
|
<a [routerLink]="['/lists']" [queryParams]="{listId: list.id}" class="align-middle">{{list.title}}</a>
|
||||||
(click)="removeAccountFromList(following.id, list.id)"></nb-icon>
|
<i class="pl-2 pi pi-times-circle align-middle cursor-pointer text-red-600" (click)="removeAccountFromList(following.id, list.id)"></i>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div footer>
|
<div footer class="md:pt-6 flex flex-col space-y-6">
|
||||||
<select class="list-select" #listSelect>
|
<p-multiSelect
|
||||||
<option *ngFor="let list of lists$ | async" [value]="list.id">{{list.title}}</option>
|
[options]="(lists$ | async)!"
|
||||||
</select>
|
appendTo="body"
|
||||||
<button class="add-to-list-button" nbButton
|
optionLabel="title"
|
||||||
status="basic"
|
optionValue="id"
|
||||||
(click)="addAccountToSelectedList(following.id, listSelect)">Add to list
|
#listSelect
|
||||||
</button>
|
></p-multiSelect>
|
||||||
|
<button
|
||||||
|
pButton
|
||||||
|
type="button"
|
||||||
|
class="p-button-sm"
|
||||||
|
(click)="addAccountToSelectedList(following.id, listSelect)"
|
||||||
|
label="Add to list(s)"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</app-account>
|
</app-account>
|
||||||
</nb-list-item>
|
</div>
|
||||||
</nb-list>
|
</div>
|
||||||
</nb-card-body>
|
</div>
|
||||||
</nb-card>
|
</app-page>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,5 @@
|
|||||||
select.list-select {
|
:host {
|
||||||
height: 40px;
|
::ng-deep .p-multiselect {
|
||||||
font-size: 16px;
|
width: 100%;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {Account, List} from 'projects/mastodon-api/src/public-api';
|
|||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||||
|
import {MultiSelect} from "primeng/multiselect";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
@@ -11,15 +12,17 @@ import {fromListViewPage, selectFilteredFollowingsWithLists, selectLists} from "
|
|||||||
})
|
})
|
||||||
export class ListComponent {
|
export class ListComponent {
|
||||||
followings$: Observable<ReadonlyArray<Account>>;
|
followings$: Observable<ReadonlyArray<Account>>;
|
||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<List[]>;
|
||||||
|
|
||||||
constructor(private store: Store) {
|
constructor(private store: Store) {
|
||||||
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccountToSelectedList(accountId: string, select: HTMLSelectElement) {
|
addAccountToSelectedList(accountId: string, select: MultiSelect) {
|
||||||
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId: select.value}));
|
select.value.forEach((listId: string) => {
|
||||||
|
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAccountFromList(accountId: string, listId: string) {
|
removeAccountFromList(accountId: string, listId: string) {
|
||||||
|
|||||||
@@ -1,65 +1,55 @@
|
|||||||
<nb-card>
|
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||||
<nb-card-header style="position: relative;">
|
<app-page>
|
||||||
<h1>Matrix View for Followings</h1>
|
<div title>Followings (Matrix View)</div>
|
||||||
<div class="divider"></div>
|
<div body>
|
||||||
<nb-badge text="experimental" position="top right" status="success"></nb-badge>
|
<div class="px-4 py-6 sm:px-0 flex flex-col divide-y">
|
||||||
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
<div class="pb-6">
|
||||||
<app-filters></app-filters>
|
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</app-account>
|
<div class="pt-6 grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||||
</div>
|
<table class="w-full text-sm text-left text-gray-500 dark:text-white">
|
||||||
</ng-template>
|
<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 {Component, TemplateRef, ViewChild} from '@angular/core';
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
import {select, Store} from "@ngrx/store";
|
||||||
import {NbDialogService} from "@nebular/theme";
|
|
||||||
import {fromMatrixViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
import {fromMatrixViewPage, selectFilteredFollowingsWithLists, selectLists} from "../../shared/state/store";
|
||||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||||
|
|
||||||
@@ -16,22 +15,23 @@ export class MatrixComponent {
|
|||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<ReadonlyArray<List>>;
|
||||||
selectedAccount: Account | undefined;
|
selectedAccount: Account | undefined;
|
||||||
|
|
||||||
constructor(private store: Store, private dialogService: NbDialogService) {
|
constructor(private store: Store) {
|
||||||
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
this.followings$ = this.store.pipe(select(selectFilteredFollowingsWithLists));
|
||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
}
|
}
|
||||||
|
|
||||||
openMoreInfo(account: Account) {
|
openMoreInfo(account: Account) {
|
||||||
this.selectedAccount = account;
|
this.selectedAccount = account;
|
||||||
this.dialogService.open(this.dialog!);
|
// TODO
|
||||||
|
// this.dialogService.open(this.dialog!);
|
||||||
}
|
}
|
||||||
|
|
||||||
isChecked(lists: List[], id: string): boolean {
|
isChecked(lists: List[], id: string): boolean {
|
||||||
return lists.some(list => list.id === id);
|
return lists.some(list => list.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCheckedChange($event: boolean, accountId: string, listId: string) {
|
onCheckedChange($event: any, accountId: string, listId: string) {
|
||||||
if ($event) {
|
if ($event.currentTarget.checked) {
|
||||||
this.store.dispatch(fromMatrixViewPage.addAccountToList({accountId, listId}));
|
this.store.dispatch(fromMatrixViewPage.addAccountToList({accountId, listId}));
|
||||||
} else {
|
} else {
|
||||||
this.store.dispatch(fromMatrixViewPage.removeAccountFromList({accountId, listId}));
|
this.store.dispatch(fromMatrixViewPage.removeAccountFromList({accountId, listId}));
|
||||||
|
|||||||
@@ -1,58 +1,78 @@
|
|||||||
<nb-card>
|
<app-not-optimized-for-mobile-devices></app-not-optimized-for-mobile-devices>
|
||||||
<nb-card-header style="position: relative;">
|
<app-page>
|
||||||
<h1>Table View for Followings</h1>
|
<div title>Followings (Table View)</div>
|
||||||
<div class="divider"></div>
|
<div body>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
<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 {
|
::ng-deep .p-datatable {
|
||||||
display: block;
|
|
||||||
height: max(600px, calc(100vh - 300px));
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-x: auto;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: none;
|
overflow-x: auto;
|
||||||
|
|
||||||
th {
|
td.medium {
|
||||||
border: 1pt solid #101426;
|
max-width: calc(100vw / 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td.large {
|
||||||
border: 1pt solid #101426;
|
min-width: 320px !important;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {map, Observable} from "rxjs";
|
import {map, Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
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 {Account, List} from 'projects/mastodon-api/src/public-api';
|
||||||
|
import {MultiSelect} from "primeng/multiselect";
|
||||||
|
import {FilterService} from "primeng/api";
|
||||||
|
|
||||||
interface DataGridRow {
|
interface DataGridRow {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -22,9 +24,23 @@ interface DataGridRow {
|
|||||||
})
|
})
|
||||||
export class TableComponent {
|
export class TableComponent {
|
||||||
rows$: Observable<DataGridRow[]>;
|
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
|
this.rows$ = this.store
|
||||||
.pipe(
|
.pipe(
|
||||||
select(selectFilteredFollowingsWithLists),
|
select(selectFilteredFollowingsWithLists),
|
||||||
@@ -46,10 +62,13 @@ export class TableComponent {
|
|||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccountToSelectedList(accountId: string, listId: string) {
|
addAccountToSelectedList(accountId: string, select: MultiSelect) {
|
||||||
this.store.dispatch(fromTableViewPage.addAccountToList({accountId, listId}));
|
select.value.forEach((listId: string) => {
|
||||||
|
this.store.dispatch(fromListViewPage.addAccountToList({accountId, listId}));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
removeAccountFromList(accountId: string, listId: string) {
|
removeAccountFromList(accountId: string, listId: string) {
|
||||||
this.store.dispatch(fromTableViewPage.removeAccountFromList({accountId, listId}));
|
this.store.dispatch(fromTableViewPage.removeAccountFromList({accountId, listId}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
<nb-card>
|
<app-page>
|
||||||
<nb-card-header>
|
<div title>Welcome to Mastolists</div>
|
||||||
<h1>Welcome to Mastolists</h1>
|
<div body>
|
||||||
</nb-card-header>
|
|
||||||
<nb-card-body>
|
|
||||||
<p>Organize your Followings into lists.</p>
|
<p>Organize your Followings into lists.</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a [routerLink]="['/authorize']">Authorize</a> with your instance</li>
|
<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><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>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>
|
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nb-card-body>
|
</div>
|
||||||
</nb-card>
|
</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 {ListsComponent} from './lists/lists.component';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {ListsRoutingModule} from "./lists-routing.module";
|
import {ListsRoutingModule} from "./lists-routing.module";
|
||||||
import {NbSpinnerModule} from "@nebular/theme";
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
import {ReactiveFormsModule} from "@angular/forms";
|
import {RippleModule} from "primeng/ripple";
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -16,8 +16,8 @@ import {ReactiveFormsModule} from "@angular/forms";
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
ListsRoutingModule,
|
ListsRoutingModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
// Nebular
|
FormsModule,
|
||||||
NbSpinnerModule,
|
RippleModule,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ListsModule {
|
export class ListsModule {
|
||||||
|
|||||||
@@ -1,31 +1,83 @@
|
|||||||
<nb-card>
|
<app-page>
|
||||||
<nb-card-header>
|
<div title>Edit Lists</div>
|
||||||
<h1>Lists</h1>
|
<div body>
|
||||||
<div class="divider"></div>
|
<div [formGroup]="newListForm" class="flex flex-col md:flex-row gap-6">
|
||||||
<div [formGroup]="newListForm">
|
<input
|
||||||
<input nbInput id="title" type="text" [formControlName]="'title'" style="margin-right: 20px;">
|
id="title"
|
||||||
<button class="new-list-button" nbButton type="button"
|
type="text"
|
||||||
[nbSpinner]="creatingList"
|
pInputText
|
||||||
(click)="createList()">Create List
|
[formControlName]="'title'"
|
||||||
</button>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</nb-card-header>
|
<div class="pt-6">
|
||||||
<nb-card-body>
|
<p-table [value]="(lists$ | async)!"
|
||||||
<table *ngIf="(instanceName$ | async) as instanceName">
|
styleClass="p-datatable-striped"
|
||||||
<tr>
|
dataKey="id"
|
||||||
<th>Id</th>
|
editMode="row"
|
||||||
<th>Title</th>
|
(sortFunction)="customSort($event)"
|
||||||
</tr>
|
[customSort]="true"
|
||||||
<tr *ngFor="let list of lists$ | async">
|
>
|
||||||
<td>
|
<ng-template pTemplate="header">
|
||||||
<a [href]="'https://' + instanceName + '/lists/' + list.id" target="_blank" rel="noopener">
|
<tr>
|
||||||
{{list.id}}
|
<th pSortableColumn="id" style="width: 80px">
|
||||||
</a>
|
Id
|
||||||
</td>
|
<p-sortIcon field="id"></p-sortIcon>
|
||||||
<td>
|
</th>
|
||||||
<input nbInput id="serverUrl" type="text" [value]="list.title" (change)="listNameChanged(list.id, $event)">
|
<th pSortableColumn="title">
|
||||||
</td>
|
Title
|
||||||
</tr>
|
<p-sortIcon field="title"></p-sortIcon>
|
||||||
</table>
|
</th>
|
||||||
</nb-card-body>
|
<th></th>
|
||||||
</nb-card>
|
</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 {List} from 'projects/mastodon-api/src/public-api';
|
||||||
import {Observable} from "rxjs";
|
import {Observable} from "rxjs";
|
||||||
import {select, Store} from "@ngrx/store";
|
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 {FormControl, FormGroup} from "@angular/forms";
|
||||||
|
import {SortEvent} from "primeng/api";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-lists',
|
selector: 'app-lists',
|
||||||
@@ -11,10 +12,11 @@ import {FormControl, FormGroup} from "@angular/forms";
|
|||||||
styleUrls: ['./lists.component.scss']
|
styleUrls: ['./lists.component.scss']
|
||||||
})
|
})
|
||||||
export class ListsComponent {
|
export class ListsComponent {
|
||||||
|
loading$: Observable<boolean>;
|
||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<List[]>;
|
||||||
instanceName$: Observable<string | undefined>;
|
instanceName$: Observable<string | undefined>;
|
||||||
creatingList: boolean = false;
|
creatingList: boolean = false;
|
||||||
|
editModels: { [s: string]: List; } = {}
|
||||||
|
|
||||||
newListForm = new FormGroup({
|
newListForm = new FormGroup({
|
||||||
title: new FormControl(''),
|
title: new FormControl(''),
|
||||||
@@ -23,12 +25,39 @@ export class ListsComponent {
|
|||||||
constructor(private store: Store) {
|
constructor(private store: Store) {
|
||||||
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
this.instanceName$ = this.store.pipe(select(selectCurrentInstance));
|
||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
|
this.loading$ = this.store.pipe(select(selectLoading));
|
||||||
}
|
}
|
||||||
|
|
||||||
listNameChanged(id: string, event: Event) {
|
customSort(event: SortEvent) {
|
||||||
const target = event.target as HTMLInputElement;
|
console.log(event);
|
||||||
const newTitle = target.value;
|
if (event.field == 'id') {
|
||||||
this.store.dispatch(fromListsPage.updateList({listId: id, newTitle}));
|
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() {
|
createList() {
|
||||||
@@ -38,4 +67,8 @@ export class ListsComponent {
|
|||||||
this.newListForm.reset();
|
this.newListForm.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadLists() {
|
||||||
|
this.store.dispatch(fromSyncPage.loadLists());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,30 @@
|
|||||||
<nb-card accent="info" class="account-card">
|
<div class="p-6 bg-white dark:bg-denim-darker rounded-lg h-full">
|
||||||
<nb-card-header>
|
<div class="flex flex-col h-full divide-y justify-between">
|
||||||
<a [href]="account.url">
|
<div class="flex flex-col md:flex-row md:items-center md:space-x-4 md:pb-6">
|
||||||
<nb-user
|
<img class="w-10 h-10 rounded-full" [src]="account.avatar" alt="">
|
||||||
size="medium"
|
<div class="font-medium">
|
||||||
shape="semi-round"
|
<div>{{account.displayName}}</div>
|
||||||
[name]="account.displayName"
|
<div class="text-sm text-gray-500 dark:text-white">
|
||||||
[title]="'@' + account.acct"
|
<a [href]="account.url" rel="noreferrer" target="_blank">
|
||||||
[picture]="account.avatar"
|
@{{account.acct}}
|
||||||
>
|
</a>
|
||||||
</nb-user>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ng-content select="'[body]'"></ng-content>
|
<div class="flex-grow pb-6">
|
||||||
</nb-card-body>
|
<div *ngIf="account.note" class="pt-6">
|
||||||
<nb-card-footer>
|
<span [innerHTML]="account.note"></span>
|
||||||
<ng-content select="'[footer]'"></ng-content>
|
</div>
|
||||||
</nb-card-footer>
|
<div *ngIf="account.fields.length > 0" class="flex flex-col md:pt-6 space-y-4">
|
||||||
</nb-card>
|
<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 {
|
:host {
|
||||||
width: 100%;
|
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="container">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
<form [formGroup]="filtersForm">
|
<form [formGroup]="filtersForm">
|
||||||
@@ -18,3 +67,4 @@
|
|||||||
<button nbButton (click)="clearFilter()">Clear Filter</button>
|
<button nbButton (click)="clearFilter()">Clear Filter</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
-->
|
||||||
|
|||||||
@@ -1,46 +1,6 @@
|
|||||||
.container {
|
::ng-deep .p-multiselect {
|
||||||
display: flex;
|
width: 100%;
|
||||||
flex-direction: column;
|
@screen md {
|
||||||
}
|
width: 200px;
|
||||||
|
}
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,29 +20,49 @@ export class FiltersComponent implements OnDestroy {
|
|||||||
filtersForm = new FormGroup({
|
filtersForm = new FormGroup({
|
||||||
text: new FormControl(),
|
text: new FormControl(),
|
||||||
lists: new FormControl([] as string[]),
|
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>;
|
filters$: Observable<FiltersForForm>;
|
||||||
formSubscription: Subscription;
|
formSubscription: Subscription;
|
||||||
|
filterSubscription: Subscription;
|
||||||
|
|
||||||
constructor(private store: Store) {
|
constructor(private store: Store) {
|
||||||
this.lists$ = this.store.pipe(select(selectLists));
|
this.lists$ = this.store.pipe(select(selectLists));
|
||||||
this.filters$ = this.store.pipe(
|
this.filters$ = this.store.pipe(
|
||||||
select(selectFiltersForForm),
|
select(selectFiltersForForm),
|
||||||
tap(filters => {
|
tap(filters => {
|
||||||
|
this.currentFilter = {...filters, lists: [...filters.lists]};
|
||||||
this.filtersForm.patchValue({
|
this.filtersForm.patchValue({
|
||||||
text: filters.fullText,
|
text: filters.fullText,
|
||||||
lists: filters.lists as string[],
|
lists: filters.lists as string[],
|
||||||
|
unlisted: filters.unlisted,
|
||||||
}, {emitEvent: false, onlySelf: true});
|
}, {emitEvent: false, onlySelf: true});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
this.filterSubscription = this.filters$.subscribe();
|
||||||
this.formSubscription = this.filtersForm.valueChanges
|
this.formSubscription = this.filtersForm.valueChanges
|
||||||
.pipe(debounceTime(500))
|
.pipe(debounceTime(500))
|
||||||
.subscribe(value => {
|
.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) {
|
if (value['lists'] && value['lists'].length > 0) {
|
||||||
this.store.dispatch(fromFilters.setLists({lists: value.lists}));
|
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) {
|
if (!value['lists'] || value['lists'].length === 0) {
|
||||||
this.store.dispatch(fromFilters.setLists({lists: []}));
|
this.store.dispatch(fromFilters.setLists({lists: []}));
|
||||||
@@ -63,22 +83,20 @@ export class FiltersComponent implements OnDestroy {
|
|||||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
if (this.currentFilter.fullText && this.currentFilter.fullText.length > 0) {
|
||||||
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
this.store.dispatch(fromFilters.setUsername({username: ''}));
|
||||||
|
this.store.dispatch(fromFilters.setFreeText({freeText: []}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.formSubscription.unsubscribe();
|
this.formSubscription.unsubscribe();
|
||||||
|
this.filterSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
this.store.dispatch(fromFilters.clearFilters());
|
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>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>
|
<p>For a better experience on mobile visit the <a [routerLink]="['/followings', 'list']">list view</a></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
.only-visible-on-mobile {
|
.only-visible-on-mobile {
|
||||||
display: none;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 573px) {
|
@screen lg {
|
||||||
.only-visible-on-mobile {
|
.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 {NgModule} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {AccountComponent} from './components/account/account.component';
|
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 {FiltersComponent} from './components/filters/filters.component';
|
||||||
import {ReactiveFormsModule} from "@angular/forms";
|
import {ReactiveFormsModule} from "@angular/forms";
|
||||||
import {NotOptimizedForMobileDevicesComponent} from './components/not-optimized-for-mobile-devices/not-optimized-for-mobile-devices.component';
|
import {NotOptimizedForMobileDevicesComponent} from './components/not-optimized-for-mobile-devices/not-optimized-for-mobile-devices.component';
|
||||||
import {RouterLink} from "@angular/router";
|
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({
|
@NgModule({
|
||||||
@@ -23,36 +20,32 @@ import {RouterLink} from "@angular/router";
|
|||||||
AccountComponent,
|
AccountComponent,
|
||||||
FiltersComponent,
|
FiltersComponent,
|
||||||
NotOptimizedForMobileDevicesComponent,
|
NotOptimizedForMobileDevicesComponent,
|
||||||
|
PageComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
MessageService
|
||||||
],
|
],
|
||||||
providers: [],
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
// Nebula import only
|
|
||||||
NbInputModule,
|
|
||||||
NbButtonModule,
|
|
||||||
NbSpinnerModule,
|
|
||||||
NbSelectModule,
|
|
||||||
NbToggleModule,
|
|
||||||
// Nebula
|
|
||||||
NbCardModule,
|
|
||||||
NbUserModule,
|
|
||||||
NbActionsModule,
|
|
||||||
NbIconModule,
|
|
||||||
NbButtonModule,
|
|
||||||
NbInputModule,
|
|
||||||
RouterLink,
|
RouterLink,
|
||||||
|
ToastModule,
|
||||||
|
//PrimeNG
|
||||||
|
ButtonModule,
|
||||||
|
InputTextModule,
|
||||||
|
TableModule,
|
||||||
|
MultiSelectModule,
|
||||||
|
InputSwitchModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
AccountComponent,
|
AccountComponent,
|
||||||
FiltersComponent,
|
FiltersComponent,
|
||||||
// Nebula
|
|
||||||
NbCardModule,
|
|
||||||
NbActionsModule,
|
|
||||||
NbIconModule,
|
|
||||||
NbButtonModule,
|
|
||||||
NbInputModule,
|
|
||||||
NotOptimizedForMobileDevicesComponent,
|
NotOptimizedForMobileDevicesComponent,
|
||||||
|
PageComponent,
|
||||||
|
//PrimeNG
|
||||||
|
ButtonModule,
|
||||||
|
InputTextModule,
|
||||||
|
TableModule,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedModule {
|
export class SharedModule {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
import {createActionGroup, emptyProps, props} from "@ngrx/store";
|
||||||
|
import {Account} from "projects/mastodon-api/src/public-api";
|
||||||
|
|
||||||
export const fromAuthorize = createActionGroup({
|
export const fromAuthorize = createActionGroup({
|
||||||
source: 'Authorize',
|
source: 'Authorize',
|
||||||
@@ -9,7 +10,7 @@ export const fromAuthorize = createActionGroup({
|
|||||||
'Register Application Error': props<{ error: any }>(),
|
'Register Application Error': props<{ error: any }>(),
|
||||||
'Authorize User': props<{ instanceName: string, clientId: string, redirectUrl: string }>(),
|
'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': 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(),
|
'Get Access Token Error': emptyProps(),
|
||||||
'Logout': emptyProps(),
|
'Logout': emptyProps(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
import {Account} from "projects/mastodon-api/src/public-api";
|
||||||
|
|
||||||
export interface AuthenticationState {
|
export interface AuthenticationState {
|
||||||
registeringApplication: boolean;
|
registeringApplication: boolean;
|
||||||
applicationRegistered: boolean;
|
applicationRegistered: boolean;
|
||||||
authorizingUser: boolean;
|
authorizingUser: boolean;
|
||||||
instanceName?: string;
|
instanceName?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
accountId?: string;
|
account?: Account;
|
||||||
id?: string;
|
id?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
import {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
import {catchError, exhaustMap, map, of, switchMap, tap, zip} from "rxjs";
|
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 {List} from "projects/mastodon-api/src/public-api";
|
||||||
import {fromApplication, fromListsPage, fromListViewPage, fromMastodonApi, fromMatrixViewPage, fromSyncPage, fromTableViewPage} from "../actions";
|
import {fromApplication, fromListsPage, fromListViewPage, fromMastodonApi, fromMatrixViewPage, fromSyncPage, fromTableViewPage} from "../actions";
|
||||||
import {ListService} from "../../../services/list.service";
|
import {ListService} from "../../../services/list.service";
|
||||||
import {AccountService} from "../../../services/account.service";
|
import {AccountService} from "../../../services/account.service";
|
||||||
|
import {NlNotificationService} from "../../../services/nl-notification.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ApplicationStateEffects {
|
export class ApplicationStateEffects {
|
||||||
@@ -36,13 +36,13 @@ export class ApplicationStateEffects {
|
|||||||
updateListSuccess$ = createEffect(() =>
|
updateListSuccess$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.updateListSuccess),
|
ofType(fromMastodonApi.updateListSuccess),
|
||||||
map(() => this.toastService.success('List updated successfully', 'Success')),
|
map(() => this.notificationService.success('List updated successfully', 'success')),
|
||||||
), {dispatch: false}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
updateListError$ = createEffect(() =>
|
updateListError$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.updateListError),
|
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}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ export class ApplicationStateEffects {
|
|||||||
createListSuccess$ = createEffect(() =>
|
createListSuccess$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.createListSuccess),
|
ofType(fromMastodonApi.createListSuccess),
|
||||||
map(() => this.toastService.success('List created successfully', 'Success')),
|
map(() => this.notificationService.success('List created successfully', 'success')),
|
||||||
), {dispatch: false}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
createListError$ = createEffect(() =>
|
createListError$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.createListError),
|
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}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export class ApplicationStateEffects {
|
|||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.listsLoadedSuccess),
|
ofType(fromMastodonApi.listsLoadedSuccess),
|
||||||
switchMap((action) => {
|
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: {}}));
|
if (action.lists.length === 0) return of(fromMastodonApi.accountIdsForListsLoadedSuccess({mappings: {}}));
|
||||||
const mappingsArr = action.lists.map((list) => this.listService.loadAccountsIdsForList(list.id));
|
const mappingsArr = action.lists.map((list) => this.listService.loadAccountsIdsForList(list.id));
|
||||||
return zip(mappingsArr)
|
return zip(mappingsArr)
|
||||||
@@ -110,14 +110,14 @@ export class ApplicationStateEffects {
|
|||||||
loadFollowingsSuccess$ = createEffect(() =>
|
loadFollowingsSuccess$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.followingsLoadedSuccess),
|
ofType(fromMastodonApi.followingsLoadedSuccess),
|
||||||
tap(() => this.toastService.success('Followings loaded successfully', 'Success')),
|
tap(() => this.notificationService.success('Followings loaded successfully', 'success')),
|
||||||
)
|
)
|
||||||
, {dispatch: false});
|
, {dispatch: false});
|
||||||
|
|
||||||
loadFollowingsError$ = createEffect(() =>
|
loadFollowingsError$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.followingsLoadedError),
|
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});
|
, {dispatch: false});
|
||||||
|
|
||||||
@@ -135,14 +135,14 @@ export class ApplicationStateEffects {
|
|||||||
addAccountToListSuccess$ = createEffect(() =>
|
addAccountToListSuccess$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.addAccountToListSuccess),
|
ofType(fromMastodonApi.addAccountToListSuccess),
|
||||||
map(() => this.toastService.success('Account added to list', 'Success')),
|
map(() => this.notificationService.success('Account added to list', 'success')),
|
||||||
), {dispatch: false}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
addCountsToListError$ = createEffect(() =>
|
addCountsToListError$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.addAccountToListError),
|
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}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -159,14 +159,14 @@ export class ApplicationStateEffects {
|
|||||||
removeAccountFromListSuccess$ = createEffect(() =>
|
removeAccountFromListSuccess$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.removeAccountFromListSuccess),
|
ofType(fromMastodonApi.removeAccountFromListSuccess),
|
||||||
map(() => this.toastService.success('Account removed from list', 'Success')),
|
map(() => this.notificationService.success('Account removed from list', 'success')),
|
||||||
), {dispatch: false}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
removeAccountFromListError$ = createEffect(() =>
|
removeAccountFromListError$ = createEffect(() =>
|
||||||
this.actions$.pipe(
|
this.actions$.pipe(
|
||||||
ofType(fromMastodonApi.removeAccountFromListError),
|
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}
|
), {dispatch: false}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ export class ApplicationStateEffects {
|
|||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
private listService: ListService,
|
private listService: ListService,
|
||||||
private accountService: AccountService,
|
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 {Actions, createEffect, ofType} from "@ngrx/effects";
|
||||||
import {fromAuthorize} from "../actions";
|
import {fromAuthorize} from "../actions";
|
||||||
import {catchError, exhaustMap, map, of, switchMap} from "rxjs";
|
import {catchError, exhaustMap, map, of, switchMap} from "rxjs";
|
||||||
import {NbToastrService} from "@nebular/theme";
|
import {Account, MastodonApiAuthenticationService, RegisteredApp} from "projects/mastodon-api/src/public-api";
|
||||||
import {MastodonApiAuthenticationService, RegisteredApp} from "projects/mastodon-api/src/public-api";
|
|
||||||
import {Store} from "@ngrx/store";
|
import {Store} from "@ngrx/store";
|
||||||
import {environment} from "../../../../../environments/environment";
|
import {environment} from "../../../../../environments/environment";
|
||||||
|
import {NlNotificationService} from "../../../services/nl-notification.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthorizationStateEffects {
|
export class AuthorizationStateEffects {
|
||||||
@@ -17,7 +17,7 @@ export class AuthorizationStateEffects {
|
|||||||
.createApp(action.instanceName, environment.appName, action.redirectUrl, environment.appWebsite)
|
.createApp(action.instanceName, environment.appName, action.redirectUrl, environment.appWebsite)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((registeredApp: RegisteredApp) => {
|
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({
|
return fromAuthorize.registerApplicationSuccess({
|
||||||
id: registeredApp.id,
|
id: registeredApp.id,
|
||||||
appName: registeredApp.name,
|
appName: registeredApp.name,
|
||||||
@@ -27,7 +27,7 @@ export class AuthorizationStateEffects {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
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}));
|
return of(fromAuthorize.registerApplicationError({error}));
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -51,18 +51,18 @@ export class AuthorizationStateEffects {
|
|||||||
this.mastodonApiAuthService
|
this.mastodonApiAuthService
|
||||||
.verifyCredentials(result.action.instanceName, result.accessToken)
|
.verifyCredentials(result.action.instanceName, result.accessToken)
|
||||||
.pipe(
|
.pipe(
|
||||||
map((accountId) => {
|
map((account: Account) => {
|
||||||
return {accessToken: result.accessToken, accountId};
|
return {accessToken: result.accessToken, account};
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
map((result) => {
|
map((result) => {
|
||||||
this.toastService.success('User successfully authenticated', 'Success');
|
this.notificationService.success('User successfully authenticated', 'success');
|
||||||
return fromAuthorize.getAccessTokenSuccess({accessToken: result.accessToken, accountId: result.accountId});
|
return fromAuthorize.getAccessTokenSuccess({accessToken: result.accessToken, account: result.account});
|
||||||
}),
|
}),
|
||||||
catchError((error) => {
|
catchError((error) => {
|
||||||
console.error(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());
|
return of(fromAuthorize.getAccessTokenError());
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@@ -80,7 +80,7 @@ export class AuthorizationStateEffects {
|
|||||||
private store: Store,
|
private store: Store,
|
||||||
private actions$: Actions,
|
private actions$: Actions,
|
||||||
private mastodonApiAuthService: MastodonApiAuthenticationService,
|
private mastodonApiAuthService: MastodonApiAuthenticationService,
|
||||||
private toastService: NbToastrService,
|
private notificationService: NlNotificationService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export const authenticationStateReducer = createReducer(
|
|||||||
authorizingUser: true,
|
authorizingUser: true,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
on(fromAuthorize.getAccessTokenSuccess, (_state, {accessToken, accountId}) => {
|
on(fromAuthorize.getAccessTokenSuccess, (_state, {accessToken, account}) => {
|
||||||
return {
|
return {
|
||||||
..._state,
|
..._state,
|
||||||
accessToken,
|
accessToken,
|
||||||
accountId,
|
account,
|
||||||
authorizingUser: false,
|
authorizingUser: false,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const authenticationStateFeature = createFeatureSelector<AuthenticationSt
|
|||||||
|
|
||||||
export const selectIsLoggedIn = createSelector(
|
export const selectIsLoggedIn = createSelector(
|
||||||
authenticationStateFeature,
|
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(
|
export const selectDataForAuthorizedRequest = createSelector(
|
||||||
@@ -14,7 +14,7 @@ export const selectDataForAuthorizedRequest = createSelector(
|
|||||||
return {
|
return {
|
||||||
instanceName: state.instanceName,
|
instanceName: state.instanceName,
|
||||||
accessToken: state.accessToken,
|
accessToken: state.accessToken,
|
||||||
accountId: state.accountId,
|
accountId: state.account?.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -34,6 +34,10 @@ export const selectCurrentInstance = createSelector(
|
|||||||
authenticationStateFeature,
|
authenticationStateFeature,
|
||||||
(state: AuthenticationState) => state.instanceName
|
(state: AuthenticationState) => state.instanceName
|
||||||
)
|
)
|
||||||
|
export const selectCurrentAccount = createSelector(
|
||||||
|
authenticationStateFeature,
|
||||||
|
(state: AuthenticationState) => state.account
|
||||||
|
)
|
||||||
export const selectCurrentInstanceWithApplicationRegisteredState = createSelector(
|
export const selectCurrentInstanceWithApplicationRegisteredState = createSelector(
|
||||||
authenticationStateFeature,
|
authenticationStateFeature,
|
||||||
(state: AuthenticationState) => {
|
(state: AuthenticationState) => {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {CommonModule} from '@angular/common';
|
|||||||
import {SyncComponent} from './sync/sync.component';
|
import {SyncComponent} from './sync/sync.component';
|
||||||
import {SharedModule} from "../shared/shared.module";
|
import {SharedModule} from "../shared/shared.module";
|
||||||
import {SyncRoutingModule} from "./sync-routing.module";
|
import {SyncRoutingModule} from "./sync-routing.module";
|
||||||
import {NbCardModule, NbSpinnerModule} from "@nebular/theme";
|
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -14,9 +13,6 @@ import {NbCardModule, NbSpinnerModule} from "@nebular/theme";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
SyncRoutingModule,
|
SyncRoutingModule,
|
||||||
// Nebula
|
|
||||||
NbCardModule,
|
|
||||||
NbSpinnerModule,
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SyncModule {
|
export class SyncModule {
|
||||||
|
|||||||
@@ -1,45 +1,61 @@
|
|||||||
<nb-card>
|
<app-page>
|
||||||
<nb-card-header>
|
<div title>Sync Remote Data with your Local Storage</div>
|
||||||
<h1>Sync Remote Data with your Local Storage</h1>
|
<div body>
|
||||||
</nb-card-header>
|
<div class="mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
<nb-card-body>
|
<div class="overflow-hidden bg-white dark:bg-denim-darker shadow sm:rounded-lg">
|
||||||
<nb-card>
|
<div class="px-4 sm:px-6">
|
||||||
<nb-card-header>
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Followings in Local Store</h3>
|
||||||
Currently in Local Store
|
</div>
|
||||||
</nb-card-header>
|
<div class="px-4 sm:px-6">
|
||||||
<nb-card-body>
|
<p>Followings {{(followings$ | async)?.length}}</p>
|
||||||
<table style="width: 100%;">
|
</div>
|
||||||
<tr>
|
</div>
|
||||||
<th colspan="2">Followings</th>
|
</div>
|
||||||
</tr>
|
|
||||||
<tr>
|
<div class="mx-auto py-6 sm:px-6 lg:px-8">
|
||||||
<td>Number of followings</td>
|
<div class="overflow-hidden bg-white dark:bg-denim-darker shadow sm:rounded-lg">
|
||||||
<td>{{(followings$ | async)?.length}}</td>
|
<div class="px-4 sm:px-6">
|
||||||
</tr>
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">Number of Accounts in Lists</h3>
|
||||||
</table>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="m-10">
|
||||||
<table style="width: 100%;">
|
<p-table [value]="(lists$ | async)!" styleClass="p-datatable-striped">
|
||||||
<tr>
|
<ng-template pTemplate="header">
|
||||||
<th colspan="2">Number of Accounts in List</th>
|
<tr>
|
||||||
</tr>
|
<th pSortableColumn="title">
|
||||||
<tr *ngFor="let list of lists$ | async">
|
List
|
||||||
<td>{{list?.title}}</td>
|
<p-sortIcon field="title"></p-sortIcon>
|
||||||
<td>{{list?.accounts?.length}}</td>
|
<p-columnFilter field="title" type="text" display="menu"></p-columnFilter>
|
||||||
</tr>
|
</th>
|
||||||
</table>
|
<th pSortableColumn="accounts.length">
|
||||||
</nb-card-body>
|
Accounts
|
||||||
<nb-card-footer>
|
<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>
|
<div>
|
||||||
<button nbButton type="button"
|
<button pButton
|
||||||
[nbSpinner]="(loading$ | async)!"
|
type="button"
|
||||||
|
[loading]="(loading$ | async)!"
|
||||||
[disabled]="(loading$ | async)!"
|
[disabled]="(loading$ | async)!"
|
||||||
(click)="loadListsAndAccounts()">Get Lists and Accounts
|
(click)="loadListsAndAccounts()"
|
||||||
</button>
|
label="Get Lists and Accounts"
|
||||||
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="divider"></div>
|
<div class="flex flex-col pt-6" *ngIf="(instanceName$ | async) as instanceName">
|
||||||
|
|
||||||
<div style="display:flex; flex-direction: column;" *ngIf="(instanceName$ | async) as instanceName">
|
|
||||||
<a [href]="'https://' + instanceName + '/settings/export'" rel="noreferrer" target="_blank">
|
<a [href]="'https://' + instanceName + '/settings/export'" rel="noreferrer" target="_blank">
|
||||||
Export lists using your account settings page
|
Export lists using your account settings page
|
||||||
</a>
|
</a>
|
||||||
@@ -48,7 +64,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="(lists$ | async) && (followings$ | async)">
|
<div class="pt-6" *ngIf="(lists$ | async) && (followings$ | async)">
|
||||||
<p>Now you can:</p>
|
<p>Now you can:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Edit the lists <a [routerLink]="['/lists']">Lists</a></li>
|
<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>
|
<li>... or use the experimental <a [routerLink]="['/followings/matrix']">Matrix view</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</nb-card-footer>
|
</div>
|
||||||
</nb-card>
|
</div>
|
||||||
</nb-card-body>
|
</div>
|
||||||
</nb-card>
|
</app-page>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {select, Store} from "@ngrx/store";
|
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 {Observable} from "rxjs";
|
||||||
import {Account, List} from 'projects/mastodon-api/src/public-api';
|
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 {
|
export class SyncComponent {
|
||||||
loading$: Observable<boolean>;
|
loading$: Observable<boolean>;
|
||||||
followings$: Observable<ReadonlyArray<Account>>;
|
followings$: Observable<ReadonlyArray<Account>>;
|
||||||
lists$: Observable<ReadonlyArray<List>>;
|
lists$: Observable<List[]>;
|
||||||
instanceName$: Observable<string | undefined>;
|
instanceName$: Observable<string | undefined>;
|
||||||
|
|
||||||
constructor(private store: Store) {
|
constructor(private store: Store) {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>Mastolists</title>
|
<title>Mastolists</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
<link id="app-theme" rel="stylesheet" type="text/css" href="light.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="@angular/localize" />
|
||||||
|
|
||||||
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
|
||||||
|
|
||||||
import {AppModule} from './app/app.module';
|
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
|
@tailwind base;
|
||||||
@use 'styles/themes' as *;
|
|
||||||
|
|
||||||
// framework component styles
|
@layer base {
|
||||||
@use '@nebular/theme/styles/globals' as *;
|
button {
|
||||||
|
border: 0;
|
||||||
// install the framework styles
|
}
|
||||||
@include nb-install() {
|
body {
|
||||||
@include nb-theme-global();
|
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 {
|
@tailwind components;
|
||||||
height: 20px;
|
@tailwind utilities;
|
||||||
}
|
|
||||||
|
|
||||||
table, th, td {
|
|
||||||
border: 1px solid black;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
td, th {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user