Add visitor settings, rework logged-in dropdown (#2514)
authorRigel Kent <sendmemail@rigelk.eu>
Fri, 28 Feb 2020 12:52:21 +0000 (13:52 +0100)
committerGitHub <noreply@github.com>
Fri, 28 Feb 2020 12:52:21 +0000 (13:52 +0100)
* Add visitor settings, rework logged-in dropdown

* Make user dropdown P2P switch functional

* Fix lint

* Fix unnecessary notification when user logs out

* Simplify visitor settings code and remove unnecessary icons

* Catch parsing errors and reindent menu styles

59 files changed:
CREDITS.md
client/src/app/+about/about-instance/about-instance.component.html
client/src/app/+accounts/account-videos/account-videos.component.ts
client/src/app/+my-account/my-account-history/my-account-history.component.ts
client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.html
client/src/app/+my-account/my-account-settings/my-account-interface/my-account-interface-settings.component.ts
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html
client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts
client/src/app/+my-account/my-account.component.html
client/src/app/+my-account/my-account.module.ts
client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts
client/src/app/app.component.html
client/src/app/app.module.ts
client/src/app/core/auth/auth-user.model.ts
client/src/app/core/theme/theme.service.ts
client/src/app/menu/language-chooser.component.scss
client/src/app/menu/language-chooser.component.ts
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/menu/menu.component.ts
client/src/app/modal/quick-settings-modal.component.html [new file with mode: 0644]
client/src/app/modal/quick-settings-modal.component.scss [new file with mode: 0644]
client/src/app/modal/quick-settings-modal.component.ts [new file with mode: 0644]
client/src/app/shared/images/global-icon.component.ts
client/src/app/shared/misc/help.component.scss
client/src/app/shared/misc/storage.service.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
client/src/app/shared/users/user.model.ts
client/src/app/shared/users/user.service.ts
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/video.service.ts
client/src/app/shared/video/videos-selection.component.ts
client/src/app/videos/+video-watch/video-watch-playlist.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/app/videos/+video-watch/video-watch.module.ts
client/src/app/videos/recommendations/recommended-videos.component.html
client/src/app/videos/recommendations/recommended-videos.component.scss
client/src/app/videos/recommendations/recommended-videos.component.ts
client/src/app/videos/video-list/video-local.component.ts
client/src/app/videos/video-list/video-most-liked.component.ts
client/src/app/videos/video-list/video-recently-added.component.ts
client/src/app/videos/video-list/video-trending.component.ts
client/src/app/videos/video-list/video-user-subscriptions.component.ts
client/src/assets/images/global/video-lang.svg [new file with mode: 0644]
client/src/assets/images/menu/about.svg [deleted file]
client/src/assets/images/menu/administration.svg [deleted file]
client/src/assets/images/menu/eye-closed.svg [new file with mode: 0644]
client/src/assets/images/menu/eye.svg [new file with mode: 0644]
client/src/assets/images/menu/language.png [deleted file]
client/src/assets/images/menu/language.svg [new file with mode: 0644]
client/src/assets/images/menu/moonsun.svg [deleted file]
client/src/assets/images/menu/p2p.svg [new file with mode: 0644]
client/src/sass/application.scss
client/src/sass/bootstrap.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss
client/src/sass/primeng-custom.scss

index 4276668f9aae0af4aee0bf33f9fa9ae17aca22b3..3e69299e93c6bab609bef85b309da2613e4c350c 100644 (file)
 
  * [Robbie Pearce](https://robbiepearce.com/softies/)
  * [Fork-Awesome](https://github.com/ForkAwesome/Fork-Awesome)
- * playlist add by Material UI
+ * `playlist add` by Material UI
+ * `language` by Aaron Jin
index b712d00446a4ff059854596e3bca9fd69b77b7ef..043f633541056e99a727ffae12923aadcdf9cb06 100644 (file)
@@ -96,7 +96,7 @@
   </div>
 
   <div class="col">
-    <div i18n class="middle-title">
+    <div id="statistics" i18n class="middle-title">
       STATISTICS
     </div>
     <my-instance-statistics></my-instance-statistics>
index ac4477c182dcc7c0f1f14e7daf97f6d93b2b4019..41b27b541b2681d783280243a874c445fbe4c5c7 100644 (file)
@@ -12,6 +12,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Subscription } from 'rxjs'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-account-videos',
@@ -34,9 +36,11 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
     protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
+    protected userService: UserService,
     protected notifier: Notifier,
     protected confirmService: ConfirmService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private accountService: AccountService,
     private videoService: VideoService
   ) {
index 13607119e147f7c429697c4f10a54a6099aae6e2..5f0ccee5095b18c22aa4f8ad56a0225bfbfb3126 100644 (file)
@@ -11,6 +11,7 @@ import { ScreenService } from '@app/shared/misc/screen.service'
 import { UserHistoryService } from '@app/shared/users/user-history.service'
 import { UserService } from '@app/shared'
 import { Notifier, ServerService } from '@app/core'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-account-history',
@@ -35,6 +36,7 @@ export class MyAccountHistoryComponent extends AbstractVideoList implements OnIn
     protected userService: UserService,
     protected notifier: Notifier,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private confirmService: ConfirmService,
     private videoService: VideoService,
     private userHistoryService: UserHistoryService
index f034c6bb386b4bef33680e4482d63f9256b425ec..6f48d8f7d405b6ebd4a7e5a6cfd82622e6e98dfe 100644 (file)
@@ -12,5 +12,5 @@
     </div>
   </div>
 
-  <input type="submit" i18n-value value="Save" [disabled]="!form.valid">
+  <input *ngIf="!reactiveUpdate" type="submit" class="mt-0" i18n-value value="Save" [disabled]="!form.valid">
 </form>
index 441f89f10729f27653738e23cc42e79831dea4de..b6c17c0e3071a5d7991ebb6ce8db8f0f94c433fc 100644 (file)
@@ -1,21 +1,26 @@
-import { Component, Input, OnInit } from '@angular/core'
+import { Component, Input, OnInit, OnDestroy } from '@angular/core'
 import { Notifier, ServerService } from '@app/core'
 import { ServerConfig, UserUpdateMe } from '../../../../../../shared'
 import { AuthService } from '../../../core'
-import { FormReactive, User, UserService } from '../../../shared'
+import { FormReactive } from '../../../shared/forms/form-reactive'
+import { User, UserService } from '../../../shared/users'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
-import { Subject } from 'rxjs'
+import { Subject, Subscription } from 'rxjs'
 
 @Component({
   selector: 'my-account-interface-settings',
   templateUrl: './my-account-interface-settings.component.html',
   styleUrls: [ './my-account-interface-settings.component.scss' ]
 })
-export class MyAccountInterfaceSettingsComponent extends FormReactive implements OnInit {
+export class MyAccountInterfaceSettingsComponent extends FormReactive implements OnInit, OnDestroy {
   @Input() user: User = null
+  @Input() reactiveUpdate = false
+  @Input() notifyOnUpdate = true
   @Input() userInformationLoaded: Subject<any>
 
+  formValuesWatcher: Subscription
+
   private serverConfig: ServerConfig
 
   constructor (
@@ -48,9 +53,17 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
         this.form.patchValue({
           theme: this.user.theme
         })
+
+        if (this.reactiveUpdate) {
+          this.formValuesWatcher = this.form.valueChanges.subscribe(val => this.updateInterfaceSettings())
+        }
       })
   }
 
+  ngOnDestroy () {
+    this.formValuesWatcher?.unsubscribe()
+  }
+
   updateInterfaceSettings () {
     const theme = this.form.value['theme']
 
@@ -58,14 +71,19 @@ export class MyAccountInterfaceSettingsComponent extends FormReactive implements
       theme
     }
 
-    this.userService.updateMyProfile(details).subscribe(
-      () => {
-        this.authService.refreshUserInformation()
+    if (this.authService.isLoggedIn()) {
+      this.userService.updateMyProfile(details).subscribe(
+        () => {
+          this.authService.refreshUserInformation()
 
-        this.notifier.success(this.i18n('Interface settings updated.'))
-      },
+          if (this.notifyOnUpdate) this.notifier.success(this.i18n('Interface settings updated.'))
+        },
 
-      err => this.notifier.error(err.message)
-    )
+        err => this.notifier.error(err.message)
+      )
+    } else {
+      this.userService.updateMyAnonymousProfile(details)
+      if (this.notifyOnUpdate) this.notifier.success(this.i18n('Interface settings updated.'))
+    }
   }
 }
index 51a672734829a0355d49b977099de58329774b3b..f17829127d1ba9c6ed0a6bf6fb001bde03fc878d 100644 (file)
@@ -35,6 +35,8 @@
     </div>
   </div>
 
+  <ng-content select="inner-title"></ng-content>
+
   <div class="form-group">
     <my-peertube-checkbox
       inputName="webTorrentEnabled" formControlName="webTorrentEnabled"
@@ -56,5 +58,5 @@
     ></my-peertube-checkbox>
   </div>
 
-  <input type="submit" i18n-value value="Save" [disabled]="!form.valid">
+  <input *ngIf="!reactiveUpdate" type="submit" i18n-value value="Save" [disabled]="!form.valid">
 </form>
index a66159b3f73429e08f0d991f4ed1fa0b2da0297b..0aaa54cd737380ea381efcd5f0b6ee860d5c5f06 100644 (file)
@@ -1,24 +1,31 @@
-import { Component, Input, OnInit } from '@angular/core'
+import { Component, Input, OnInit, OnDestroy } from '@angular/core'
 import { Notifier, ServerService } from '@app/core'
-import { UserUpdateMe } from '../../../../../../shared'
+import { UserUpdateMe } from '../../../../../../shared/models/users'
+import { User, UserService } from '@app/shared/users'
 import { AuthService } from '../../../core'
-import { FormReactive, User, UserService } from '../../../shared'
+import { FormReactive } from '@app/shared/forms/form-reactive'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
-import { forkJoin, Subject } from 'rxjs'
+import { forkJoin, Subject, Subscription } from 'rxjs'
 import { SelectItem } from 'primeng/api'
 import { first } from 'rxjs/operators'
+import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
+import { pick } from 'lodash-es'
 
 @Component({
   selector: 'my-account-video-settings',
   templateUrl: './my-account-video-settings.component.html',
   styleUrls: [ './my-account-video-settings.component.scss' ]
 })
