diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index e5054663396c99d4bac7ab35be48520ad5cda7e1..09b2c15be7225cbf9fae9ae6964a5ffe39ff7f24 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -18,9 +18,7 @@ </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 }"> diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 6edf966f99717b6fe2f4e6fda9eec80387517390..9eca313203eb4ee8c15a012eacd57c59a00f4b52 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -9,17 +9,6 @@ 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; diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 9cffdd31e7ccc8fe136202a4b2477d2b6e4980ef..48886fd4e865a3f0620417f898c0242ffbd62757 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -17,6 +17,7 @@ import { SignupModule } from './signup' 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({ @@ -36,6 +37,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { AppComponent, MenuComponent, + LanguageChooserComponent, HeaderComponent ], imports: [ diff --git a/client/src/app/menu/language-chooser.component.html b/client/src/app/menu/language-chooser.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f941e32f81e56adf193a76f1d5c69cbb18b05b45 --- /dev/null +++ b/client/src/app/menu/language-chooser.component.html @@ -0,0 +1,15 @@ +<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> diff --git a/client/src/app/menu/language-chooser.component.scss b/client/src/app/menu/language-chooser.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..4574f78c6970d8eaeaab10aac42eb0098a771258 --- /dev/null +++ b/client/src/app/menu/language-chooser.component.scss @@ -0,0 +1,15 @@ +@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 diff --git a/client/src/app/menu/language-chooser.component.ts b/client/src/app/menu/language-chooser.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3de6a129d356f28156b541a7095be51a7b46349a --- /dev/null +++ b/client/src/app/menu/language-chooser.component.ts @@ -0,0 +1,32 @@ +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 + } + +} diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 8e3b295f77bcab42407db800bbdd27d0cf8df168..784b5cd85705539b375f0f3aabc975dcd8b32373 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -1,70 +1,82 @@ -<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 diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index c36a7aa36c89590780d9f2bf9c6a96eb88c3fbab..e61f4acd33fbce7e3e167c46777f1d8a18af6a37 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -1,6 +1,13 @@ @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; @@ -11,6 +18,13 @@ menu { 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; @@ -100,7 +114,7 @@ menu { a { display: flex; align-items: center; - padding-left: 26px; + padding-left: $menu-left-padding; color: $menu-color; cursor: pointer; height: 40px; @@ -155,4 +169,35 @@ menu { } } } + + .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; + } + } + } + } } diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index c0aea89b33d9bc2d408858ce4ff857eb04d4b324..dded6b4d5a6b6e2453437cddba32b9646d64d9d1 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -1,8 +1,8 @@ -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', @@ -10,6 +10,8 @@ import { User } from '../shared/users/user.model' styleUrls: [ './menu.component.scss' ] }) export class MenuComponent implements OnInit { + @ViewChild('languageChooserModal') languageChooserModal: LanguageChooserComponent + user: User isLoggedIn: boolean userHasAdminAccess = false @@ -90,6 +92,10 @@ export class MenuComponent implements OnInit { this.redirectService.redirectToHomepage() } + openLanguageChooser () { + this.languageChooserModal.show() + } + private computeIsUserHasAdminAccess () { const right = this.getFirstAdminRightAvailable() diff --git a/client/src/assets/images/menu/language.png b/client/src/assets/images/menu/language.png new file mode 100644 index 0000000000000000000000000000000000000000..60e6fec00fd20a3beae24b37729d8f16d1d7d476 Binary files /dev/null and b/client/src/assets/images/menu/language.png differ diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index dae0c52c213e2b654b16ff2ab61abd9739d52865..96602dc38de98bb65d5c293e36a3b14d8e7e2c70 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -288,7 +288,7 @@ table { // 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; diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss index 092f8ed2422f183e9e879f86da7225c91345fec9..f1f7551264508849e215ce9e62418df5a97c7cb7 100644 --- a/client/src/sass/include/_variables.scss +++ b/client/src/sass/include/_variables.scss @@ -22,7 +22,9 @@ $header-border-color: #e9eff6; $search-input-width: 375px; $menu-color: #fff; +$menu-bottom-color: #C6C6C6; $menu-width: 240px; +$menu-left-padding: 26px; $footer-height: 30px; $footer-margin: 30px; diff --git a/package.json b/package.json index edb14ff591c8d8be39ec5a83636f579dba078c02..254281df5060c64c55424f90640847b68f5a8b69 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,7 @@ "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", diff --git a/server.ts b/server.ts index fb01ed572bcdd9ac15ea3b2fef103f73ce257e85..5511c5435856fec72698f20295fa35e754d44267 100644 --- a/server.ts +++ b/server.ts @@ -12,6 +12,7 @@ import * as bodyParser from 'body-parser' import * as express from 'express' import * as morgan from 'morgan' import * as cors from 'cors' +import * as cookieParser from 'cookie-parser' process.title = 'peertube' @@ -112,6 +113,8 @@ app.use(bodyParser.json({ type: [ 'application/json', 'application/*+json' ], limit: '500kb' })) +// Cookies +app.use(cookieParser()) // ----------- Views, routes and static files ----------- diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 385757fa6bb45729433b142458f918e9a815589e..dfffe5487d024b9781f3f6e115ff835a7af8545d 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -7,8 +7,14 @@ import { ACCEPT_HEADERS, CONFIG, EMBED_SIZE, OPENGRAPH_AND_OEMBED_COMMENT, STATI 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() @@ -22,7 +28,8 @@ clientsRouter.use('/videos/watch/:id', 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) }) @@ -63,7 +70,7 @@ clientsRouter.use('/client/*', (req: express.Request, res: express.Response, nex // 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() @@ -77,16 +84,24 @@ export { // --------------------------------------------------------------------------- -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') @@ -181,18 +196,18 @@ async function generateWatchHtmlPage (req: express.Request, res: express.Respons } 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) diff --git a/shared/models/i18n/i18n.ts b/shared/models/i18n/i18n.ts index e2b4409008cebb5e4d62cba366e2fea0a1a99158..14b02a01d57129933ae6a41e8e87241940769932 100644 --- a/shared/models/i18n/i18n.ts +++ b/shared/models/i18n/i18n.ts @@ -1,8 +1,8 @@ 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 = { @@ -13,8 +13,6 @@ 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' } @@ -23,6 +21,7 @@ export function isDefaultLocale (locale: string) { return getCompleteLocale(locale) === getCompleteLocale(getDefaultLocale()) } +const possiblePaths = POSSIBLE_LOCALES.map(l => '/' + l) export function is18nPath (path: string) { return possiblePaths.indexOf(path) !== -1 } diff --git a/yarn.lock b/yarn.lock index 65b78b4fa504191c718569f6cae1953b07e8782a..8c79ab282a9a8f50280e8e21c7a046ac1fcd0a64 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1590,6 +1590,13 @@ content-type@~1.0.1, content-type@~1.0.4: 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"