</div>
<div class="sub-header-container">
- <div *ngIf="isMenuDisplayed" class="title-menu-left">
- <my-menu></my-menu>
- </div>
+ <my-menu *ngIf="isMenuDisplayed"></my-menu>
<div class="main-col container-fluid" [ngClass]="{ expanded: isMenuDisplayed === false }">
margin-top: $header-height;
}
-.title-menu-left {
- position: fixed;
- height: calc(100vh - #{$header-height});
- padding: 0;
- width: $menu-width;
-
- .title-menu-left-block.menu {
- height: 100%;
- }
-}
-
.header {
height: $header-height;
position: fixed;
import { VideosModule } from './videos'
import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n'
import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils'
+import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
export function metaFactory (serverService: ServerService): MetaLoader {
return new MetaStaticLoader({
AppComponent,
MenuComponent,
+ LanguageChooserComponent,
HeaderComponent
],
imports: [
--- /dev/null
+<div bsModal #modal="bs-modal" class="modal" tabindex="-1">
+ <div class="modal-dialog">
+ <div class="modal-content">
+
+ <div class="modal-header">
+ <span class="close" aria-hidden="true" (click)="hide()"></span>
+ <h4 i18n class="modal-title">Change the language</h4>
+ </div>
+
+ <div class="modal-body" *ngFor="let lang of languages">
+ <a [href]="buildLanguageLink(lang)">{{ lang.label }}</a>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+@import '_variables';
+@import '_mixins';
+
+.modal-title {
+ text-align: center;
+}
+
+.modal-body {
+ text-align: center;
+
+ a {
+ font-size: 16px;
+ margin-top: 10px;
+ }
+}
\ No newline at end of file
--- /dev/null
+import { Component, ViewChild } from '@angular/core'
+import { ModalDirective } from 'ngx-bootstrap/modal'
+import { I18N_LOCALES } from '../../../../shared'
+
+@Component({
+ selector: 'my-language-chooser',
+ templateUrl: './language-chooser.component.html',
+ styleUrls: [ './language-chooser.component.scss' ]
+})
+export class LanguageChooserComponent {
+ @ViewChild('modal') modal: ModalDirective
+
+ languages: { [ id: string ]: string }[] = []
+
+ constructor () {
+ this.languages = Object.keys(I18N_LOCALES)
+ .map(k => ({ id: k, label: I18N_LOCALES[k] }))
+ }
+
+ show () {
+ this.modal.show()
+ }
+
+ hide () {
+ this.modal.hide()
+ }
+
+ buildLanguageLink (lang: { id: string }) {
+ return window.location.origin + '/' + lang.id
+ }
+
+}
-<menu>
- <div *ngIf="isLoggedIn" class="logged-in-block">
- <a routerLink="/my-account/settings">
- <img [src]="user.accountAvatarUrl" alt="Avatar" />
- </a>
+<div class="menu-wrapper">
+ <menu>
+ <div class="top-menu">
+ <div *ngIf="isLoggedIn" class="logged-in-block">
+ <a routerLink="/my-account/settings">
+ <img [src]="user.accountAvatarUrl" alt="Avatar" />
+ </a>
- <div class="logged-in-info">
- <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
- <div class="logged-in-email">{{ user.email }}</div>
- </div>
+ <div class="logged-in-info">
+ <a routerLink="/my-account/settings" class="logged-in-username">{{ user.account?.displayName }}</a>
+ <div class="logged-in-email">{{ user.email }}</div>
+ </div>
- <div class="logged-in-more" dropdown placement="right" container="body">
- <span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
+ <div class="logged-in-more" dropdown placement="right" container="body">
+ <span class="glyphicon glyphicon-option-vertical" dropdownToggle></span>
- <ul *dropdownMenu class="dropdown-menu">
- <li>
- <a i18n [routerLink]="[ '/accounts', user.account?.nameWithHost ]" class="dropdown-item" title="My public profile">
- My public profile
- </a>
+ <ul *dropdownMenu class="dropdown-menu">
+ <li>
+ <a i18n [routerLink]="[ '/accounts', user.account?.nameWithHost ]" class="dropdown-item" title="My public profile">
+ My public profile
+ </a>
- <a i18n routerLink="/my-account" class="dropdown-item" title="My account">
- My account
- </a>
+ <a i18n routerLink="/my-account" class="dropdown-item" title="My account">
+ My account
+ </a>
- <a i18n (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
- Log out
- </a>
- </li>
- </ul>
- </div>
- </div>
+ <a i18n (click)="logout($event)" class="dropdown-item" title="Log out" href="#">
+ Log out
+ </a>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <div *ngIf="!isLoggedIn" class="button-block">
+ <a i18n routerLink="/login" class="login-button">Login</a>
+ <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
+ </div>
- <div *ngIf="!isLoggedIn" class="button-block">
- <a i18n routerLink="/login" class="login-button">Login</a>
- <a i18n *ngIf="isRegistrationAllowed()" routerLink="/signup" class="create-account-button">Create an account</a>
- </div>
+ <div class="panel-block">
+ <div i18n class="block-title">Videos</div>
- <div class="panel-block">
- <div i18n class="block-title">Videos</div>
+ <a routerLink="/videos/trending" routerLinkActive="active">
+ <span class="icon icon-videos-trending"></span>
+ <ng-container i18n>Trending</ng-container>
+ </a>
- <a routerLink="/videos/trending" routerLinkActive="active">
- <span class="icon icon-videos-trending"></span>
- <ng-container i18n>Trending</ng-container>
- </a>
+ <a routerLink="/videos/recently-added" routerLinkActive="active">
+ <span class="icon icon-videos-recently-added"></span>
+ <ng-container i18n>Recently added</ng-container>
+ </a>
- <a routerLink="/videos/recently-added" routerLinkActive="active">
- <span class="icon icon-videos-recently-added"></span>
- <ng-container i18n>Recently added</ng-container>
- </a>
+ <a routerLink="/videos/local" routerLinkActive="active">
+ <span class="icon icon-videos-local"></span>
+ <ng-container i18n>Local</ng-container>
+ </a>
+ </div>
- <a routerLink="/videos/local" routerLinkActive="active">
- <span class="icon icon-videos-local"></span>
- <ng-container i18n>Local</ng-container>
- </a>
- </div>
+ <div class="panel-block">
+ <div class="block-title">More</div>
- <div class="panel-block">
- <div class="block-title">More</div>
+ <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
+ <span class="icon icon-administration"></span>
+ <ng-container i18n>Administration</ng-container>
+ </a>
- <a *ngIf="userHasAdminAccess" [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
- <span class="icon icon-administration"></span>
- <ng-container i18n>Administration</ng-container>
- </a>
+ <a routerLink="/about" routerLinkActive="active">
+ <span class="icon icon-about"></span>
+ <ng-container i18n>About</ng-container>
+ </a>
+ </div>
+ </div>
+
+ <div class="footer">
+ <span class="language">
+ <span (click)="openLanguageChooser()" i18n-title title="Change the language" class="icon icon-language"></span>
+ </span>
+ </div>
+ </menu>
+</div>
- <a routerLink="/about" routerLinkActive="active">
- <span class="icon icon-about"></span>
- <ng-container i18n>About</ng-container>
- </a>
- </div>
-</menu>
+<my-language-chooser #languageChooserModal></my-language-chooser>
\ No newline at end of file
@import '_variables';
@import '_mixins';
+.menu-wrapper {
+ position: fixed;
+ height: calc(100vh - #{$header-height});
+ padding: 0;
+ width: $menu-width;
+}
+
menu {
background-color: $black-background;
margin: 0;
overflow: hidden;
z-index: 1000;
color: $menu-color;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+
+ .top-menu {
+ flex-grow: 1;
+ }
.logged-in-block {
height: 100px;
a {
display: flex;
align-items: center;
- padding-left: 26px;
+ padding-left: $menu-left-padding;
color: $menu-color;
cursor: pointer;
height: 40px;
}
}
}
+
+ .footer {
+ margin-bottom: 15px;
+ padding-left: $menu-left-padding;
+
+ .language {
+ display: inline-block;
+ color: $menu-bottom-color;
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: $font-semibold;
+
+ .icon {
+ @include icon(28px);
+ opacity: 0.9;
+
+ &.icon-language {
+ position: relative;
+ top: -1px;
+ width: 28px;
+ height: 24px;
+
+ background-image: url('../../assets/images/menu/language.png');
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
+ }
}
-import { Component, OnInit } from '@angular/core'
-import { Router } from '@angular/router'
+import { Component, OnInit, ViewChild } from '@angular/core'
import { UserRight } from '../../../../shared/models/users/user-right.enum'
import { AuthService, AuthStatus, RedirectService, ServerService } from '../core'
import { User } from '../shared/users/user.model'
+import { LanguageChooserComponent } from '@app/menu/language-chooser.component'
@Component({
selector: 'my-menu',
styleUrls: [ './menu.component.scss' ]
})
export class MenuComponent implements OnInit {
+ @ViewChild('languageChooserModal') languageChooserModal: LanguageChooserComponent
+
user: User
isLoggedIn: boolean
userHasAdminAccess = false
this.redirectService.redirectToHomepage()
}
+ openLanguageChooser () {
+ this.languageChooserModal.show()
+ }
+
private computeIsUserHasAdminAccess () {
const right = this.getFirstAdminRightAvailable()
// On small screen, menu is absolute
@media screen and (max-width: 600px) {
- .title-menu-left {
+ .menu-wrapper {
width: 100% !important;
position: absolute !important;
z-index: 10000;
$search-input-width: 375px;
$menu-color: #fff;
+$menu-bottom-color: #C6C6C6;
$menu-width: 240px;
+$menu-left-padding: 26px;
$footer-height: 30px;
$footer-margin: 30px;
"commander": "^2.13.0",
"concurrently": "^3.5.1",
"config": "^1.14.0",
+ "cookie-parser": "^1.4.3",
"cors": "^2.8.1",
"create-torrent": "^3.24.5",
"express": "^4.12.4",
import * as express from 'express'
import * as morgan from 'morgan'
import * as cors from 'cors'
+import * as cookieParser from 'cookie-parser'
process.title = 'peertube'
type: [ 'application/json', 'application/*+json' ],
limit: '500kb'
}))
+// Cookies
+app.use(cookieParser())
// ----------- Views, routes and static files -----------
import { asyncMiddleware } from '../middlewares'
import { VideoModel } from '../models/video/video'
import { VideoPrivacy } from '../../shared/models/videos'
-import { buildFileLocale, getCompleteLocale, getDefaultLocale, is18nLocale } from '../../shared/models'
-import { LOCALE_FILES } from '../../shared/models/i18n/i18n'
+import {
+ buildFileLocale,
+ getCompleteLocale,
+ getDefaultLocale,
+ is18nLocale,
+ LOCALE_FILES,
+ POSSIBLE_LOCALES
+} from '../../shared/models/i18n/i18n'
const clientsRouter = express.Router()
asyncMiddleware(generateWatchHtmlPage)
)
-clientsRouter.use('/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
+clientsRouter.use('' +
+ '/videos/embed', (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.sendFile(embedPath)
})
// Try to provide the right language index.html
clientsRouter.use('/(:language)?', function (req, res) {
if (req.accepts(ACCEPT_HEADERS) === 'html') {
- return res.sendFile(getIndexPath(req, req.params.language))
+ return res.sendFile(getIndexPath(req, res, req.params.language))
}
return res.status(404).end()
// ---------------------------------------------------------------------------
-function getIndexPath (req: express.Request, paramLang?: string) {
+function getIndexPath (req: express.Request, res: express.Response, paramLang?: string) {
let lang: string
// Check param lang validity
if (paramLang && is18nLocale(paramLang)) {
lang = paramLang
+
+ // Save locale in cookies
+ res.cookie('clientLanguage', lang, {
+ secure: CONFIG.WEBSERVER.SCHEME === 'https',
+ sameSite: true,
+ maxAge: 1000 * 3600 * 24 * 90 // 3 months
+ })
+
+ } else if (req.cookies.clientLanguage && is18nLocale(req.cookies.clientLanguage)) {
+ lang = req.cookies.clientLanguage
} else {
- // lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale()
- // Disable auto language for now
- lang = getDefaultLocale()
+ lang = req.acceptsLanguages(POSSIBLE_LOCALES) || getDefaultLocale()
}
return join(__dirname, '../../../client/dist/' + buildFileLocale(lang) + '/index.html')
} else if (validator.isInt(videoId)) {
videoPromise = VideoModel.loadAndPopulateAccountAndServerAndTags(+videoId)
} else {
- return res.sendFile(getIndexPath(req))
+ return res.sendFile(getIndexPath(req, res))
}
let [ file, video ] = await Promise.all([
- readFileBufferPromise(getIndexPath(req)),
+ readFileBufferPromise(getIndexPath(req, res)),
videoPromise
])
const html = file.toString()
// Let Angular application handle errors
- if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req))
+ if (!video || video.privacy === VideoPrivacy.PRIVATE) return res.sendFile(getIndexPath(req, res))
const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
export const LOCALE_FILES = [ 'player', 'server' ]
export const I18N_LOCALES = {
- 'en-US': 'English (US)',
- 'fr-FR': 'Français (France)'
+ 'en-US': 'English',
+ 'fr-FR': 'Français'
}
const I18N_LOCALE_ALIAS = {
export const POSSIBLE_LOCALES = Object.keys(I18N_LOCALES)
.concat(Object.keys(I18N_LOCALE_ALIAS))
-const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
-
export function getDefaultLocale () {
return 'en-US'
}
return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale())
}
+const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l)
export function is18nPath (path: string) {
return possiblePaths.indexOf(path) !== -1
}
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+cookie-parser@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5"
+ dependencies:
+ cookie "0.3.1"
+ cookie-signature "1.0.6"
+
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"