-export class MyAccountVideoSettingsComponent extends FormReactive implements OnInit {
+export class MyAccountVideoSettingsComponent extends FormReactive implements OnInit, OnDestroy {
   @Input() user: User = null
+  @Input() reactiveUpdate = false
+  @Input() notifyOnUpdate = true
   @Input() userInformationLoaded: Subject<any>
 
   languageItems: SelectItem[] = []
+  defaultNSFWPolicy: NSFWPolicyType
+  formValuesWatcher: Subscription
 
   constructor (
     protected formValidatorService: FormValidatorService,
@@ -32,6 +39,8 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
   }
 
   ngOnInit () {
+    let oldForm: any
+
     this.buildForm({
       nsfwPolicy: null,
       webTorrentEnabled: null,
@@ -42,8 +51,9 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
 
     forkJoin([
       this.serverService.getVideoLanguages(),
+      this.serverService.getConfig(),
       this.userInformationLoaded.pipe(first())
-    ]).subscribe(([ languages ]) => {
+    ]).subscribe(([ languages, config ]) => {
       this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
       this.languageItems = this.languageItems
                                .concat(languages.map(l => ({ label: l.label, value: l.id })))
@@ -52,17 +62,32 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
         ? this.user.videoLanguages
         : this.languageItems.map(l => l.value)
 
+      this.defaultNSFWPolicy = config.instance.defaultNSFWPolicy
+
       this.form.patchValue({
-        nsfwPolicy: this.user.nsfwPolicy,
+        nsfwPolicy: this.user.nsfwPolicy || this.defaultNSFWPolicy,
         webTorrentEnabled: this.user.webTorrentEnabled,
         autoPlayVideo: this.user.autoPlayVideo === true,
         autoPlayNextVideo: this.user.autoPlayNextVideo,
         videoLanguages
       })
+
+      if (this.reactiveUpdate) {
+        oldForm = { ...this.form.value }
+        this.formValuesWatcher = this.form.valueChanges.subscribe((formValue: any) => {
+          const updatedKey = Object.keys(formValue).find(k => formValue[k] !== oldForm[k])
+          oldForm = { ...this.form.value }
+          this.updateDetails([updatedKey])
+        })
+      }
     })
   }
 
-  updateDetails () {
+  ngOnDestroy () {
+    this.formValuesWatcher?.unsubscribe()
+  }
+
+  updateDetails (onlyKeys?: string[]) {
     const nsfwPolicy = this.form.value[ 'nsfwPolicy' ]
     const webTorrentEnabled = this.form.value['webTorrentEnabled']
     const autoPlayVideo = this.form.value['autoPlayVideo']
@@ -81,7 +106,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
       }
     }
 
-    const details: UserUpdateMe = {
+    let details: UserUpdateMe = {
       nsfwPolicy,
       webTorrentEnabled,
       autoPlayVideo,
@@ -89,15 +114,22 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
       videoLanguages
     }
 
-    this.userService.updateMyProfile(details).subscribe(
-      () => {
-        this.notifier.success(this.i18n('Video settings updated.'))
+    if (onlyKeys) details = pick(details, onlyKeys)
 
-        this.authService.refreshUserInformation()
-      },
+    if (this.authService.isLoggedIn()) {
+      this.userService.updateMyProfile(details).subscribe(
+        () => {
+          this.authService.refreshUserInformation()
 
-      err => this.notifier.error(err.message)
-    )
+          if (this.notifyOnUpdate) this.notifier.success(this.i18n('Video settings updated.'))
+        },
+
+        err => this.notifier.error(err.message)
+      )
+    } else {
+      this.userService.updateMyAnonymousProfile(details)
+      if (this.notifyOnUpdate) this.notifier.success(this.i18n('Display/Video settings updated.'))
+    }
   }
 
   getDefaultVideoLanguageLabel () {
index 3999252beb60fdb7668011bcadc7ee4f1f7a90de..d885eb24363cb8d5e5db0c008e46ec4291a69900 100644 (file)
@@ -1,7 +1,7 @@
 <div class="row">
   <my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
 
-  <div class="margin-content">
+  <div class="margin-content pb-5">
     <router-outlet></router-outlet>
   </div>
 </div>
index 6cf1499d336583373624dc3afe41822d418961a0..db8ffac1699391d1e6be9509aa375e1a0db881c6 100644 (file)
@@ -5,7 +5,6 @@ import { InputSwitchModule } from 'primeng/inputswitch'
 import { SharedModule } from '../shared'
 import { MyAccountRoutingModule } from './my-account-routing.module'
 import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component'
-import { MyAccountVideoSettingsComponent } from './my-account-settings/my-account-video-settings/my-account-video-settings.component'
 import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component'
 import { MyAccountComponent } from './my-account.component'
 import { MyAccountVideosComponent } from './my-account-videos/my-account-videos.component'
@@ -37,7 +36,6 @@ import {
 } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
 import { DragDropModule } from '@angular/cdk/drag-drop'
 import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
-import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
 
 @NgModule({
   imports: [
@@ -54,10 +52,8 @@ import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account
     MyAccountComponent,
     MyAccountSettingsComponent,
     MyAccountChangePasswordComponent,
-    MyAccountVideoSettingsComponent,
     MyAccountProfileComponent,
     MyAccountChangeEmailComponent,
-    MyAccountInterfaceSettingsComponent,
 
     MyAccountVideosComponent,
 
index f32a892a44dae2b2609302e14eccaf09691f92db..9eaa3ba325a924ece0948d0e9b2f2a1b7ee56ad6 100644 (file)
@@ -12,6 +12,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { Subscription } from 'rxjs'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-video-channel-videos',
@@ -34,9 +36,11 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On
     protected serverService: ServerService,
     protected route: ActivatedRoute,
     protected authService: AuthService,
+    protected userService: UserService,
     protected notifier: Notifier,
     protected confirmService: ConfirmService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoChannelService: VideoChannelService,
     private videoService: VideoService
   ) {
index 7743124d4a1e5fc035bafa9aeaa1df70d766c26e..54b320f796a81375a8c0103208b2c43f509f2682 100644 (file)
       <div class="main-row">
         <router-outlet></router-outlet>
       </div>
-
-      <footer class="row">
-        <a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer" i18n-title>Powered by PeerTube</a>&nbsp;-&nbsp;
-        <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer" i18n-title>CopyLeft 2015-2020</a>
-      </footer>
     </div>
   </div>
 </div>
index 9e220a3836d83d516ef4cc0c974e260e5af9e070..55e929e78a64cc5e7e41d74ad37eb5f0c0223d34 100644 (file)
@@ -19,6 +19,7 @@ import { WelcomeModalComponent } from '@app/modal/welcome-modal.component'
 import { InstanceConfigWarningModalComponent } from '@app/modal/instance-config-warning-modal.component'
 import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '@shared/models'
 import { APP_BASE_HREF } from '@angular/common'
+import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
 
 export function metaFactory (serverService: ServerService): MetaLoader {
   return new MetaStaticLoader({
@@ -39,6 +40,7 @@ export function metaFactory (serverService: ServerService): MetaLoader {
 
     MenuComponent,
     LanguageChooserComponent,
+    QuickSettingsModalComponent,
     AvatarNotificationComponent,
     HeaderComponent,
     SearchTypeaheadComponent,
index 1447daead20e2300804fe9fa919953a70f057a43..4ad904beb2389db37e482cf1c3e848ef91ba8f69 100644 (file)
@@ -67,17 +67,6 @@ class Tokens {
 }
 
 export class AuthUser extends User implements ServerMyUserModel {
-  private static KEYS = {
-    ID: 'id',
-    ROLE: 'role',
-    EMAIL: 'email',
-    VIDEOS_HISTORY_ENABLED: 'videos-history-enabled',
-    USERNAME: 'username',
-    NSFW_POLICY: 'nsfw_policy',
-    WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
-    AUTO_PLAY_VIDEO: 'auto_play_video'
-  }
-
   tokens: Tokens
   specialPlaylists: MyUserSpecialPlaylist[]
 
@@ -106,10 +95,6 @@ export class AuthUser extends User implements ServerMyUserModel {
     peertubeLocalStorage.removeItem(this.KEYS.USERNAME)
     peertubeLocalStorage.removeItem(this.KEYS.ID)
     peertubeLocalStorage.removeItem(this.KEYS.ROLE)
-    peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY)
-    peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED)
-    peertubeLocalStorage.removeItem(this.KEYS.VIDEOS_HISTORY_ENABLED)
-    peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO)
     peertubeLocalStorage.removeItem(this.KEYS.EMAIL)
     Tokens.flush()
   }
index 2c5873cb31bb7da654d89f20d583185536aeaaaa..3c066ca7400be456ab9bf35294ac5ebb402c2d9a 100644 (file)
@@ -4,16 +4,15 @@ import { ServerService } from '@app/core/server'
 import { environment } from '../../../environments/environment'
 import { PluginService } from '@app/core/plugins/plugin.service'
 import { ServerConfig, ServerConfigTheme } from '@shared/models'
-import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
 import { first } from 'rxjs/operators'
+import { User } from '@app/shared/users/user.model'
+import { UserService } from '@app/shared/users/user.service'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
+import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
 
 @Injectable()
 export class ThemeService {
 
-  private static KEYS = {
-    LAST_ACTIVE_THEME: 'last_active_theme'
-  }
-
   private oldThemeName: string
   private themes: ServerConfigTheme[] = []
 
@@ -24,8 +23,10 @@ export class ThemeService {
 
   constructor (
     private auth: AuthService,
+    private userService: UserService,
     private pluginService: PluginService,
-    private server: ServerService
+    private server: ServerService,
+    private localStorageService: LocalStorageService
   ) {}
 
   initialize () {
@@ -77,11 +78,11 @@ export class ThemeService {
   private getCurrentTheme () {
     if (this.themeFromLocalStorage) return this.themeFromLocalStorage.name
 
-    if (this.auth.isLoggedIn()) {
-      const theme = this.auth.getUser().theme
-      if (theme !== 'instance-default') return theme
-    }
+    const theme = this.auth.isLoggedIn()
+      ? this.auth.getUser().theme
+      : this.userService.getAnonymousUser().theme
 
+    if (theme !== 'instance-default') return theme
     return this.serverConfig.theme.default
   }
 
@@ -111,9 +112,9 @@ export class ThemeService {
 
       this.pluginService.reloadLoadedScopes()
 
-      peertubeLocalStorage.setItem(ThemeService.KEYS.LAST_ACTIVE_THEME, JSON.stringify(theme))
+      this.localStorageService.setItem(User.KEYS.THEME, JSON.stringify(theme), false)
     } else {
-      peertubeLocalStorage.removeItem(ThemeService.KEYS.LAST_ACTIVE_THEME)
+      this.localStorageService.removeItem(User.KEYS.THEME, false)
     }
 
     this.oldThemeName = currentTheme
@@ -126,6 +127,10 @@ export class ThemeService {
 
     if (!this.auth.isLoggedIn()) {
       this.updateCurrentTheme()
+
+      this.localStorageService.watch([User.KEYS.THEME]).subscribe(
+        () => this.updateCurrentTheme()
+      )
     }
 
     this.auth.userInformationLoaded
@@ -134,7 +139,7 @@ export class ThemeService {
   }
 
   private loadAndSetFromLocalStorage () {
-    const lastActiveThemeString = peertubeLocalStorage.getItem(ThemeService.KEYS.LAST_ACTIVE_THEME)
+    const lastActiveThemeString = this.localStorageService.getItem(User.KEYS.THEME)
     if (!lastActiveThemeString) return
 
     try {
index 72deb3952e431e09ff994c694ac2bb4eff9d380a..50d19fd1fc09b51e0997ed9feab537288e51d0e5 100644 (file)
@@ -4,6 +4,8 @@
 .help-to-translate {
   @include peertube-button-link;
   @include orange-button;
+
+  border-radius: 0;
 }
 
 .modal-body {
index 43f622dfb1baf5f168de652c1358bc75d6b3a6be..fb74cdf19f2ff75219da0ea3ed55702440bc90a2 100644 (file)
@@ -1,7 +1,9 @@
-import { Component, ElementRef, ViewChild } from '@angular/core'
+import { Component, ElementRef, ViewChild, Inject, LOCALE_ID } from '@angular/core'
 import { I18N_LOCALES } from '../../../../shared'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { sortBy } from '@app/shared/misc/utils'
+import { getCompleteLocale } from '@shared/models/i18n'
+import { isOnDevLocale, getDevLocale } from '@app/shared/i18n/i18n-utils'
 
 @Component({
   selector: 'my-language-chooser',
@@ -13,7 +15,10 @@ export class LanguageChooserComponent {
 
   languages: { id: string, label: string }[] = []
 
-  constructor (private modalService: NgbModal) {
+  constructor (
+    private modalService: NgbModal,
+    @Inject(LOCALE_ID) private localeId: string
+  ) {
     const l = Object.keys(I18N_LOCALES)
                     .map(k => ({ id: k, label: I18N_LOCALES[k] }))
 
@@ -28,4 +33,10 @@ export class LanguageChooserComponent {
     return window.location.origin + '/' + lang.id
   }
 
+  getCurrentLanguage () {
+    const english = 'English'
+    const locale = isOnDevLocale() ? getDevLocale() : getCompleteLocale(this.localeId)
+    if (locale) return I18N_LOCALES[locale] || english
+    return english
+  }
 }
index 790a8af00b3245e5125a909846080b5e51e23ff6..399350616bf1b4820eed3fc4715e8b849c456e2b 100644 (file)
           <div class="logged-in-username">{{ user.username }}</div>
         </div>
 
-        <div class="logged-in-more" ngbDropdown placement="bottom-right auto" container="body">
+        <div class="logged-in-more" ngbDropdown placement="right-top auto" container="body" autoClose="outside">
           <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button"></my-global-icon>
 
           <div ngbDropdownMenu>
-            <a *ngIf="user.account" [routerLink]="[ '/accounts', user.account.nameWithHost ]" class="dropdown-item">
+            <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]">
               <my-global-icon iconName="go"></my-global-icon> <ng-container i18n>Public profile</ng-container>
             </a>
 
             <div class="dropdown-divider"></div>
 
-            <a routerLink="/my-account" class="dropdown-item">
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account">
               <my-global-icon iconName="user"></my-global-icon> <ng-container i18n>Account settings</ng-container>
             </a>
 
-            <a routerLink="/my-account/video-channels" class="dropdown-item">
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account/video-channels">
               <my-global-icon iconName="folder"></my-global-icon> <ng-container i18n>Channels settings</ng-container>
             </a>
 
             <div class="dropdown-divider"></div>
 
-            <a class="dropdown-item" href="https://joinpeertube.org/help" target="_blank" rel="noopener noreferrer">
-              <my-global-icon iconName="help"></my-global-icon> <ng-container i18n>Help</ng-container>
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openLanguageChooser()">
+              <my-global-icon iconName="language"></my-global-icon>
+              <ng-container i18n>Interface: {{ language }}</ng-container>
+              <i class="ml-auto glyphicon glyphicon-menu-right"></i>
             </a>
 
-            <a (click)="logout($event)" class="dropdown-item" href="#">
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account">
+              <my-global-icon iconName="video-lang"></my-global-icon>
+              <ng-container i18n>Videos: {{ videoLanguages.join(', ') }}</ng-container>
+              <i class="ml-auto glyphicon glyphicon-menu-right"></i>
+            </a>
+
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account">
+              <my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy === 'display' }" iconName="sensitive"></my-global-icon>
+              <my-global-icon class="hover-display-toggle" [ngClass]="{ 'not-displayed': user.nsfwPolicy !== 'display' }" iconName="unsensitive"></my-global-icon>
+              <ng-container i18n>Sensitive: {{ nsfwPolicy }}</ng-container>
+              <i class="ml-auto glyphicon glyphicon-menu-right"></i>
+            </a>
+
+            <a ngbDropdownItem class="dropdown-item" (click)="toggleUseP2P()">
+              <my-global-icon iconName="p2p"></my-global-icon>
+              <ng-container i18n>Help share videos</ng-container>
+              <input type="checkbox" [checked]="user.webTorrentEnabled"/><label class="ml-auto" for="switch">Toggle p2p</label>
+            </a>
+
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" routerLink="/my-account">
+              <my-global-icon iconName="more-horizontal"></my-global-icon> <ng-container i18n>More account settings</ng-container>
+            </a>
+
+            <div class="dropdown-divider"></div>
+
+            <a ngbDropdownItem ngbDropdownToggle class="dropdown-item" (click)="openHotkeysCheatSheet()">
+              <i class="icon icon-shortcuts"></i> <ng-container i18n>Keyboard shortcuts</ng-container>
+            </a>
+
+            <a ngbDropdownItem ngbDropdownToggle (click)="logout($event)" class="dropdown-item" href="#">
               <my-global-icon iconName="sign-out"></my-global-icon> <ng-container i18n>Log out</ng-container>
             </a>
           </div>
           <ng-container i18n>Local</ng-container>
         </a>
       </div>
+    </div>
 
+    <div class="footer">
       <div class="panel-block">
-        <div class="block-title" i18n>MORE</div>
-
         <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
-          <my-global-icon iconName="administration"></my-global-icon>
+          <my-global-icon iconName="cog"></my-global-icon>
           <ng-container i18n>Administration</ng-container>
         </a>
-
-        <a routerLink="/about" routerLinkActive="active">
-          <my-global-icon iconName="about"></my-global-icon>
+        <a *ngIf="!isLoggedIn" (click)="openQuickSettings()">
+          <my-global-icon iconName="cog"></my-global-icon>
+          <ng-container i18n>Settings</ng-container>
+        </a>
+        <a routerLink="/about/instance">
+          <my-global-icon iconName="help"></my-global-icon>
           <ng-container i18n>About</ng-container>
         </a>
       </div>
-    </div>
-
-    <div class="footer d-flex justify-content-between">
-      <span class="language">
-        <span tabindex="0" role="button" (keyup.enter)="openLanguageChooser()" (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
-      </span>
 
-      <span class="shortcuts">
-        <span tabindex="0" role="button" (keyup.enter)="openHotkeysCheatSheet()" (click)="openHotkeysCheatSheet()" i18n-title title="Show keyboard shortcuts" class="icon icon-shortcuts"></span>
-      </span>
+      <div class="d-flex flex-column">
+        <div class="footer-links">
+          <a i18n routerLink="/about/instance">Contact</a>
+          <a i18n routerLink="/about/instance">Terms of Service</a>
+          <a i18n routerLink="/about/instance" fragment="statistics">Stats</a>
+          <a (click)="openLanguageChooser()" class="c-hand">
+            <span i18n>Interface: {{ language }}</span>
+          </a>
+        </div>
+        <div class="footer-links">
+          <a i18n href="https://joinpeertube.org/#you-are-a-video-maker" i18n-title title="Creator guide" target="_blank" rel="noopener noreferrer">Creators</a>
+          <a i18n href="https://docs.joinpeertube.org/#/contribute-getting-started" i18n-title title="PeerTube license" target="_blank" rel="noopener noreferrer">Contributors</a>
+          <a i18n routerLink="/about/peertube" i18n-title title="More information about privacy within PeerTube">Privacy</a>
+          <a i18n href="https://joinpeertube.org/faq" i18n-title title="Frequently asked questions about PeerTube" target="_blank" rel="noopener noreferrer">FAQ</a>
+          <a i18n href="https://docs.joinpeertube.org/api-rest-reference.html" i18n-title title="API documentation" target="_blank" rel="noopener noreferrer">API</a>
+          <a i18n href="https://joinpeertube.org/help" i18n-title title="Get help using PeerTube" target="_blank" rel="noopener noreferrer">Help</a>
+          <a (click)="openHotkeysCheatSheet()" class="c-hand" i18n>Shortcuts</a>
+        </div>
+        <div class="footer-copyleft">
+          <small class="d-inline" i18n-title title="powered by PeerTube - CopyLeft 2015-2020">
+            <a href="https://joinpeertube.org" i18n-title title="PeerTube website" target="_blank" rel="noopener noreferrer" i18n>
+              powered by PeerTube
+            </a>
+            <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" i18n-title title="PeerTube license" target="_blank" rel="noopener noreferrer">
+              <span aria-label="copyleft" class="d-inline-block" style="transform: rotateY(180deg)">&copy;</span> 2015-2020
+            </a>
+          </small>
+        </div>
+      </div>
     </div>
   </menu>
 </div>
 
 <my-language-chooser #languageChooserModal></my-language-chooser>
+<my-quick-settings #quickSettingsModal></my-quick-settings>
index 40c9a2b251a4e2330f81553bc67eee590319a5de..a4b1ec000bd41a0da273d5524302e2eea84594ce 100644 (file)
@@ -29,7 +29,7 @@ menu {
 
   &.logged-in {
     .panel-block {
-      margin-bottom: 25px;
+      margin-bottom: 20px;
     }
 
     .block-title {
@@ -88,22 +88,6 @@ menu {
           @include apply-svg-color(var(--menuForegroundColor));
         }
       }
-
-      .dropdown-item {
-        @include dropdown-with-icon-item;
-
-        my-global-icon {
-          width: 22px;
-          height: 22px;
-
-          &[iconName="sign-out"] {
-            position: relative;
-            right: -1px;
-            height: 21px;
-            width: 21px;
-          }
-        }
-      }
     }
   }
 
@@ -143,7 +127,7 @@ menu {
   }
 
   .panel-block {
-    margin-bottom: 45px;
+    margin-bottom: 15px;
 
     a {
       @include disable-default-a-behaviour;
@@ -198,60 +182,162 @@ menu {
   }
 
   .footer {
-    padding-bottom: 15px;
-    padding-left: $menu-lateral-padding;
-    padding-right: $menu-lateral-padding;
     width: $menu-width;
+    padding-bottom: 15px;
 
-    .language, .shortcuts, .color-palette {
-      display: inline-block;
-      color: $menu-bottom-color;
-      cursor: pointer;
-      font-size: 12px;
-      font-weight: $font-semibold;
+    & > div:not(.panel-block) {
+      padding-left: $menu-lateral-padding;
+      padding-right: $menu-lateral-padding;
+      row-gap: 1em;
+    }
 
-      .icon {
-        @include disable-outline;
-        @include icon(28px);
-        opacity: 0.9;
+    $footer-links-base-opacity: .8;
 
-        &.icon-language  {
-          position: relative;
-          top: -1px;
-          width: 28px;
-          height: 24px;
+    .footer-links {
+      display: inline-flex;
+      flex-wrap: wrap;
+
+      & > a {
+        @include disable-default-a-behaviour;
 
-          background-image: url('../../assets/images/menu/language.png');
+        display: inline-block;
+        text-decoration: none;
+        color: var(--mainBackgroundColor);
+        opacity: $footer-links-base-opacity;
+        white-space: nowrap;
+        font-size: 90%;
+        font-weight: 500;
+        line-height: 1.4rem;
+        margin-right: 8px;
+
+        &.inline-global-icon {
+          display: inline-flex;
+          align-items: center;
+          white-space: nowrap;
+          height: 1.4rem;
+
+          my-global-icon {
+            @include apply-svg-color(var(--mainBackgroundColor));
+        
+            display: flex;
+            width: auto;
+            height: 90%;
+            margin-right: .2rem;
+          }
         }
+      }
+    }
 
-        &.icon-shortcuts  {
-          position: relative;
-          top: -1px;
-          width: 24px;
-          height: 24px;
+    .footer-copyleft small a {
+      @include disable-default-a-behaviour;
 
-          background-image: url('../../assets/images/menu/keyboard.png');
-          filter: invert(100%);
-        }
+      color: var(--mainBackgroundColor);
+      opacity: $footer-links-base-opacity - .2;
+    }
+  }
+}
 
-        &.icon-moonsun  {
-          margin-left: 10px;
-          position: relative;
-          top: -1px;
-          width: 24px;
-          height: 24px;
+.dropdown-menu {
+  width: calc(100% + 40px);
+}
 
-          background-image: url('../../assets/images/menu/moonsun.svg');
-        }
+.dropdown-item {
+  @include dropdown-with-icon-item;
 
-        &:hover {
-          opacity: 1;
-        }
-      }
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+
+  i.glyphicon-menu-right {
+    opacity: .4;
+  }
+
+  my-global-icon {
+    &[iconName="cog"],
+    &[iconName="sign-out"] {
+      position: relative;
+      right: -2px;
+      height: 20px;
+      width: 20px;
+    }
+  }
+
+  my-global-icon.not-displayed {
+    display: none;
+  }
+
+  &:hover {
+    my-global-icon.hover-display-toggle.not-displayed {
+      display: inherit;
+    }
+    my-global-icon.hover-display-toggle {
+      display: none;
     }
   }
 }
 
+.more-settings {
+  text-transform: uppercase;
+  font-size: 80%;
+  color: #6c757d;
+}
+
+.icon {
+  @include disable-outline;
+  @include icon(22px);
+  opacity: 0.8;
+
+  &.icon-shortcuts  {
+    position: relative;
+    top: -1px;
+    margin-right: 10px;
+
+    background-image: url('../../assets/images/menu/keyboard.png');
+  }
+}
+
+input[type=checkbox]{
+  position: absolute;
+  visibility: hidden;
+}
+
+label {
+  cursor: pointer;
+  text-indent: -9999px;
+  width: 35px;
+  height: 20px;
+  background: #cccccc;
+  display: block;
+  border-radius: 100px;
+  position: relative;
+  margin: 0;
+
+  &:after {
+    content: '';
+    position: absolute;
+    top: 3px;
+    left: 3px;
+    width: 14px;
+    height: 14px;
+    background: var(--mainBackgroundColor);
+    border-radius: 50%;
+    transition: 0.3s ease-out;
+  }
+
+  &:active:after {
+    width: 40px;
+  }
+}
+
+input:checked + label {
+  background: var(--mainColor);
+
+  &:after {
+    left: calc(100% - 3px);
+    transform: translateX(-100%);
+  }
+}
+
 @media screen and (max-width: $mobile-view) {
   .menu-wrapper {
     width: 100% !important;
index 1d7651e78f4f99b157b8cd0bf1abfd780de8045e..5f3dfc52a599acd87ebfb5faecd751090405326b 100644 (file)
@@ -1,10 +1,13 @@
 import { Component, OnInit, ViewChild } from '@angular/core'
 import { UserRight } from '../../../../shared/models/users/user-right.enum'
-import { AuthService, AuthStatus, RedirectService, ServerService, ThemeService } from '../core'
-import { User } from '../shared/users/user.model'
+import { AuthService, AuthStatus, RedirectService, ServerService } from '../core'
+import { User } from '@app/shared/users/user.model'
+import { UserService } from '@app/shared/users/user.service'
 import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
 import { HotkeysService } from 'angular2-hotkeys'
-import { ServerConfig } from '@shared/models'
+import { ServerConfig, VideoConstant } from '@shared/models'
+import { QuickSettingsModalComponent } from '@app/modal/quick-settings-modal.component'
+import { I18n } from '@ngx-translate/i18n-polyfill'
 
 @Component({
   selector: 'my-menu',
@@ -13,11 +16,14 @@ import { ServerConfig } from '@shared/models'
 })
 export class MenuComponent implements OnInit {
   @ViewChild('languageChooserModal', { static: true }) languageChooserModal: LanguageChooserComponent
+  @ViewChild('quickSettingsModal', { static: true }) quickSettingsModal: QuickSettingsModalComponent
 
   user: User
   isLoggedIn: boolean
+
   userHasAdminAccess = false
   helpVisible = false
+  languages: VideoConstant<string>[] = []
 
   private serverConfig: ServerConfig
   private routesPerRight: { [ role in UserRight ]?: string } = {
@@ -31,9 +37,11 @@ export class MenuComponent implements OnInit {
 
   constructor (
     private authService: AuthService,
+    private userService: UserService,
     private serverService: ServerService,
     private redirectService: RedirectService,
-    private hotkeysService: HotkeysService
+    private hotkeysService: HotkeysService,
+    private i18n: I18n
   ) {}
 
   ngOnInit () {
@@ -63,9 +71,33 @@ export class MenuComponent implements OnInit {
       }
     )
 
-    this.hotkeysService.cheatSheetToggle.subscribe(isOpen => {
-      this.helpVisible = isOpen
-    })
+    this.hotkeysService.cheatSheetToggle.subscribe(isOpen => this.helpVisible = isOpen)
+
+    this.serverService.getVideoLanguages().subscribe(languages => this.languages = languages)
+  }
+
+  get language () {
+    return this.languageChooserModal.getCurrentLanguage()
+  }
+
+  get videoLanguages (): string[] {
+    if (!this.user) return
+    if (!this.user.videoLanguages) return [this.i18n('any language')]
+    return this.user.videoLanguages
+      .map(locale => this.langForLocale(locale))
+      .map(value => value === undefined ? '?' : value)
+  }
+
+  get nsfwPolicy () {
+    if (!this.user) return
+    switch (this.user.nsfwPolicy) {
+      case 'do_not_list':
+        return this.i18n('hide')
+      case 'blur':
+        return this.i18n('blur')
+      case 'display':
+        return this.i18n('display')
+    }
   }
 
   isRegistrationAllowed () {
@@ -117,6 +149,22 @@ export class MenuComponent implements OnInit {
     this.hotkeysService.cheatSheetToggle.next(!this.helpVisible)
   }
 
+  openQuickSettings () {
+    this.quickSettingsModal.show()
+  }
+
+  toggleUseP2P () {
+    if (!this.user) return
+    this.user.webTorrentEnabled = !this.user.webTorrentEnabled
+    this.userService.updateMyProfile({
+      webTorrentEnabled: this.user.webTorrentEnabled
+    }).subscribe(() => this.authService.refreshUserInformation())
+  }
+
+  langForLocale(localeId: string) {
+    return this.languages.find(lang => lang.id = localeId).label
+  }
+
   private computeIsUserHasAdminAccess () {
     const right = this.getFirstAdminRightAvailable()
 
diff --git a/client/src/app/modal/quick-settings-modal.component.html b/client/src/app/modal/quick-settings-modal.component.html
new file mode 100644 (file)
index 0000000..8ee83e7
--- /dev/null
@@ -0,0 +1,19 @@
+<ng-template #modal let-hide="close">
+  <div class="modal-header">
+    <h4 i18n class="modal-title">Settings</h4>
+  </div>
+  
+  <div class="modal-body">
+    <div i18n class="mb-4 quick-settings-title">Display settings</div>
+
+    <my-account-video-settings *ngIf="!isUserLoggedIn()" [user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true">
+      <ng-container ngProjectAs="inner-title">
+        <div i18n class="mb-4 mt-4 quick-settings-title">Video settings</div>
+      </ng-container>
+    </my-account-video-settings>
+
+    <div i18n class="mb-4 mt-4 quick-settings-title">Interface settings</div>
+
+    <my-account-interface-settings *ngIf="!isUserLoggedIn()" [user]="user" [userInformationLoaded]="userInformationLoaded" [reactiveUpdate]="true" [notifyOnUpdate]="true"></my-account-interface-settings>
+  </div>
+</ng-template>
diff --git a/client/src/app/modal/quick-settings-modal.component.scss b/client/src/app/modal/quick-settings-modal.component.scss
new file mode 100644 (file)
index 0000000..ef21542
--- /dev/null
@@ -0,0 +1,39 @@
+@import '_mixins';
+
+.modal-button {
+  @include disable-default-a-behaviour;
+  transform: translateY(2px);
+
+  button {
+    @include peertube-button;
+    @include grey-button;
+    @include button-with-icon(18px, 4px, -1px);
+
+    my-global-icon {
+      @include apply-svg-color(#585858);
+    }
+  }
+
+  & + .modal-button {
+    margin-left: 1rem;
+  }
+}
+
+.icon {
+  @include disable-outline;
+  @include icon(22px);
+  opacity: 0.6;
+  margin-left: -1px;
+
+  &.icon-shortcuts  {
+    position: relative;
+    top: -1px;
+    margin-right: 4px;
+
+    background-image: url('../../assets/images/menu/keyboard.png');
+  }
+}
+
+.quick-settings-title {
+  @include in-content-small-title;
+}
\ No newline at end of file
diff --git a/client/src/app/modal/quick-settings-modal.component.ts b/client/src/app/modal/quick-settings-modal.component.ts
new file mode 100644 (file)
index 0000000..41d6c9f
--- /dev/null
@@ -0,0 +1,62 @@
+import { Component, ViewChild, OnInit } from '@angular/core'
+import { AuthService, AuthStatus } from '@app/core'
+import { FormReactive, FormValidatorService, UserService, User } from '@app/shared'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
+import { ReplaySubject } from 'rxjs'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
+import { filter } from 'rxjs/operators'
+
+@Component({
+  selector: 'my-quick-settings',
+  templateUrl: './quick-settings-modal.component.html',
+  styleUrls: [ './quick-settings-modal.component.scss' ]
+})
+export class QuickSettingsModalComponent extends FormReactive implements OnInit {
+  @ViewChild('modal', { static: true }) modal: NgbModal
+
+  user: User
+  userInformationLoaded = new ReplaySubject<boolean>(1)
+
+  private openedModal: NgbModalRef
+
+  constructor (
+    protected formValidatorService: FormValidatorService,
+    private modalService: NgbModal,
+    private userService: UserService,
+    private authService: AuthService,
+    private localStorageService: LocalStorageService
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    this.user = this.userService.getAnonymousUser()
+    this.localStorageService.watch().subscribe(
+      () => this.user = this.userService.getAnonymousUser()
+    )
+    this.userInformationLoaded.next(true)
+
+    this.authService.loginChangedSource
+      .pipe(filter(status => status !== AuthStatus.LoggedIn))
+      .subscribe(
+        () => {
+          this.user = this.userService.getAnonymousUser()
+          this.userInformationLoaded.next(true)
+        }
+      )
+  }
+
+  isUserLoggedIn () {
+    return this.authService.isLoggedIn()
+  }
+
+  show () {
+    this.openedModal = this.modalService.open(this.modal, { centered: true })
+  }
+
+  hide () {
+    this.openedModal.close()
+    this.form.reset()
+  }
+}
index b6e641228ad8e727f130d0f670196747e5c02685..e83daf077cf72ad4dac93bf1de74ea8060fef1ce 100644 (file)
@@ -39,15 +39,18 @@ const icons = {
   'playlist-add': require('!!raw-loader?!../../../assets/images/video/playlist-add.svg'),
   'play': require('!!raw-loader?!../../../assets/images/global/play.svg'),
   'playlists': require('!!raw-loader?!../../../assets/images/global/playlists.svg'),
-  'about': require('!!raw-loader?!../../../assets/images/menu/about.svg'),
   'globe': require('!!raw-loader?!../../../assets/images/menu/globe.svg'),
   'home': require('!!raw-loader?!../../../assets/images/menu/home.svg'),
   'recently-added': require('!!raw-loader?!../../../assets/images/menu/recently-added.svg'),
   'trending': require('!!raw-loader?!../../../assets/images/menu/trending.svg'),
+  'video-lang': require('!!raw-loader?!../../../assets/images/global/video-lang.svg'),
   'videos': require('!!raw-loader?!../../../assets/images/global/videos.svg'),
   'folder': require('!!raw-loader?!../../../assets/images/global/folder.svg'),
-  'administration': require('!!raw-loader?!../../../assets/images/menu/administration.svg'),
   'subscriptions': require('!!raw-loader?!../../../assets/images/menu/subscriptions.svg'),
+  'language': require('!!raw-loader?!../../../assets/images/menu/language.svg'),
+  'unsensitive': require('!!raw-loader?!../../../assets/images/menu/eye.svg'),
+  'sensitive': require('!!raw-loader?!../../../assets/images/menu/eye-closed.svg'),
+  'p2p': require('!!raw-loader?!../../../assets/images/menu/p2p.svg'),
   'users': require('!!raw-loader?!../../../assets/images/global/users.svg'),
   'search': require('!!raw-loader?!../../../assets/images/global/search.svg'),
   'refresh': require('!!raw-loader?!../../../assets/images/global/refresh.svg')
index f55a516e4730651f04679d0a350ad543ae1f859b..f00df88b5f71aa57032acd9b89ab9a5763a58c9a 100644 (file)
@@ -17,6 +17,7 @@
 
 ::ng-deep {
   .help-popover {
+    z-index: z(help-popover) !important;
     max-width: 300px;
 
     .popover-body {
diff --git a/client/src/app/shared/misc/storage.service.ts b/client/src/app/shared/misc/storage.service.ts
new file mode 100644 (file)
index 0000000..0d4a8ab
--- /dev/null
@@ -0,0 +1,40 @@
+import { Injectable } from '@angular/core'
+import { Observable, Subject } from 'rxjs'
+import {
+  peertubeLocalStorage,
+  peertubeSessionStorage
+} from './peertube-web-storage'
+import { filter } from 'rxjs/operators'
+
+abstract class StorageService {
+  protected instance: Storage
+  static storageSub = new Subject<string>()
+
+  watch (keys?: string[]): Observable<string> {
+    return StorageService.storageSub.asObservable().pipe(filter(val => keys ? keys.includes(val) : true))
+  }
+
+  getItem (key: string) {
+    return this.instance.getItem(key)
+  }
+
+  setItem (key: string, data: any, notifyOfUpdate = true) {
+    this.instance.setItem(key, data)
+    if (notifyOfUpdate) StorageService.storageSub.next(key)
+  }
+
+  removeItem (key: string, notifyOfUpdate = true) {
+    this.instance.removeItem(key)
+    if (notifyOfUpdate) StorageService.storageSub.next(key)
+  }
+}
+
+@Injectable()
+export class LocalStorageService extends StorageService {
+  protected instance: Storage = peertubeLocalStorage
+}
+
+@Injectable()
+export class SessionStorageService extends StorageService {
+  protected instance: Storage = peertubeSessionStorage
+}
index 30b3ba0c12761c804ccdb19aa55ba0f434b04f8f..75aa30dab7acca52c842c91f9cc340db0b5115bb 100644 (file)
@@ -47,6 +47,7 @@ import {
 import { I18nPrimengCalendarService } from '@app/shared/i18n/i18n-primeng-calendar'
 import { InputMaskModule } from 'primeng/inputmask'
 import { ScreenService } from '@app/shared/misc/screen.service'
+import { LocalStorageService, SessionStorageService } from '@app/shared/misc/storage.service'
 import { VideoCaptionsValidatorsService } from '@app/shared/forms/form-validators/video-captions-validators.service'
 import { VideoCaptionService } from '@app/shared/video-caption'
 import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.component'
@@ -101,6 +102,10 @@ import { FeatureBooleanComponent } from '@app/shared/instance/feature-boolean.co
 import { InputReadonlyCopyComponent } from '@app/shared/forms/input-readonly-copy.component'
 import { RedundancyService } from '@app/shared/video/redundancy.service'
 import { ClipboardModule } from '@angular/cdk/clipboard'
+import { InputSwitchModule } from 'primeng/inputswitch'
+
+import { MyAccountVideoSettingsComponent } from '@app/+my-account/my-account-settings/my-account-video-settings'
+import { MyAccountInterfaceSettingsComponent } from '@app/+my-account/my-account-settings/my-account-interface'
 
 @NgModule({
   imports: [
@@ -122,7 +127,8 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
     PrimeSharedModule,
     InputMaskModule,
     NgPipesModule,
-    MultiSelectModule
+    MultiSelectModule,
+    InputSwitchModule
   ],
 
   declarations: [
@@ -180,7 +186,10 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
     DateToggleComponent,
 
     GlobalIconComponent,
-    PreviewUploadComponent
+    PreviewUploadComponent,
+
+    MyAccountVideoSettingsComponent,
+    MyAccountInterfaceSettingsComponent
   ],
 
   exports: [
@@ -258,7 +267,10 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
     FromNowPipe,
     HighlightPipe,
     PeerTubeTemplateDirective,
-    VideoDurationPipe
+    VideoDurationPipe,
+
+    MyAccountVideoSettingsComponent,
+    MyAccountInterfaceSettingsComponent
   ],
 
   providers: [
@@ -303,6 +315,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'
 
     I18nPrimengCalendarService,
     ScreenService,
+    LocalStorageService, SessionStorageService,
 
     UserNotificationService,
 
index 7707d7dda7ecc73bda9d2b7ec485de0b7c224b85..a37cae749c1e64d596111c4a680651e1cb09ed8e 100644 (file)
@@ -1,10 +1,32 @@
-import { hasUserRight, User as UserServerModel, UserNotificationSetting, UserRight, UserRole, VideoChannel } from '../../../../../shared'
+import {
+  hasUserRight,
+  User as UserServerModel,
+  UserNotificationSetting,
+  UserRight,
+  UserRole
+} from '../../../../../shared/models/users'
+import { VideoChannel } from '../../../../../shared/models/videos'
 import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
 import { Account } from '@app/shared/account/account.model'
 import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
 import { UserAdminFlag } from '@shared/models/users/user-flag.model'
 
 export class User implements UserServerModel {
+  static KEYS = {
+    ID: 'id',
+    ROLE: 'role',
+    EMAIL: 'email',
+    VIDEOS_HISTORY_ENABLED: 'videos-history-enabled',
+    USERNAME: 'username',
+    NSFW_POLICY: 'nsfw_policy',
+    WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled',
+    AUTO_PLAY_VIDEO: 'auto_play_video',
+    SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO: 'auto_play_next_video',
+    AUTO_PLAY_VIDEO_PLAYLIST: 'auto_play_video_playlist',
+    THEME: 'last_active_theme',
+    VIDEO_LANGUAGES: 'video_languages'
+  }
+
   id: number
   username: string
   email: string
@@ -60,8 +82,11 @@ export class User implements UserServerModel {
 
     this.nsfwPolicy = hash.nsfwPolicy
     this.webTorrentEnabled = hash.webTorrentEnabled
-    this.videosHistoryEnabled = hash.videosHistoryEnabled
     this.autoPlayVideo = hash.autoPlayVideo
+    this.autoPlayNextVideo = hash.autoPlayNextVideo
+    this.autoPlayNextVideoPlaylist = hash.autoPlayNextVideoPlaylist
+    this.videosHistoryEnabled = hash.videosHistoryEnabled
+    this.videoLanguages = hash.videoLanguages
 
     this.theme = hash.theme
 
index e24d91df3fcac9b814bc5135034d3acd81e483ee..a7934364686476d95f1a7f834616add83874fcaa 100644 (file)
@@ -1,8 +1,8 @@
-import { from, Observable, of } from 'rxjs'
-import { catchError, concatMap, map, share, shareReplay, tap, toArray } from 'rxjs/operators'
+import { from, Observable } from 'rxjs'
+import { catchError, concatMap, map, shareReplay, toArray } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
-import { ResultList, User, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
+import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate, UserUpdateMe, UserVideoQuota } from '../../../../../shared'
 import { environment } from '../../../environments/environment'
 import { RestExtractor, RestPagination, RestService } from '../rest'
 import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
@@ -10,6 +10,10 @@ import { SortMeta } from 'primeng/api'
 import { BytesPipe } from 'ngx-pipes'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { UserRegister } from '@shared/models/users/user-register.model'
+import { User } from './user.model'
+import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
+import { has } from 'lodash-es'
+import { LocalStorageService, SessionStorageService } from '../misc/storage.service'
 
 @Injectable()
 export class UserService {
@@ -17,12 +21,14 @@ export class UserService {
 
   private bytesPipe = new BytesPipe()
 
-  private userCache: { [ id: number ]: Observable<User> } = {}
+  private userCache: { [ id: number ]: Observable<UserServerModel> } = {}
 
   constructor (
     private authHttp: HttpClient,
     private restExtractor: RestExtractor,
     private restService: RestService,
+    private localStorageService: LocalStorageService,
+    private sessionStorageService: SessionStorageService,
     private i18n: I18n
   ) { }
 
@@ -64,6 +70,30 @@ export class UserService {
                )
   }
 
+  updateMyAnonymousProfile (profile: UserUpdateMe) {
+    const supportedKeys = {
+      // local storage keys
+      nsfwPolicy: (val: NSFWPolicyType) => this.localStorageService.setItem(User.KEYS.NSFW_POLICY, val),
+      webTorrentEnabled: (val: boolean) => this.localStorageService.setItem(User.KEYS.WEBTORRENT_ENABLED, String(val)),
+      autoPlayVideo: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO, String(val)),
+      autoPlayNextVideoPlaylist: (val: boolean) => this.localStorageService.setItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST, String(val)),
+      theme: (val: string) => this.localStorageService.setItem(User.KEYS.THEME, val),
+      videoLanguages: (val: string[]) => this.localStorageService.setItem(User.KEYS.VIDEO_LANGUAGES, JSON.stringify(val)),
+
+      // session storage keys
+      autoPlayNextVideo: (val: boolean) =>
+        this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, String(val))
+    }
+
+    for (const key of Object.keys(profile)) {
+      try {
+        if (has(supportedKeys, key)) supportedKeys[key](profile[key])
+      } catch (err) {
+        console.error(`Cannot set item ${key} in localStorage. Likely due to a value impossible to stringify.`, err)
+      }
+    }
+  }
+
   deleteMe () {
     const url = UserService.BASE_USERS_URL + 'me'
 
@@ -187,7 +217,7 @@ export class UserService {
                )
   }
 
-  updateUsers (users: User[], userUpdate: UserUpdate) {
+  updateUsers (users: UserServerModel[], userUpdate: UserUpdate) {
     return from(users)
       .pipe(
         concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)),
@@ -205,17 +235,40 @@ export class UserService {
   }
 
   getUser (userId: number) {
-    return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
+    return this.authHttp.get<UserServerModel>(UserService.BASE_USERS_URL + userId)
                .pipe(catchError(err => this.restExtractor.handleError(err)))
   }
 
-  getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<User>> {
+  getAnonymousUser () {
+    let videoLanguages
+    try {
+      videoLanguages = JSON.parse(this.localStorageService.getItem(User.KEYS.VIDEO_LANGUAGES))
+    } catch (err) {
+      videoLanguages = null
+      console.error('Cannot parse desired video languages from localStorage.', err)
+    }
+
+    return new User({
+      // local storage keys
+      nsfwPolicy: this.localStorageService.getItem(User.KEYS.NSFW_POLICY) as NSFWPolicyType,
+      webTorrentEnabled: this.localStorageService.getItem(User.KEYS.WEBTORRENT_ENABLED) !== 'false',
+      autoPlayVideo: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO) === 'true',
+      autoPlayNextVideoPlaylist: this.localStorageService.getItem(User.KEYS.AUTO_PLAY_VIDEO_PLAYLIST) === 'true',
+      theme: this.localStorageService.getItem(User.KEYS.THEME) || 'default',
+      videoLanguages,
+
+      // session storage keys
+      autoPlayNextVideo: this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
+    })
+  }
+
+  getUsers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<UserServerModel>> {
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
 
     if (search) params = params.append('search', search)
 
-    return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params })
+    return this.authHttp.get<ResultList<UserServerModel>>(UserService.BASE_USERS_URL, { params })
                .pipe(
                  map(res => this.restExtractor.convertResultListDateToHuman(res)),
                  map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))),
@@ -223,7 +276,7 @@ export class UserService {
                )
   }
 
-  removeUser (usersArg: User | User[]) {
+  removeUser (usersArg: UserServerModel | UserServerModel[]) {
     const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
 
     return from(users)
@@ -234,7 +287,7 @@ export class UserService {
       )
   }
 
-  banUsers (usersArg: User | User[], reason?: string) {
+  banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
     const body = reason ? { reason } : {}
     const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
 
@@ -246,7 +299,7 @@ export class UserService {
       )
   }
 
-  unbanUsers (usersArg: User | User[]) {
+  unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
     const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
 
     return from(users)
@@ -257,7 +310,7 @@ export class UserService {
       )
   }
 
-  private formatUser (user: User) {
+  private formatUser (user: UserServerModel) {
     let videoQuota
     if (user.videoQuota === -1) {
       videoQuota = this.i18n('Unlimited')
index 2f5f82aa379e410d7e22bc6beae53fba79163701..b146d701499838f050321f00719a501073c321d4 100644 (file)
@@ -1,4 +1,4 @@
-import { debounceTime, first, tap } from 'rxjs/operators'
+import { debounceTime, first, tap, throttleTime } from 'rxjs/operators'
 import { OnDestroy, OnInit } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { fromEvent, Observable, of, Subject, Subscription } from 'rxjs'
@@ -15,6 +15,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { isLastMonth, isLastWeek, isToday, isYesterday } from '@shared/core-utils/miscs/date'
 import { ServerConfig } from '@shared/models'
 import { GlobalIconName } from '@app/shared/images/global-icon.component'
+import { UserService, User } from '../users'
+import { LocalStorageService } from '../misc/storage.service'
 
 enum GroupDate {
   UNKNOWN = 0,
@@ -72,9 +74,11 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
 
   protected abstract notifier: Notifier
   protected abstract authService: AuthService
+  protected abstract userService: UserService
   protected abstract route: ActivatedRoute
   protected abstract serverService: ServerService
   protected abstract screenService: ScreenService
+  protected abstract storageService: LocalStorageService
   protected abstract router: Router
   protected abstract i18n: I18n
   abstract titlePage: string
@@ -124,6 +128,16 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
     if (this.loadOnInit === true) {
       loadUserObservable.subscribe(() => this.loadMoreVideos())
     }
+
+    this.storageService.watch([
+      User.KEYS.NSFW_POLICY,
+      User.KEYS.VIDEO_LANGUAGES
+    ]).pipe(throttleTime(200)).subscribe(
+      () => {
+        this.loadUserVideoLanguagesIfNeeded()
+        if (this.hasDoneFirstQuery) this.reloadVideos()
+      }
+    )
   }
 
   ngOnDestroy () {
@@ -279,7 +293,12 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy, DisableFor
   }
 
   private loadUserVideoLanguagesIfNeeded () {
-    if (!this.authService.isLoggedIn() || !this.useUserVideoLanguagePreferences) {
+    if (!this.useUserVideoLanguagePreferences) {
+      return of(true)
+    }
+
+    if (!this.authService.isLoggedIn()) {
+      this.languageOneOf = this.userService.getAnonymousUser().videoLanguages
       return of(true)
     }
 
index 9962021545a6def2fa95527ed73f23222f3845b5..a51b9cab9eb9cd281642d41d1cdf5516de6b9201 100644 (file)
@@ -27,10 +27,11 @@ import { objectToFormData } from '@app/shared/misc/utils'
 import { Account } from '@app/shared/account/account.model'
 import { AccountService } from '@app/shared/account/account.service'
 import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
-import { ServerService } from '@app/core'
+import { ServerService, AuthService } from '@app/core'
 import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { NSFWPolicyType } from '@shared/models/videos/nsfw-policy.type'
 
 export interface VideosProvider {
   getVideos (parameters: {
@@ -49,6 +50,8 @@ export class VideoService implements VideosProvider {
 
   constructor (
     private authHttp: HttpClient,
+    private authService: AuthService,
+    private userService: UserService,
     private restExtractor: RestExtractor,
     private restService: RestService,
     private serverService: ServerService,
@@ -199,9 +202,10 @@ export class VideoService implements VideosProvider {
     filter?: VideoFilter,
     categoryOneOf?: number,
     languageOneOf?: string[],
-    skipCount?: boolean
+    skipCount?: boolean,
+    nsfw?: boolean
   }): Observable<ResultList<Video>> {
-    const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount } = parameters
+    const { videoPagination, sort, filter, categoryOneOf, languageOneOf, skipCount, nsfw } = parameters
 
     const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
 
@@ -212,6 +216,15 @@ export class VideoService implements VideosProvider {
     if (categoryOneOf) params = params.set('categoryOneOf', categoryOneOf + '')
     if (skipCount) params = params.set('skipCount', skipCount + '')
 
+    if (nsfw) {
+      params = params.set('nsfw', nsfw + '')
+    } else {
+      const nsfwPolicy = this.authService.isLoggedIn()
+        ? this.authService.getUser().nsfwPolicy
+        : this.userService.getAnonymousUser().nsfwPolicy
+      if (this.nsfwPolicyToFilter(nsfwPolicy)) params.set('nsfw', 'false')
+    }
+
     if (languageOneOf) {
       for (const l of languageOneOf) {
         params = params.append('languageOneOf[]', l)
@@ -368,4 +381,8 @@ export class VideoService implements VideosProvider {
                  catchError(err => this.restExtractor.handleError(err))
                )
   }
+
+  private nsfwPolicyToFilter (policy: NSFWPolicyType) {
+    return policy === 'do_not_list'
+  }
 }
index 0644200568c195a4767fd7d1132dde462738aea1..17e5beb2476c49673d00eeabf02ae569d60e8b77 100644 (file)
@@ -22,6 +22,8 @@ import { VideoSortField } from '@app/shared/video/sort-field.type'
 import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ResultList } from '@shared/models'
+import { UserService } from '../users'
+import { LocalStorageService } from '../misc/storage.service'
 
 export type SelectionType = { [ id: number ]: boolean }
 
@@ -51,7 +53,9 @@ export class VideosSelectionComponent extends AbstractVideoList implements OnIni
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     protected serverService: ServerService
   ) {
     super()
index c5ed36000130b2964387601d942b5f606b1eb1b6..827c34d414d90361d65716d1d38adb4493b5d5a4 100644 (file)
@@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.
 import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
 import { peertubeLocalStorage, peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { SessionStorageService, LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-video-watch-playlist',
@@ -42,16 +43,18 @@ export class VideoWatchPlaylistComponent {
     private notifier: Notifier,
     private i18n: I18n,
     private videoPlaylist: VideoPlaylistService,
+    private localStorageService: LocalStorageService,
+    private sessionStorageService: SessionStorageService,
     private router: Router
   ) {
     // defaults to true
     this.autoPlayNextVideoPlaylist = this.auth.isLoggedIn()
       ? this.auth.getUser().autoPlayNextVideoPlaylist
-      : peertubeLocalStorage.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false'
+      : this.localStorageService.getItem(VideoWatchPlaylistComponent.LOCAL_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) !== 'false'
     this.setAutoPlayNextVideoPlaylistSwitchText()
 
     // defaults to false
-    this.loopPlaylist = peertubeSessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true'
+    this.loopPlaylist = this.sessionStorageService.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true'
     this.setLoopPlaylistSwitchText()
   }
 
index cfa0432ad009e7b69454b04480ad287ec156a60f..585acd7a89cb627bd59fcce47ef4efa92cf38d15 100644 (file)
 
   <div class="row privacy-concerns" *ngIf="hasAlreadyAcceptedPrivacyConcern === false">
     <div class="privacy-concerns-text">
-      <strong i18n>Friendly Reminder: </strong>
-      <ng-container i18n>
-        the sharing system used for this video implies that some technical information about your system (such as a public IP address) can be sent to other peers.
-      </ng-container>
+      <span i18n class="mr-2">
+        <strong>Help your peers</strong>
+        and activate the sharing system to improve the experience for everyone.
+      </span>
       <a i18n i18n-title title="Get more information" target="_blank" rel="noopener noreferrer" href="/about/peertube">More information</a>
     </div>
 
-    <div i18n class="privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
-      OK
+    <div i18n class="privacy-concerns-button" (click)="declinedPrivacyConcern()">
+      No thanks
+    </div>
+    <div i18n class="privacy-concerns-button privacy-concerns-okay" (click)="acceptedPrivacyConcern()">
+      Activate
     </div>
   </div>
 </div>
index ae79c2ff6c19cf74d3f11a7177d7ab260b0fc4f6..10e129ac589555e9b4f6d8b871779d36b4a2d4b1 100644 (file)
@@ -443,6 +443,7 @@ my-video-comments {
 
 // If the view is not expanded, take into account the menu
 .privacy-concerns {
+  z-index: z(dropdown) + 1;
   width: calc(100% - #{$menu-width});
 }
 
@@ -488,11 +489,11 @@ my-video-comments {
     }
   }
 
-  .privacy-concerns-okay {
-    background-color: var(--mainColor);
+  .privacy-concerns-button {
     padding: 5px 8px 5px 7px;
     margin-left: auto;
     border-radius: 3px;
+    white-space: nowrap;
     cursor: pointer;
     transition: background-color 0.3s;
     font-weight: $font-semibold;
@@ -501,6 +502,11 @@ my-video-comments {
       background-color: #000;
     }
   }
+
+  .privacy-concerns-okay {
+    background-color: var(--mainColor);
+    margin-left: 10px;
+  }
 }
 
 @media screen and (max-width: 1600px) {
index ee3deb5e97f170fca413583f6fd46e4c93e40459..9ba14316caa5388b06e31b061a874225187ed170 100644 (file)
@@ -2,7 +2,7 @@ import { catchError } from 'rxjs/operators'
 import { ChangeDetectorRef, Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { RedirectService } from '@app/core/routing/redirect.service'
-import { peertubeLocalStorage, peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage'
+import { peertubeLocalStorage } from '@app/shared/misc/peertube-web-storage'
 import { VideoSupportComponent } from '@app/videos/+video-watch/modal/video-support.component'
 import { MetaService } from '@ngx-meta/core'
 import { AuthUser, Notifier, ServerService } from '@app/core'
@@ -10,7 +10,7 @@ import { forkJoin, Observable, Subscription } from 'rxjs'
 import { Hotkey, HotkeysService } from 'angular2-hotkeys'
 import { ServerConfig, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '../../../../../shared'
 import { AuthService, ConfirmService } from '../../core'
-import { RestExtractor } from '../../shared'
+import { RestExtractor, UserService } from '../../shared'
 import { VideoDetails } from '../../shared/video/video-details.model'
 import { VideoService } from '../../shared/video/video.service'
 import { VideoShareComponent } from './modal/video-share.component'
@@ -35,7 +35,6 @@ import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watc
 import { getStoredP2PEnabled, getStoredTheater } from '../../../assets/player/peertube-player-local-storage'
 import { HooksService } from '@app/core/plugins/hooks.service'
 import { PlatformLocation } from '@angular/common'
-import { RecommendedVideosComponent } from '../recommendations/recommended-videos.component'
 import { scrollToTop, isXPercentInViewport } from '@app/shared/misc/utils'
 
 @Component({
@@ -95,6 +94,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private confirmService: ConfirmService,
     private metaService: MetaService,
     private authService: AuthService,
+    private userService: UserService,
     private serverService: ServerService,
     private restExtractor: RestExtractor,
     private notifier: Notifier,
@@ -118,6 +118,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.authService.getUser()
   }
 
+  get anonymousUser () {
+    return this.userService.getAnonymousUser()
+  }
+
   async ngOnInit () {
     this.serverConfig = this.serverService.getTmpConfig()
 
@@ -266,6 +270,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.redirectService.redirectToHomepage()
   }
 
+  declinedPrivacyConcern () {
+    peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'false')
+    this.hasAlreadyAcceptedPrivacyConcern = false
+  }
+
   acceptedPrivacyConcern () {
     peertubeLocalStorage.setItem(VideoWatchComponent.LOCAL_STORAGE_PRIVACY_CONCERN_KEY, 'true')
     this.hasAlreadyAcceptedPrivacyConcern = true
@@ -290,7 +299,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   isAutoPlayEnabled () {
     return (
       (this.user && this.user.autoPlayNextVideo) ||
-      peertubeSessionStorage.getItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
+      this.anonymousUser.autoPlayNextVideo
     )
   }
 
@@ -302,7 +311,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
   isPlaylistAutoPlayEnabled () {
     return (
       (this.user && this.user.autoPlayNextVideoPlaylist) ||
-      peertubeSessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true'
+      this.anonymousUser.autoPlayNextVideoPlaylist
     )
   }
 
index 5fa50ecbb1fa30a584eb81e8ae9b0ddb4aee59d4..9b445269d3634a513a22a1c9b3c3dca66ce3158e 100644 (file)
@@ -12,7 +12,6 @@ import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
 import { RecommendationsModule } from '@app/videos/recommendations/recommendations.module'
 import { VideoWatchPlaylistComponent } from '@app/videos/+video-watch/video-watch-playlist.component'
 import { QRCodeModule } from 'angularx-qrcode'
-import { InputSwitchModule } from 'primeng/inputswitch'
 import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestamp-route-transformer.directive'
 
 @NgModule({
@@ -21,8 +20,7 @@ import { TimestampRouteTransformerDirective } from '@app/shared/angular/timestam
     SharedModule,
     NgbTooltipModule,
     QRCodeModule,
-    RecommendationsModule,
-    InputSwitchModule
+    RecommendationsModule
   ],
 
   declarations: [
index 4548c7d800ab8fd3c07a1196348d2b5184f3966b..74f9ed2a5a6e1c4a70ed2007581eb3e6b8f17136 100644 (file)
@@ -8,7 +8,7 @@
         [ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto"
       >
         <span i18n>AUTOPLAY</span>
-        <p-inputSwitch [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></p-inputSwitch>
+        <p-inputSwitch class="small" [(ngModel)]="autoPlayNextVideo" (ngModelChange)="switchAutoPlayNextVideo()"></p-inputSwitch>
       </div>
     </div>
 
index 1ab0c47ff54073ae9bca17bfaab3f1aef7b93487..cde62f87f7471f6d87837c23f1d407cc94b09e21 100644 (file)
     font-weight: 600;
   }
 }
-
-/* p-inputSwitch styles to reduce the switch size */
-
-::ng-deep {
-  p-inputswitch {
-    height: 20px;
-  }
-
-  .ui-inputswitch {
-    width: 2.5em !important;
-    height: 1.45em !important;
-
-    .ui-inputswitch-slider::before {
-      height: 1em !important;
-      width: 1em !important;
-    }
-  }
-
-  .ui-inputswitch-checked .ui-inputswitch-slider::before {
-    transform: translateX(1em) !important;
-  }
-}
index ada6d3433023fff2bd10fec7fed64ec52a685b31..d4b4c929b42dcd79e0f81ce9a01180d205f2fe89 100644 (file)
@@ -7,8 +7,8 @@ import { RecommendedVideosStore } from '@app/videos/recommendations/recommended-
 import { User } from '@app/shared'
 import { AuthService, Notifier } from '@app/core'
 import { UserService } from '@app/shared/users/user.service'
-import { peertubeSessionStorage } from '@app/shared/misc/peertube-web-storage'
 import { I18n } from '@ngx-translate/i18n-polyfill'
+import { SessionStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-recommended-videos',
@@ -16,8 +16,6 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './recommended-videos.component.scss' ]
 })
 export class RecommendedVideosComponent implements OnChanges {
-  static SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO = 'auto_play_next_video'
-
   @Input() inputRecommendation: RecommendationInfo
   @Input() user: User
   @Input() playlist: VideoPlaylist
@@ -34,15 +32,21 @@ export class RecommendedVideosComponent implements OnChanges {
     private authService: AuthService,
     private notifier: Notifier,
     private i18n: I18n,
-    private store: RecommendedVideosStore
+    private store: RecommendedVideosStore,
+    private sessionStorageService: SessionStorageService
   ) {
     this.videos$ = this.store.recommendations$
     this.hasVideos$ = this.store.hasRecommendations$
     this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
 
-    this.autoPlayNextVideo = this.authService.isLoggedIn()
-      ? this.authService.getUser().autoPlayNextVideo
-      : peertubeSessionStorage.getItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' || false
+    if (this.authService.isLoggedIn()) {
+      this.autoPlayNextVideo = this.authService.getUser().autoPlayNextVideo
+    } else {
+      this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true' || false
+      this.sessionStorageService.watch([User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO]).subscribe(
+        () => this.autoPlayNextVideo = this.sessionStorageService.getItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
+      )
+    }
 
     this.autoPlayNextVideoTooltip = this.i18n('When active, the next video is automatically played after the current one.')
   }
@@ -58,7 +62,7 @@ export class RecommendedVideosComponent implements OnChanges {
   }
 
   switchAutoPlayNextVideo () {
-    peertubeSessionStorage.setItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString())
+    this.sessionStorageService.setItem(User.KEYS.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO, this.autoPlayNextVideo.toString())
 
     if (this.authService.isLoggedIn()) {
       const details = {
index 59f65f95c57487513d256722e30dd78767163886..757b0e498fe54c808d3b523ef9308fda4679b141 100644 (file)
@@ -11,6 +11,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
 import { UserRight } from '../../../../../shared/models/users'
 import { Notifier, ServerService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-videos-local',
@@ -31,7 +33,9 @@ export class VideoLocalComponent extends AbstractVideoList implements OnInit, On
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoService: VideoService,
     private hooks: HooksService
   ) {
index 6ff7a1e0e0887fd84b7e0107c91f51738dac298d..b69fad05f4890fe9bc238acecb29f3201ce6c436 100644 (file)
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-videos-most-liked',
@@ -28,7 +30,9 @@ export class VideoMostLikedComponent extends AbstractVideoList implements OnInit
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoService: VideoService,
     private hooks: HooksService
   ) {
index 7568f4536df21d933acd246aefcb9112f8f4159b..c1ddd4fd469bcda00e93cbe70668d0c57c82edf1 100644 (file)
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-videos-recently-added',
@@ -29,7 +31,9 @@ export class VideoRecentlyAddedComponent extends AbstractVideoList implements On
     protected router: Router,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoService: VideoService,
     private hooks: HooksService
   ) {
index e29830b5b1f08882c09a8e9ccb04db5c909b76a8..fbe0522771dd2c7181ebe0bb6c0ad18c55d45880 100644 (file)
@@ -9,6 +9,8 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
 import { Notifier, ServerService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-videos-trending',
@@ -28,7 +30,9 @@ export class VideoTrendingComponent extends AbstractVideoList implements OnInit,
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoService: VideoService,
     private hooks: HooksService
   ) {
index cf0b15054df68a6a18ae58c2448c708085b3578b..036fd8dcbb1a6a2812f3cb5513bc239ac47093a1 100644 (file)
@@ -10,6 +10,8 @@ import { ScreenService } from '@app/shared/misc/screen.service'
 import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
 import { Notifier, ServerService } from '@app/core'
 import { HooksService } from '@app/core/plugins/hooks.service'
+import { UserService } from '@app/shared'
+import { LocalStorageService } from '@app/shared/misc/storage.service'
 
 @Component({
   selector: 'my-videos-user-subscriptions',
@@ -29,7 +31,9 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement
     protected route: ActivatedRoute,
     protected notifier: Notifier,
     protected authService: AuthService,
+    protected userService: UserService,
     protected screenService: ScreenService,
+    protected storageService: LocalStorageService,
     private videoService: VideoService,
     private hooks: HooksService
   ) {
diff --git a/client/src/assets/images/global/video-lang.svg b/client/src/assets/images/global/video-lang.svg
new file mode 100644 (file)
index 0000000..8d7b6a0
--- /dev/null
@@ -0,0 +1,15 @@
+<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" style="transform:scale(1.1);">
+ <g class="layer" style="transform: scale(.9);">
+  <g fill="none" fill-rule="evenodd">
+   <g transform="translate(-884.000000, -863.000000)">
+    <g transform="translate(884.000000, 863.000000)">
+     <path d="m22.78031,7.45167c0,0 -0.21495,-1.56763 -0.87461,-2.25797c-0.83658,-0.90605 -1.7743,-0.91055 -2.20433,-0.96357c-3.07858,-0.23013 -7.69661,-0.23013 -7.69661,-0.23013l-0.00956,0c0,0 -4.61792,0 -7.69661,0.23013c-0.43005,0.05302 -1.36743,0.05752 -2.20431,0.96357c-0.65962,0.69034 -0.87427,2.25797 -0.87427,2.25797c0,0 -0.22001,1.84092 -0.22001,3.68182l0,1.72585c0,1.84087 0.22001,3.68177 0.22001,3.68177c0,0 0.21465,1.56766 0.87427,2.25799c0.83688,0.90608 1.93618,0.87741 2.42581,0.97238c1.76002,0.17451 7.47991,0.22852 7.47991,0.22852c0,0 4.62279,-0.00719 7.70137,-0.2373c0.43003,-0.05305 1.36775,-0.05752 2.20433,-0.9636c0.65966,-0.69033 0.87461,-2.25799 0.87461,-2.25799c0,0 0.21969,-1.8409 0.21969,-3.68177l0,-1.72585c0,-1.8409 -0.21969,-3.68182 -0.21969,-3.68182l0,0z" fill="#ffffff" stroke="#000000" stroke-width="2"/>
+    </g>
+   </g>
+  </g>
+  <g>
+   <path d="m9.639451,16.289861a0.758829,0.758829 0 0 1 -0.537251,-1.296079l3.226539,-3.226539l-2.689289,0a0.758829,0.758829 0 0 1 0,-1.517657l4.521101,0a0.758829,0.758829 0 0 1 0.537251,1.296079l-4.522619,4.521101a0.758829,0.758829 0 0 1 -0.535733,0.223096z" fill="#000000" stroke="#000000" stroke-width="0"/>
+   <path d="m13.029897,9.507451l-2.208191,0a0.758829,0.758829 0 1 1 0,-1.517657l2.21578,0a0.758829,0.758829 0 0 1 0,1.517657l-0.007588,0z" fill="#000000" stroke="#000000" stroke-width="0"/>
+  </g>
+ </g>
+</svg>
diff --git a/client/src/assets/images/menu/about.svg b/client/src/assets/images/menu/about.svg
deleted file mode 100644 (file)
index bea602a..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="Artboard-4" transform="translate(-400.000000, -247.000000)">
-            <g id="69" transform="translate(400.000000, 247.000000)">
-                <circle id="Oval-7" stroke="#000000" stroke-width="2" cx="12" cy="12" r="10"></circle>
-                <path d="M12.016,14.544 C12.384,14.544 12.64,14.256 12.704,13.904 L12.768,13.168 C14.544,12.864 16,11.952 16,9.936 L16,9.904 C16,7.904 14.48,6.656 12.24,6.656 C10.768,6.656 9.696,7.184 8.848,7.984 C8.624,8.176 8.528,8.432 8.528,8.672 C8.528,9.152 8.928,9.552 9.424,9.552 C9.648,9.552 9.856,9.456 10.016,9.328 C10.656,8.752 11.344,8.448 12.192,8.448 C13.344,8.448 14.032,9.072 14.032,9.968 L14.032,10 C14.032,11.008 13.2,11.584 11.696,11.728 C11.264,11.776 11.008,12.096 11.072,12.528 L11.232,13.904 C11.28,14.272 11.552,14.544 11.92,14.544 L12.016,14.544 Z M10.784,16.816 L10.784,16.976 C10.784,17.6 11.264,18.08 11.92,18.08 C12.576,18.08 13.056,17.6 13.056,16.976 L13.056,16.816 C13.056,16.192 12.576,15.712 11.92,15.712 C11.264,15.712 10.784,16.192 10.784,16.816 Z" id="?" fill="#000000"></path>
-            </g>
-        </g>
-    </g>
-</svg>
diff --git a/client/src/assets/images/menu/administration.svg b/client/src/assets/images/menu/administration.svg
deleted file mode 100644 (file)
index 0dceda0..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
-    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
-        <g id="Artboard-4" transform="translate(-444.000000, -247.000000)" fill="#000000">
-            <g id="70" transform="translate(444.000000, 247.000000)">
-                <path d="M8.82929429,17 L20.0066023,17 C20.5552407,17 21,17.4438648 21,18 C21,18.5522847 20.5550537,19 20.0066023,19 L8.82929429,19 C8.41745788,20.1651924 7.30621883,21 6,21 C4.34314575,21 3,19.6568542 3,18 C3,16.3431458 4.34314575,15 6,15 C7.30621883,15 8.41745788,15.8348076 8.82929429,17 Z M9.17070571,13 L3.99339768,13 C3.44475929,13 3,12.5561352 3,12 C3,11.4477153 3.44494629,11 3.99339768,11 L9.17070571,11 C9.58254212,9.83480763 10.6937812,9 12,9 C13.3062188,9 14.4174579,9.83480763 14.8292943,11 L20.0066023,11 C20.5552407,11 21,11.4438648 21,12 C21,12.5522847 20.5550537,13 20.0066023,13 L14.8292943,13 C14.4174579,14.1651924 13.3062188,15 12,15 C10.6937812,15 9.58254212,14.1651924 9.17070571,13 Z M15.1659641,6.98648118 C15.1124525,6.99537358 15.05751,7 15.0014977,7 L3.99850233,7 C3.44704472,7 3,6.55613518 3,6 C3,5.44771525 3.44748943,5 3.99850233,5 L15.0014977,5 C15.0575314,5 15.1124871,5.00458274 15.1660053,5.01340035 C15.5740343,3.84121344 16.6887792,3 18,3 C19.6568542,3 21,4.34314575 21,6 C21,7.65685425 19.6568542,9 18,9 C16.688735,9 15.5739592,8.15872988 15.1659641,6.98648118 Z M18,7 C18.5522847,7 19,6.55228475 19,6 C19,5.44771525 18.5522847,5 18,5 C17.4477153,5 17,5.44771525 17,6 C17,6.55228475 17.4477153,7 18,7 Z M12,13 C12.5522847,13 13,12.5522847 13,12 C13,11.4477153 12.5522847,11 12,11 C11.4477153,11 11,11.4477153 11,12 C11,12.5522847 11.4477153,13 12,13 Z M6,19 C6.55228475,19 7,18.5522847 7,18 C7,17.4477153 6.55228475,17 6,17 C5.44771525,17 5,17.4477153 5,18 C5,18.5522847 5.44771525,19 6,19 Z" id="Combined-Shape"></path>
-            </g>
-        </g>
-    </g>
-</svg>
diff --git a/client/src/assets/images/menu/eye-closed.svg b/client/src/assets/images/menu/eye-closed.svg
new file mode 100644 (file)
index 0000000..5c441e7
--- /dev/null
@@ -0,0 +1,17 @@
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs/>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
+    <g transform="translate(-796.000000, -1046.000000)" stroke="#000000" stroke-width="2">
+      <g transform="translate(48.000000, 1046.000000)">
+        <g transform="translate(760.000000, 12.000000) scale(1, -1) translate(-760.000000, -12.000000) translate(748.000000, 0.000000)">
+          <path d="M2,14 C2,14 5,7 12,7 C19,7 22,14 22,14" id="Path-80" stroke-linejoin="round"/>
+          <path d="M12,7 L12,5"/>
+          <path d="M18,8.5 L19,7"/>
+          <path d="M21,12 L22.5,11"/>
+          <path d="M1.5,12 L3,11" transform="translate(2.250000, 11.500000) scale(1, -1) translate(-2.250000, -11.500000) "/>
+          <path d="M5,8.5 L6,7" transform="translate(5.500000, 7.750000) scale(-1, 1) translate(-5.500000, -7.750000) "/>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/client/src/assets/images/menu/eye.svg b/client/src/assets/images/menu/eye.svg
new file mode 100644 (file)
index 0000000..d1c3941
--- /dev/null
@@ -0,0 +1,15 @@
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g transform="translate(-268.000000, -203.000000)" stroke="#000000" stroke-width="2">
+      <g transform="translate(268.000000, 203.000000)">
+        <path d="M2,12 C2,12 5,5 12,5 C19,5 22,12 22,12 C22,12 19,19 12,19 C5,19 2,12 2,12 Z" stroke-linejoin="round"/>
+        <circle id="Oval-50" cx="12" cy="12" r="3"/>
+        <path d="M12,5 L12,3" stroke-linecap="round"/>
+        <path d="M18,6.5 L19,5" stroke-linecap="round"/>
+        <path d="M21,10 L22.5,9" stroke-linecap="round"/>
+        <path d="M1.5,10 L3,9" stroke-linecap="round" transform="translate(2.250000, 9.500000) scale(1, -1) translate(-2.250000, -9.500000) "/>
+        <path d="M5,6.5 L6,5" stroke-linecap="round" transform="translate(5.500000, 5.750000) scale(-1, 1) translate(-5.500000, -5.750000) "/>
+      </g>
+    </g>
+  </g>
+</svg>
\ No newline at end of file
diff --git a/client/src/assets/images/menu/language.png b/client/src/assets/images/menu/language.png
deleted file mode 100644 (file)
index 60e6fec..0000000
Binary files a/client/src/assets/images/menu/language.png and /dev/null differ
diff --git a/client/src/assets/images/menu/language.svg b/client/src/assets/images/menu/language.svg
new file mode 100644 (file)
index 0000000..0ac754c
--- /dev/null
@@ -0,0 +1,10 @@
+<!-- by Aaron Jin - free for commercial use -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" style="transform:scale(1.2)">
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M92.63,155H42.09a17.8,17.8,0,0,1-17.78-17.78V29.31a5,5,0,0,1,5-5h88.32a5,5,0,0,1,4.9,6L97.53,151A5,5,0,0,1,92.63,155ZM34.31,34.31V137.22A7.79,7.79,0,0,0,42.09,145H88.56L111.49,34.31Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M170.69,175.69H75a5,5,0,0,1-4.9-6L74.39,149a5,5,0,0,1,9.8,2l-3,14.67h84.55V62.78A7.79,7.79,0,0,0,157.91,55H113.35a5,5,0,0,1,0-10h44.56a17.8,17.8,0,0,1,17.78,17.78V170.69A5,5,0,0,1,170.69,175.69Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M50,92h0a5,5,0,0,1-5-5l0-24.49a17.49,17.49,0,0,1,35,0V87a5,5,0,0,1-10,0V62.49a7.49,7.49,0,0,0-15,0L55,87A5,5,0,0,1,50,92Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M75,76H50a5,5,0,0,1,0-10H75a5,5,0,0,1,0,10Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M120.21,155a5,5,0,0,1-3.54-8.54l21.26-21.26H120.21a5,5,0,0,1,0-10H150a5,5,0,0,1,3.54,8.54l-29.8,29.79A5,5,0,0,1,120.21,155Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M150,155a5,5,0,0,1-3.54-1.47l-14.89-14.89a5,5,0,0,1,7.07-7.07l14.9,14.89A5,5,0,0,1,150,155Z"/>
+  <path stroke="#000000" fill="#000000" stroke-width="3" d="M142.55,110.31H128a5,5,0,1,1,0-10h14.6a5,5,0,0,1,0,10Z"/>
+</svg>
\ No newline at end of file
diff --git a/client/src/assets/images/menu/moonsun.svg b/client/src/assets/images/menu/moonsun.svg
deleted file mode 100644 (file)
index fe2a963..0000000
+++ /dev/null
@@ -1 +0,0 @@
-<svg height="300px" width="300px" fill="#fff" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 100 100" x="0px" y="0px"><title>Artboard 633</title><circle cx="50" cy="6" r="4"/><circle cx="50" cy="94" r="4"/><circle cx="6" cy="50" r="4"/><circle cx="94" cy="50" r="4"/><circle cx="18" cy="18" r="4"/><circle cx="82" cy="82" r="4"/><circle cx="18" cy="82" r="4"/><circle cx="82" cy="18" r="4"/><path d="M82,50A32,32,0,1,0,50,82,32,32,0,0,0,82,50ZM50,26a23.67,23.67,0,0,1,5.87.76c4.36,9.93.57,19-4.66,24.29s-14.4,9.24-24.45,4.83A23.75,23.75,0,0,1,26,50,24,24,0,0,1,50,26Zm0,48a23.94,23.94,0,0,1-18.26-8.47,29.38,29.38,0,0,0,3.74.26,30.07,30.07,0,0,0,21.41-9.11,29.82,29.82,0,0,0,8.61-25A24,24,0,0,1,50,74Z"/></svg>
\ No newline at end of file
diff --git a/client/src/assets/images/menu/p2p.svg b/client/src/assets/images/menu/p2p.svg
new file mode 100644 (file)
index 0000000..7446430
--- /dev/null
@@ -0,0 +1,11 @@
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs/>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g transform="translate(-752.000000, -423.000000)" fill="#000000">
+      <g transform="translate(752.000000, 423.000000)">
+        <path d="M19.1632285,17.9958742 C20.7455119,17.9132011 22,16.5984601 22,14.9914698 L22,7.0085302 C22,5.35043647 20.6598453,4 19.0049107,4 L4.99508929,4 C3.33899222,4 2,5.34829734 2,7.0085302 L2,14.9914698 C2,16.5963573 3.25552676,17.9130154 4.83678095,17.9958629 L6.5,16 L4.99508929,16 C4.4481604,16 4,15.5484013 4,14.9914698 L4,7.0085302 C4,6.4497782 4.44667411,6 4.99508929,6 L19.0049107,6 C19.5518396,6 20,6.45159872 20,7.0085302 L20,14.9914698 C20,15.5502218 19.5533259,16 19.0049107,16 L17.5,16 L19.1632285,17.9958742 Z" fill-rule="nonzero"/>
+        <polygon stroke="#000000" stroke-width="2" stroke-linejoin="round" points="12 14 17 20 7 20"/>
+      </g>
+    </g>
+  </g>
+</svg>
\ No newline at end of file
index 560414e90fa60200356f4afb2c4b2321adf8afd1..046368c8b71a392c6efc3af1c4bad9ef7124a78f 100644 (file)
@@ -24,6 +24,7 @@ body {
   // now beware node-sass requires interpolation
   // for css custom properties #{$var}
   --mainColor: #{$orange-color};
+  --mainColorLighter: #{$orange-color-lighter};
   --mainHoverColor: #{$orange-hover-color};
   --mainBackgroundColor: #{$bg-color};
   --mainForegroundColor: #{$fg-color};
index 6cced995e27fe4f9d34ec7b80eee2cff34509acc..bb7b21274e147e9e0aaa679be694e066d556d07d 100644 (file)
@@ -13,6 +13,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
   flex: auto;
 }
 
+.c-hand {
+  cursor: pointer;
+}
+
 @keyframes spin {
   from {
     transform: scale(1) rotate(0deg);
@@ -41,6 +45,10 @@ $icon-font-path: '~@neos21/bootstrap3-glyphicons/assets/fonts/';
       background-color: var(--mainHoverColor);
       opacity: .9;
     }
+    
+    &::after {
+      display: none;
+    }
   }
 
   button {
index 4766e449085f77657c8f51ec22d904387d9c48b7..bafc82d7d7cfc21d3ad6b7f1d47619f4acbf5dd8 100644 (file)
   }
 }
 
+@mixin fill-svg-color ($color) {
+  ::ng-deep svg {
+    path {
+      fill: $color;
+    }
+  }
+}
+
 @mixin button-focus-visible-shadow($color) {
   &.focus-visible {
     box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 4px $color;
index 4ef8e17b92c7d958b9f0ef279439e5d1a58b330b..91229cee071c6d7dc462248289f3b133c2481ea8 100644 (file)
@@ -14,6 +14,7 @@ $grey-foreground-color: #585858;
 $grey-foreground-hover-color: #303030;
 
 $orange-color: #F1680D;
+$orange-color-lighter: rgb(233, 159, 110);
 $orange-hover-color: #F97D46;
 
 $cyan-color: hsl(187, 77%, 34%);
@@ -74,6 +75,7 @@ $activated-action-button-color: black;
 // to be warned of non-existing variables
 $variables: (
   --mainColor: var(--mainColor),
+  --mainColorLighter: var(--mainColorLighter),
   --mainHoverColor: var(--mainHoverColor),
   --mainBackgroundColor: var(--mainBackgroundColor),
   --mainForegroundColor: var(--mainForegroundColor),
@@ -112,8 +114,9 @@ $zindex: (
   tooltip      : 14000,
   loadbar      : 15000,
   modal        : 16000,
-  notification : 17000,
-  hotkeys      : 18000
+  help-popover : 17000,
+  notification : 18000,
+  hotkeys      : 19000
 );
 
 @function z($label) {
index 4d2d6cb67669391d14d5d3f63157973b318d042b..e2c4532280eea0b49c31ddeb6c7fb1f444d2c5f6 100644 (file)
@@ -274,6 +274,15 @@ p-multiselect {
     //  left: -2px !important;
     //}
   }
+
+  .ui-multiselect-panel .ui-multiselect-items .ui-multiselect-item.ui-state-highlight {
+    background-color: var(--mainColorLighter);
+  }
+
+  .ui-inputtext:enabled:focus:not(.ui-state-error) {
+    border-color: var(--mainColorLighter) !important;
+    box-shadow: none;
+  }
 }
 
 // PrimeNG calendar tweaks
@@ -379,6 +388,24 @@ p-inputswitch {
   .ui-inputswitch-checked .ui-inputswitch-slider {
     background-color: var(--mainColor) !important;
   }
+
+  &.small {
+    height: 20px;
+
+    .ui-inputswitch {
+      width: 2.5em !important;
+      height: 1.45em !important;
+
+      .ui-inputswitch-slider::before {
+        height: 1em !important;
+        width: 1em !important;
+      }
+    }
+
+    .ui-inputswitch-checked .ui-inputswitch-slider::before {
+      transform: translateX(1em) !important;
+    }
+  }
 }
 
 p-toast {