* [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
</div>
<div class="col">
- <div i18n class="middle-title">
+ <div id="statistics" i18n class="middle-title">
STATISTICS
</div>
<my-instance-statistics></my-instance-statistics>
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',
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
) {
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',
protected userService: UserService,
protected notifier: Notifier,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private confirmService: ConfirmService,
private videoService: VideoService,
private userHistoryService: UserHistoryService
</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>
-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 (
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']
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.'))
+ }
}
}
</div>
</div>
+ <ng-content select="inner-title"></ng-content>
+
<div class="form-group">
<my-peertube-checkbox
inputName="webTorrentEnabled" formControlName="webTorrentEnabled"
></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>
-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,
}
ngOnInit () {
+ let oldForm: any
+
this.buildForm({
nsfwPolicy: null,
webTorrentEnabled: null,
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 })))
? 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']
}
}
- const details: UserUpdateMe = {
+ let details: UserUpdateMe = {
nsfwPolicy,
webTorrentEnabled,
autoPlayVideo,
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 () {
<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>
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'
} 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: [
MyAccountComponent,
MyAccountSettingsComponent,
MyAccountChangePasswordComponent,
- MyAccountVideoSettingsComponent,
MyAccountProfileComponent,
MyAccountChangeEmailComponent,
- MyAccountInterfaceSettingsComponent,
MyAccountVideosComponent,
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',
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
) {
<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> -
- <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>
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({
MenuComponent,
LanguageChooserComponent,
+ QuickSettingsModalComponent,
AvatarNotificationComponent,
HeaderComponent,
SearchTypeaheadComponent,
}
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[]
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()
}
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[] = []
constructor (
private auth: AuthService,
+ private userService: UserService,
private pluginService: PluginService,
- private server: ServerService
+ private server: ServerService,
+ private localStorageService: LocalStorageService
) {}
initialize () {
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
}
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
if (!this.auth.isLoggedIn()) {
this.updateCurrentTheme()
+
+ this.localStorageService.watch([User.KEYS.THEME]).subscribe(
+ () => this.updateCurrentTheme()
+ )
}
this.auth.userInformationLoaded
}
private loadAndSetFromLocalStorage () {
- const lastActiveThemeString = peertubeLocalStorage.getItem(ThemeService.KEYS.LAST_ACTIVE_THEME)
+ const lastActiveThemeString = this.localStorageService.getItem(User.KEYS.THEME)
if (!lastActiveThemeString) return
try {
.help-to-translate {
@include peertube-button-link;
@include orange-button;
+
+ border-radius: 0;
}
.modal-body {
-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',
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] }))
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
+ }
}
<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)">©</span> 2015-2020
+ </a>
+ </small>
+ </div>
+ </div>
</div>
</menu>
</div>
<my-language-chooser #languageChooserModal></my-language-chooser>
+<my-quick-settings #quickSettingsModal></my-quick-settings>
&.logged-in {
.panel-block {
- margin-bottom: 25px;
+ margin-bottom: 20px;
}
.block-title {
@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;
- }
- }
- }
}
}
}
.panel-block {
- margin-bottom: 45px;
+ margin-bottom: 15px;
a {
@include disable-default-a-behaviour;
}
.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;
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',
})
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 } = {
constructor (
private authService: AuthService,
+ private userService: UserService,
private serverService: ServerService,
private redirectService: RedirectService,
- private hotkeysService: HotkeysService
+ private hotkeysService: HotkeysService,
+ private i18n: I18n
) {}
ngOnInit () {
}
)
- 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 () {
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()
--- /dev/null
+<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>
--- /dev/null
+@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
--- /dev/null
+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()
+ }
+}
'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')
::ng-deep {
.help-popover {
+ z-index: z(help-popover) !important;
max-width: 300px;
.popover-body {
--- /dev/null
+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
+}
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'
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: [
PrimeSharedModule,
InputMaskModule,
NgPipesModule,
- MultiSelectModule
+ MultiSelectModule,
+ InputSwitchModule
],
declarations: [
DateToggleComponent,
GlobalIconComponent,
- PreviewUploadComponent
+ PreviewUploadComponent,
+
+ MyAccountVideoSettingsComponent,
+ MyAccountInterfaceSettingsComponent
],
exports: [
FromNowPipe,
HighlightPipe,
PeerTubeTemplateDirective,
- VideoDurationPipe
+ VideoDurationPipe,
+
+ MyAccountVideoSettingsComponent,
+ MyAccountInterfaceSettingsComponent
],
providers: [
I18nPrimengCalendarService,
ScreenService,
+ LocalStorageService, SessionStorageService,
UserNotificationService,
-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
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
-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'
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 {
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
) { }
)
}
+ 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'
)
}
- 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)),
}
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))),
)
}
- removeUser (usersArg: User | User[]) {
+ removeUser (usersArg: UserServerModel | UserServerModel[]) {
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
return from(users)
)
}
- banUsers (usersArg: User | User[], reason?: string) {
+ banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) {
const body = reason ? { reason } : {}
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
)
}
- unbanUsers (usersArg: User | User[]) {
+ unbanUsers (usersArg: UserServerModel | UserServerModel[]) {
const users = Array.isArray(usersArg) ? usersArg : [ usersArg ]
return from(users)
)
}
- private formatUser (user: User) {
+ private formatUser (user: UserServerModel) {
let videoQuota
if (user.videoQuota === -1) {
videoQuota = this.i18n('Unlimited')
-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'
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,
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
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 () {
}
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)
}
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: {
constructor (
private authHttp: HttpClient,
+ private authService: AuthService,
+ private userService: UserService,
private restExtractor: RestExtractor,
private restService: RestService,
private serverService: ServerService,
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)
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)
catchError(err => this.restExtractor.handleError(err))
)
}
+
+ private nsfwPolicyToFilter (policy: NSFWPolicyType) {
+ return policy === 'do_not_list'
+ }
}
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 }
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
protected serverService: ServerService
) {
super()
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',
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()
}
<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>
// If the view is not expanded, take into account the menu
.privacy-concerns {
+ z-index: z(dropdown) + 1;
width: calc(100% - #{$menu-width});
}
}
}
- .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;
background-color: #000;
}
}
+
+ .privacy-concerns-okay {
+ background-color: var(--mainColor);
+ margin-left: 10px;
+ }
}
@media screen and (max-width: 1600px) {
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'
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'
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({
private confirmService: ConfirmService,
private metaService: MetaService,
private authService: AuthService,
+ private userService: UserService,
private serverService: ServerService,
private restExtractor: RestExtractor,
private notifier: Notifier,
return this.authService.getUser()
}
+ get anonymousUser () {
+ return this.userService.getAnonymousUser()
+ }
+
async ngOnInit () {
this.serverConfig = this.serverService.getTmpConfig()
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
isAutoPlayEnabled () {
return (
(this.user && this.user.autoPlayNextVideo) ||
- peertubeSessionStorage.getItem(RecommendedVideosComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO) === 'true'
+ this.anonymousUser.autoPlayNextVideo
)
}
isPlaylistAutoPlayEnabled () {
return (
(this.user && this.user.autoPlayNextVideoPlaylist) ||
- peertubeSessionStorage.getItem(VideoWatchPlaylistComponent.SESSION_STORAGE_AUTO_PLAY_NEXT_VIDEO_PLAYLIST) === 'true'
+ this.anonymousUser.autoPlayNextVideoPlaylist
)
}
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({
SharedModule,
NgbTooltipModule,
QRCodeModule,
- RecommendationsModule,
- InputSwitchModule
+ RecommendationsModule
],
declarations: [
[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>
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;
- }
-}
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',
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
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.')
}
}
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 = {
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',
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private videoService: VideoService,
private hooks: HooksService
) {
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',
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private videoService: VideoService,
private hooks: HooksService
) {
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',
protected router: Router,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private videoService: VideoService,
private hooks: HooksService
) {
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',
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private videoService: VideoService,
private hooks: HooksService
) {
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',
protected route: ActivatedRoute,
protected notifier: Notifier,
protected authService: AuthService,
+ protected userService: UserService,
protected screenService: ScreenService,
+ protected storageService: LocalStorageService,
private videoService: VideoService,
private hooks: HooksService
) {
--- /dev/null
+<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>
+++ /dev/null
-<?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>
+++ /dev/null
-<?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>
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+<!-- 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
+++ /dev/null
-<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
--- /dev/null
+<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
// 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};
flex: auto;
}
+.c-hand {
+ cursor: pointer;
+}
+
@keyframes spin {
from {
transform: scale(1) rotate(0deg);
background-color: var(--mainHoverColor);
opacity: .9;
}
+
+ &::after {
+ display: none;
+ }
}
button {
}
}
+@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;
$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%);
// to be warned of non-existing variables
$variables: (
--mainColor: var(--mainColor),
+ --mainColorLighter: var(--mainColorLighter),
--mainHoverColor: var(--mainHoverColor),
--mainBackgroundColor: var(--mainBackgroundColor),
--mainForegroundColor: var(--mainForegroundColor),
tooltip : 14000,
loadbar : 15000,
modal : 16000,
- notification : 17000,
- hotkeys : 18000
+ help-popover : 17000,
+ notification : 18000,
+ hotkeys : 19000
);
@function z($label) {
// 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
.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 {