diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index e50546633..09b2c15be 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -18,9 +18,7 @@
-
- -
+
diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index 6edf966f9..9eca31320 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 9cffdd31e..48886fd4e 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 000000000..f941e32f8 --- /dev/null +++ b/client/src/app/menu/language-chooser.component.html @@ -0,0 +1,15 @@ + 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 000000000..4574f78c6 --- /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 000000000..3de6a129d --- /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 8e3b295f7..784b5cd85 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -1,70 +1,82 @@ - - + \ 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 c36a7aa36..e61f4acd3 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 c0aea89b3..dded6b4d5 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 000000000..60e6fec00 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 dae0c52c2..96602dc38 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 092f8ed24..f1f755126 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 edb14ff59..254281df5 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 fb01ed572..5511c5435 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 385757fa6..dfffe5487 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 e2b440900..14b02a01d 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 65b78b4fa..8c79ab282 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"