├── .vscode
├── settings.json
└── launch.json
├── _redirects
├── .eslintignore
├── static
├── logo.png
├── logo-16.png
├── logo-32.png
├── logo-192.png
├── logo-512.png
├── fallback_background.jpg
├── manifest.json
├── icons
│ ├── google-icon.svg
│ ├── github-icon.svg
│ ├── discord-icon.svg
│ ├── twitter-icon.svg
│ └── kitsu-icon.svg
└── logo-animated.svg
├── src
├── mse
│ ├── TODO
│ └── MegaMediaSource.js
├── graphql
│ └── index.js
├── store
│ ├── index.js
│ └── app.js
├── utils
│ ├── cookies.js
│ └── auth.js
├── components
│ ├── ProgressiveImg.vue
│ ├── Upload.vue
│ ├── layout
│ │ ├── Loader.vue
│ │ ├── navbar
│ │ │ ├── AuthMenu.vue
│ │ │ ├── LanguageSelect.vue
│ │ │ ├── AuthMenuLinks.vue
│ │ │ ├── Notifications.vue
│ │ │ ├── Navbar.vue
│ │ │ └── MobileNavbar.vue
│ │ ├── Search.vue
│ │ ├── Layout.vue
│ │ ├── AuthLayout.vue
│ │ ├── SearchResults.vue
│ │ └── LayoutFooter.vue
│ ├── Rating.vue
│ ├── player
│ │ ├── PlayerSlider.vue
│ │ └── GlobalPlayer.js
│ ├── user
│ │ └── User.vue
│ ├── media
│ │ ├── MediaList.vue
│ │ └── Comment.vue
│ ├── cover
│ │ ├── Cover.vue
│ │ └── CoverList.vue
│ ├── Floating.vue
│ └── room
│ │ └── Chatbox.vue
├── sw.js
├── index.js
├── credentials.js
├── stylus
│ ├── colors.styl
│ ├── main.styl
│ ├── transitions.styl
│ └── theme.styl
├── main.js
├── views
│ ├── Translations.vue
│ ├── user
│ │ ├── Followers.vue
│ │ ├── settings
│ │ │ ├── Account.vue
│ │ │ └── Connections.vue
│ │ ├── Follows.vue
│ │ ├── library
│ │ │ ├── AnimeStatus.vue
│ │ │ ├── Search.vue
│ │ │ └── Follows.vue
│ │ ├── Settings.vue
│ │ └── Library.vue
│ ├── news
│ │ ├── NewsList.vue
│ │ └── News.vue
│ ├── 404.vue
│ ├── Author.vue
│ ├── auth
│ │ ├── Login.vue
│ │ └── SignUp.vue
│ ├── Index.vue
│ └── Search.vue
├── App.vue
├── i18n
│ └── index.js
├── router
│ └── index.js
└── assets
│ └── loader.svg
├── tests
└── e2e
│ ├── mock
│ ├── static
│ │ ├── cover.png
│ │ └── background.gif
│ ├── mocks
│ │ ├── Url.js
│ │ ├── DateTime.js
│ │ ├── index.js
│ │ ├── Anime.js
│ │ ├── User.js
│ │ └── Query.js
│ └── index.js
│ ├── utils
│ └── istanbul.js
│ └── Navbar.test.js
├── .editorconfig
├── config
├── prod.env.js
├── test.env.js
├── dev.env.js
└── index.js
├── .postcssrc.js
├── .gitignore
├── .eslintrc.js
├── mockSchema.js
├── .babelrc
├── README.md
├── index.html
└── package.json
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | }
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 |
--------------------------------------------------------------------------------
/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/logo.png
--------------------------------------------------------------------------------
/static/logo-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/logo-16.png
--------------------------------------------------------------------------------
/static/logo-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/logo-32.png
--------------------------------------------------------------------------------
/static/logo-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/logo-192.png
--------------------------------------------------------------------------------
/static/logo-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/logo-512.png
--------------------------------------------------------------------------------
/src/mse/TODO:
--------------------------------------------------------------------------------
1 | Don't seek if it's in the same TimeRange
2 | Don't redownload downloaded regions after seeking
--------------------------------------------------------------------------------
/static/fallback_background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/static/fallback_background.jpg
--------------------------------------------------------------------------------
/tests/e2e/mock/static/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/tests/e2e/mock/static/cover.png
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/Url.js:
--------------------------------------------------------------------------------
1 | const faker = require("faker");
2 |
3 | module.exports = () => faker.internet.url();
4 |
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/DateTime.js:
--------------------------------------------------------------------------------
1 | const faker = require("faker");
2 |
3 | module.exports = () => faker.date.past();
4 |
--------------------------------------------------------------------------------
/tests/e2e/mock/static/background.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Popcorn-moe/Web/HEAD/tests/e2e/mock/static/background.gif
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | Anime: require("./Anime"),
3 | DateTime: require("./DateTime"),
4 | Query: require("./Query"),
5 | Url: require("./Url"),
6 | User: require("./User")
7 | };
8 |
--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | NODE_ENV: '"production"',
3 | AUTH_URL: `"${process.env.AUTH_URL || "https://auth.popcorn.moe"}"`,
4 | API_URL: `"${process.env.API_URL || "https://api.popcorn.moe"}"`
5 | };
6 |
--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 |
3 | module.exports = {
4 | "plugins": {
5 | // to edit target browsers: use "browserslist" field in package.json
6 | "autoprefixer": {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/Anime.js:
--------------------------------------------------------------------------------
1 | const faker = require("faker");
2 |
3 | module.exports = () => ({
4 | names: [faker.commerce.productName()],
5 | cover: "http://localhost:3029/cover.png",
6 | background: "http://localhost:3029/background.gif"
7 | });
8 |
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/User.js:
--------------------------------------------------------------------------------
1 | const faker = require("faker");
2 |
3 | module.exports = () => ({
4 | login: faker.name.findName(),
5 | group: "VIEWER",
6 | newsletter: true,
7 | avatar: faker.image.avatar(),
8 | email: faker.internet.email()
9 | });
10 |
--------------------------------------------------------------------------------
/config/test.env.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const devEnv = require("./dev.env");
3 |
4 | module.exports = merge(devEnv, {
5 | API_URL: process.env.API_URL
6 | ? `'${process.env.API_URL}'`
7 | : "'http://localhost:3029'",
8 | NODE_ENV: '"test"'
9 | });
10 |
--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | const merge = require("webpack-merge");
2 | const prodEnv = require("./prod.env");
3 |
4 | module.exports = merge(prodEnv, {
5 | NODE_ENV: '"development"',
6 | AUTH_URL: `"${process.env.AUTH_URL || "http://localhost:3031"}"`,
7 | API_URL: `"${process.env.API_URL || "http://localhost:3030"}"`
8 | });
9 |
--------------------------------------------------------------------------------
/src/graphql/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueApollo from "vue-apollo";
3 | import createClient from "@popcorn.moe/apollo";
4 |
5 | Vue.use(VueApollo);
6 |
7 | export const client = createClient();
8 |
9 | window.graphql = client;
10 |
11 | export default new VueApollo({
12 | defaultClient: client
13 | });
14 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Vuex from "vuex";
3 |
4 | import app from "./app";
5 | export { onLoad } from "./app";
6 |
7 | Vue.use(Vuex);
8 |
9 | const store = new Vuex.Store({
10 | strict: true, // process.env.NODE_ENV !== 'production',
11 | ...app
12 | });
13 |
14 | export default store;
15 |
--------------------------------------------------------------------------------
/src/utils/cookies.js:
--------------------------------------------------------------------------------
1 | export default getCookiesMap(document.cookie);
2 |
3 | export function getCookiesMap(cookiesString) {
4 | return cookiesString
5 | .split(";")
6 | .map(cookieString => cookieString.trim().split("="))
7 | .reduce((acc, [name, ...values]) => {
8 | acc[name] = values.join("=");
9 | return acc;
10 | }, {});
11 | }
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | dist2/
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | yarn.lock
9 |
10 | # Editor directories and files
11 | .idea
12 | *.suo
13 | *.ntvs*
14 | *.njsproj
15 | .sln
16 | *.iml
17 |
18 | # custom env run file
19 | run.sh
20 |
21 | # testcafe screenshots
22 | screenshots
23 |
24 | .bin
25 | html-report
--------------------------------------------------------------------------------
/src/components/ProgressiveImg.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
21 |
--------------------------------------------------------------------------------
/src/sw.js:
--------------------------------------------------------------------------------
1 | workbox.clientsClaim();
2 | workbox.skipWaiting();
3 |
4 | workbox.routing.registerRoute(
5 | new RegExp("^https://fonts.(?:googleapis|gstatic).com/(.*)"),
6 | workbox.strategies.cacheFirst()
7 | );
8 |
9 | workbox.routing.registerNavigationRoute("/index.html", {
10 | blacklist: [/static/, /report\.html/, /VERSION|COMMITHASH/]
11 | });
12 |
13 | workbox.precaching.precacheAndRoute(self.__precacheManifest);
14 |
--------------------------------------------------------------------------------
/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Popcorn.moe",
3 | "short_name": "Popcorn.moe",
4 | "icons": [
5 | {
6 | "src": "/static/logo-192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/static/logo-512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/",
17 | "display": "standalone",
18 | "background_color": "#ffffff",
19 | "theme_color": "#f6416c"
20 | }
--------------------------------------------------------------------------------
/tests/e2e/mock/mocks/Query.js:
--------------------------------------------------------------------------------
1 | const { MockList } = require("graphql-tools");
2 |
3 | module.exports = () => ({
4 | animes(root, { limit }, context) {
5 | return new MockList(limit);
6 | },
7 | searchUser(root, { name, limit }, context) {
8 | if (name) return new MockList(limit);
9 | else return [];
10 | },
11 | searchAnime(root, { name, limit }, context) {
12 | if (name) return new MockList(limit);
13 | else return [];
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/tests/e2e/utils/istanbul.js:
--------------------------------------------------------------------------------
1 | import { ClientFunction } from "testcafe";
2 | import { Collector, Report } from "istanbul";
3 | import exitHook from "exit-hook";
4 |
5 | const getCoverage = ClientFunction(() => window.__coverage__);
6 | const collector = new Collector();
7 |
8 | exitHook(() => {
9 | Report.create("html").writeReport(collector, true);
10 | Report.create("text").writeReport(collector, true);
11 | });
12 |
13 | export default async function(t) {
14 | collector.add(await getCoverage());
15 | }
16 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | parserOptions: {
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | // required to lint *.vue files
13 | plugins: [
14 | 'html'
15 | ],
16 | // add your custom rules here
17 | 'rules': {
18 | // allow debugger during development
19 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import("./main.js").catch(console.error); // Async load
2 |
3 | if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
4 | Promise.all([
5 | navigator.serviceWorker.ready,
6 | fetch("/VERSION").then(res => res.text())
7 | ]).then(([reg, version]) => {
8 | if (version !== VERSION) {
9 | console.log(
10 | "Updating service worker old:",
11 | version,
12 | "new:",
13 | VERSION,
14 | "..."
15 | );
16 | return reg.update().then(() => console.log("Service worker updated"));
17 | }
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/mockSchema.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const fs = require("fs");
3 | const { introspectionQuery } = require("graphql");
4 |
5 | fetch(`http://localhost:3030/graphql`, {
6 | method: "POST",
7 | headers: { "Content-Type": "application/json" },
8 | body: JSON.stringify({
9 | query: introspectionQuery
10 | })
11 | })
12 | .then(result => result.json())
13 | .then(result => {
14 | fs.writeFile(
15 | "./tests/e2e/mock/schema.json",
16 | JSON.stringify(result.data, null, 2),
17 | err => {
18 | if (err) console.error("Error writing schema file", err);
19 | console.log("Schema successfully extracted!");
20 | }
21 | );
22 | })
23 | .catch(console.error);
24 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false,
5 | "targets": {
6 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
7 | }
8 | }],
9 | "stage-2"
10 | ],
11 | "plugins": [
12 | "transform-runtime",
13 | ["transform-imports", {
14 | "vuetify/es5/components": {
15 | "transform": "vuetify/es5/components/${member}",
16 | "preventFullImport": true
17 | },
18 | "vuetify": {
19 | "transform": "vuetify/es5/components/${member}",
20 | "preventFullImport": true
21 | }
22 | }]
23 | ],
24 | "env": {
25 | "test": {
26 | "plugins": ["istanbul"]
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Upload.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
18 |
19 |
39 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "chrome",
9 | "request": "attach",
10 | "port": 9222,
11 | "name": "Launch",
12 | "url": "http://localhost:8080/",
13 | "urlFilter": "http://localhost:8080/*",
14 | "webRoot": "${workspaceFolder}",
15 | "sourceMapPathOverrides": {
16 | "webpack:///*": "${workspaceFolder}/*"
17 | }
18 | }
19 | ]
20 | }
--------------------------------------------------------------------------------
/src/components/layout/Loader.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
40 |
--------------------------------------------------------------------------------
/src/credentials.js:
--------------------------------------------------------------------------------
1 | import { login, ssoLogin, PROVIDERS } from "./utils/auth";
2 | import store from "./store";
3 |
4 | const INVERTED_PROVIDERS = Object.entries(PROVIDERS).reduce(
5 | (c, [key, value]) => ((c[value] = key), c),
6 | {}
7 | );
8 |
9 | export default function autoSignIn() {
10 | if (window.PasswordCredential || window.FederatedCredential) {
11 | navigator.credentials
12 | .get({
13 | password: true,
14 | federated: {
15 | providers: Object.values(PROVIDERS)
16 | }
17 | })
18 | .then(cred => {
19 | if (cred) {
20 | switch (cred.type) {
21 | case "password":
22 | login(cred.id, cred.password);
23 | break;
24 | case "federated":
25 | ssoLogin(INVERTED_PROVIDERS[cred.provider]);
26 | break;
27 | }
28 | }
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/stylus/colors.styl:
--------------------------------------------------------------------------------
1 | color-class(name, $color) {
2 | .{name} {
3 | background-color: $color !important;
4 | border-color: $color !important;
5 | color: #fff !important;
6 | }
7 |
8 | .{name}--text {
9 | color: $color !important;
10 | }
11 | }
12 |
13 | $discord-color = #8c9eff
14 | $google-color = #b8b8b8
15 | $kitsu-color = #f75239
16 | $twitter-color = #36a2e4
17 |
18 | color-class('main-color', $main-color)
19 | color-class('discord-color', $discord-color)
20 | color-class('google-color', $google-color)
21 | color-class('kitsu-color', $kitsu-color)
22 | color-class('twitter-color', $twitter-color)
23 | color-class('discord-color--darken', darken($discord-color, 10))
24 | color-class('google-color--darken', darken($google-color, 10))
25 | color-class('kitsu-color--darken', darken($kitsu-color, 10))
26 | color-class('twitter-color--darken', darken($twitter-color, 10))
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/e2e/mock/index.js:
--------------------------------------------------------------------------------
1 | const { buildClientSchema } = require("graphql");
2 | const introspectionResult = require("./schema.json");
3 | const { addMockFunctionsToSchema } = require("graphql-tools");
4 | const express = require("express");
5 | const cors = require("cors");
6 | const graphqlHTTP = require("express-graphql");
7 | const { graphql } = require("graphql");
8 | const mocks = require("./mocks");
9 | const faker = require("faker");
10 | const { join } = require("path");
11 |
12 | const schema = buildClientSchema(introspectionResult);
13 |
14 | addMockFunctionsToSchema({ schema, mocks });
15 |
16 | const app = express();
17 | app.use(express.static(join(__dirname, "static")));
18 | app.use(
19 | cors({
20 | origin: ["http://localhost:8042"],
21 | credentials: true
22 | })
23 | );
24 |
25 | app.use((req, res, next) => {
26 | faker.seed(123);
27 | next();
28 | });
29 | app.use("/graphql", graphqlHTTP({ schema }));
30 |
31 | app.listen(3029);
32 |
33 | process.on("SIGTERM", () => process.exit(0));
34 |
--------------------------------------------------------------------------------
/static/icons/google-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/stylus/main.styl:
--------------------------------------------------------------------------------
1 | @import './theme'
2 | @import '~vuetify/src/stylus/app'
3 | @import './transitions'
4 | @import './colors'
5 |
6 | .uppercase {
7 | text-transform: uppercase;
8 | }
9 |
10 | .vertical-center {
11 | position: relative;
12 | top: 50%;
13 | transform: translateY(-50%);
14 | }
15 |
16 | $anime := {
17 | padding: 15px
18 | width: 150px
19 | img-height: 210px
20 | img-border: 4px
21 | font-size: 16px
22 | rating-size: 25px
23 | description-size: 24px
24 | subtitle-size: 19px
25 | }
26 |
27 | $anime-height = $anime.img-height + $anime.img-border + $anime.padding + $anime.subtitle-size + $anime.font-size * 2.5 + $anime.rating-size + $anime.description-size + $anime.padding
28 |
29 | .application.theme--dark {
30 | h1, h2, h3, h4, h5, h6 {
31 | color: #D3D3D3;
32 | }
33 | }
34 |
35 | a, .list__tile--active {
36 | color: inherit;
37 | }
38 |
39 | .icon.material-icons {
40 | display: inline-flex;
41 | }
42 |
43 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons')
--------------------------------------------------------------------------------
/static/icons/github-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/static/icons/discord-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import { Vuetify } from "vuetify";
3 | import colors from "vuetify/es5/util/colors";
4 | import VueHead from "vue-head";
5 | import App from "./App";
6 | import router from "./router";
7 | import store, { onLoad } from "./store";
8 | import apolloProvider from "./graphql";
9 | import i18n from "./i18n";
10 | import autoSignIn from "./credentials";
11 |
12 | const mainColor = "#f6416c";
13 | const secondaryColor = "#fddd84";
14 |
15 | Vue.config.productionTip = false;
16 | Vue.use(Vuetify, {
17 | theme: {
18 | primary: mainColor,
19 | accent: mainColor,
20 | secondary: secondaryColor,
21 | info: colors.blue.base,
22 | warning: colors.amber.base,
23 | error: colors.red.accent2,
24 | success: colors.green.base
25 | }
26 | });
27 |
28 | Vue.use(VueHead, {
29 | separator: "-",
30 | complement: "Popcorn.moe"
31 | });
32 |
33 | /* eslint-disable no-new */
34 | window.vue = new Vue({
35 | el: "#app",
36 | router,
37 | store,
38 | provide: apolloProvider.provide(),
39 | i18n,
40 | render: h => h(App)
41 | });
42 |
43 | onLoad(store).then(() => {
44 | if (!store.state.isAuth) autoSignIn();
45 | });
46 |
--------------------------------------------------------------------------------
/src/stylus/transitions.styl:
--------------------------------------------------------------------------------
1 | .tab-transition-enter-active {
2 | animation: slideRightIn 1s
3 | }
4 |
5 | .tab-transition-leave-active {
6 | animation: slideRightOut 1s;
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | right: 0;
11 | bottom: 0;
12 | width: 100%
13 | }
14 |
15 | @keyframes slideRightIn {
16 | 0% {
17 | transform: translateX(100%)
18 | }
19 |
20 | to {
21 | transform: translateX(0)
22 | }
23 | }
24 |
25 | @keyframes slideRightOut {
26 | 0% {
27 | transform: translateX(0)
28 | }
29 |
30 | to {
31 | transform: translateX(-100%)
32 | }
33 | }
34 |
35 | .tab-reverse-transition-enter-active {
36 | animation: slideLeftIn 1s
37 | }
38 |
39 | .tab-reverse-transition-leave-active {
40 | animation: slideLeftOut 1s;
41 | position: absolute;
42 | top: 0;
43 | left: 0;
44 | right: 0;
45 | bottom: 0;
46 | width: 100%
47 | }
48 |
49 | @keyframes slideLeftIn {
50 | 0% {
51 | transform: translateX(-100%)
52 | }
53 |
54 | to {
55 | transform: translateX(0)
56 | }
57 | }
58 |
59 | @keyframes slideLeftOut {
60 | 0% {
61 | transform: translateX(0)
62 | }
63 |
64 | to {
65 | transform: translateX(100%)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/AuthMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ me.login }}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
61 |
62 |
70 |
--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
1 | // see http://vuejs-templates.github.io/webpack for documentation.
2 | const { resolve } = require("path");
3 | const { NODE_ENV } = process.env;
4 |
5 | module.exports = {
6 | build: {
7 | env: require("./prod.env"),
8 | index: resolve(__dirname, "../dist/index.html"),
9 | assetsRoot: resolve(__dirname, "../dist"),
10 | assetsSubDirectory: "static",
11 | assetsPublicPath: "/",
12 | devtool: "source-map",
13 | productionSourceMap: true,
14 | // Gzip off by default as many popular static hosts such as
15 | // Surge or Netlify already gzip all static assets for you.
16 | // Before setting to `true`, make sure to:
17 | // npm install --save-dev compression-webpack-plugin
18 | productionGzip: false,
19 | productionGzipExtensions: ["js", "css"]
20 | },
21 | dev: {
22 | env: NODE_ENV === "test" ? require("./test.env") : require("./dev.env"),
23 | port: NODE_ENV === "test" ? 8042 : 8080,
24 | autoOpenBrowser: NODE_ENV !== "test",
25 | devtool: NODE_ENV === "test" ? "inline-source-map" : "source-map",
26 | assetsSubDirectory: "static",
27 | assetsPublicPath: "/",
28 | proxyTable: {},
29 | // CSS Sourcemaps off by default because relative paths are "buggy"
30 | // with this option, according to the CSS-Loader README
31 | // (https://github.com/webpack/css-loader#sourcemaps)
32 | // In our experience, they generally work as expected,
33 | // just be aware of this issue when enabling this option.
34 | cssSourceMap: false
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/src/views/Translations.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ messages }}
4 |
5 |
6 |
7 |
54 |
--------------------------------------------------------------------------------
/src/views/user/Followers.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cet utilisateur n'as aucuns followers
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
49 |
50 |
75 |
--------------------------------------------------------------------------------
/static/icons/twitter-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/views/user/settings/Account.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Profile
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Change Password
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
71 |
72 |
74 |
--------------------------------------------------------------------------------
/src/views/user/Follows.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Cet utilisateur ne follow personne
6 |
7 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
58 |
59 |
84 |
--------------------------------------------------------------------------------
/src/views/user/library/AnimeStatus.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ajoutez des animes a votre librairie
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
63 |
64 |
65 |
75 |
--------------------------------------------------------------------------------
/src/components/Rating.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
16 |
21 | {{ r.name }}
22 |
23 |
24 |
25 |
26 |
53 |
54 |
82 |
--------------------------------------------------------------------------------
/src/components/player/PlayerSlider.vue:
--------------------------------------------------------------------------------
1 |
60 |
61 |
62 |
83 |
--------------------------------------------------------------------------------
/src/components/layout/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 | arrow_back
10 |
11 |
18 |
19 |
20 |
21 |
22 | search
23 |
24 |
25 |
26 |
27 |
72 |
73 |
86 |
--------------------------------------------------------------------------------
/src/views/user/library/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aucuns resultat trouvé
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
74 |
75 |
78 |
--------------------------------------------------------------------------------
/src/views/news/NewsList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
78 |
79 |
82 |
--------------------------------------------------------------------------------
/static/icons/kitsu-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/components/user/User.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ user.login }}
8 |
9 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
10 |
11 |
12 |
13 |
14 |
37 |
38 |
39 |
67 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Popcorn.moe Web
2 |
3 | 
4 |
5 | ## Getting Started
6 |
7 | These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
8 |
9 | ### Prerequisites
10 |
11 | - [Api Service](https://github.com/Popcorn-moe/Api) (Optional)
12 | - [Auth Service](https://github.com/Popcorn-moe/Auth) (Optional)
13 |
14 | ### Installing
15 |
16 | ```bash
17 | # install dependencies
18 | npm install
19 |
20 | # serve with hot reload at localhost:8080
21 | npm start
22 |
23 | # serve with hot reload at localhost:8080 by using prod environment (Use this if you don't installed localy Auth and Api Service)
24 | API_URL="https://api.popcorn.moe" AUTH_URL="https://auth.popcorn.moe" npm start
25 |
26 | # build for production
27 | npm run build
28 | ```
29 |
30 | ## Running the tests
31 |
32 | ```bash
33 | npm run test
34 | ```
35 |
36 | ## Built With
37 |
38 | * [Vuetify](https://vuetifyjs.com/) - The web framework used
39 |
40 | ## Contributing
41 |
42 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
43 |
44 | ## Authors
45 |
46 | * **David Duarte** - *Initial work* - [DeltaEvo](https://github.com/DeltaEvo)
47 | * **Lucas Lelièvre** - *Initial work* - [loucass003](https://github.com/loucass003)
48 | * **SkyBeast** - *Some commits to show us that he is working* - [SkyBeastMC](https://github.com/SkyBeastMC)
49 |
50 | See also the list of [contributors](https://github.com/Popcorn-moe/Web/contributors) who participated in this project.
51 |
52 | ## License
53 |
54 | This project is licensed under the GPLv3 License - see the [LICENSE.md](LICENSE.md) file for details
55 |
56 | ## Acknowledgments
57 |
58 | * **valmax72** - *For the awesome logo and design* - [valmax72](https://github.com/valmax72)
--------------------------------------------------------------------------------
/src/components/layout/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | menu
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
63 |
64 |
76 |
--------------------------------------------------------------------------------
/src/components/player/GlobalPlayer.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VideoPlayer from "./VideoPlayer.vue";
3 |
4 | const VIDEO_PROPS = Array.isArray(VideoPlayer.props)
5 | ? VideoPlayer.props
6 | : Object.keys(VideoPlayer.props);
7 |
8 | export const videoPlayer = {
9 | owner: null,
10 | media: null,
11 | destroyed: true,
12 | paused: true,
13 | destroy() {
14 | this.destroyed = true;
15 | }
16 | };
17 |
18 | export default {
19 | props: ["owner", "media"].concat(VIDEO_PROPS),
20 | render(h) {
21 | return h("div");
22 | },
23 | mounted() {
24 | if (videoPlayer.owner != null)
25 | throw new Error(`Already owned by ${videoPlayer.owner}`);
26 |
27 | if (
28 | this.value &&
29 | videoPlayer.instance &&
30 | this.value != videoPlayer.instance.value
31 | ) {
32 | if (!videoPlayer.destroyed) videoPlayer.destroy();
33 | videoPlayer.instance.$destroy();
34 | videoPlayer.instance = null;
35 | videoPlayer.paused = true;
36 | }
37 |
38 | if (!videoPlayer.instance) {
39 | videoPlayer.instance = new Vue(VideoPlayer);
40 | VIDEO_PROPS.forEach(prop => (videoPlayer.instance[prop] = this[prop]));
41 | videoPlayer.instance.$mount();
42 | videoPlayer.destroyed = false;
43 | videoPlayer.media = this.media;
44 | }
45 |
46 | this.$el.appendChild(videoPlayer.instance.$el);
47 | videoPlayer.instance.paused = videoPlayer.instance.$refs.video.paused; //Resync chrome bug
48 | if (videoPlayer.paused !== videoPlayer.instance.paused)
49 | videoPlayer.instance.togglePlay();
50 | videoPlayer.owner = this.owner;
51 | },
52 | beforeDestroy() {
53 | videoPlayer.paused = videoPlayer.instance.paused;
54 | this.$el.removeChild(videoPlayer.instance.$el);
55 |
56 | if (videoPlayer.destroyed || !videoPlayer.instance.hasPlayed) {
57 | if (!videoPlayer.destroyed) videoPlayer.destroy();
58 | videoPlayer.instance.$destroy();
59 | videoPlayer.instance = null;
60 | videoPlayer.paused = true;
61 | }
62 | videoPlayer.owner = null;
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/store/app.js:
--------------------------------------------------------------------------------
1 | import cookies from "../utils/cookies";
2 | import { exchangeSSOToken, isLoggedIn } from "../utils/auth";
3 |
4 | export const SET_DARK_THEME = "DARK_THEME";
5 | export const TOGGLE_DRAWER = "TOGGLE_DRAWER";
6 | export const IS_AUTH = "IS_AUTH";
7 | export const IS_LOADING = "IS_LOADING";
8 |
9 | const state = {
10 | darkTheme: localStorage.getItem("darkTheme") === "true",
11 | drawer: (localStorage.getItem("drawer") || "true") === "true",
12 | isLoading: true,
13 | isAuth: null
14 | };
15 |
16 | export function onLoad(store) {
17 | window.addEventListener(
18 | "storage",
19 | event => {
20 | switch (event.key) {
21 | case "darkTheme":
22 | store.commit(SET_DARK_THEME, event.newValue === "true");
23 | break;
24 | case "drawer":
25 | store.commit(TOGGLE_DRAWER, event.newValue === "true");
26 | break;
27 | }
28 | },
29 | false
30 | );
31 | if (cookies.ssoExchange) {
32 | return exchangeSSOToken(cookies.ssoExchange);
33 | } else if (localStorage.getItem("csrf")) {
34 | return isLoggedIn().then(isAuth => store.dispatch("setIsAuth", isAuth));
35 | } else {
36 | store.dispatch("setIsAuth", false);
37 | return Promise.resolve();
38 | }
39 | }
40 |
41 | const mutations = {
42 | [SET_DARK_THEME](state, darkTheme) {
43 | state.darkTheme = darkTheme;
44 | },
45 | [TOGGLE_DRAWER](state, drawer) {
46 | state.drawer = drawer;
47 | },
48 | [IS_AUTH](state, isAuth) {
49 | state.isAuth = isAuth;
50 | },
51 | [IS_LOADING](state, isLoading) {
52 | state.isLoading = isLoading;
53 | }
54 | };
55 |
56 | const actions = {
57 | setDarkTheme({ commit }, darkTheme) {
58 | commit(SET_DARK_THEME, darkTheme);
59 | localStorage.setItem("darkTheme", darkTheme);
60 | },
61 | toggleDrawer({ commit }, drawer) {
62 | commit(TOGGLE_DRAWER, drawer);
63 | localStorage.setItem("drawer", drawer);
64 | },
65 | setIsAuth({ commit }, isAuth) {
66 | commit(IS_AUTH, isAuth);
67 | }
68 | };
69 |
70 | const getters = {
71 | darkTheme: ({ darkTheme }) => darkTheme,
72 | drawer: ({ drawer }) => drawer,
73 | isAuth: ({ isAuth }) => isAuth,
74 | isLoading: ({ isLoading }) => isLoading
75 | };
76 |
77 | export default {
78 | state,
79 | mutations,
80 | actions,
81 | getters
82 | };
83 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/LanguageSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
89 |
90 |
96 |
--------------------------------------------------------------------------------
/src/views/user/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 | arrow_back
14 |
15 |
16 |
17 |
18 | User Settings
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
77 |
78 |
91 |
--------------------------------------------------------------------------------
/src/components/layout/AuthLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | arrow_back
5 |
6 |
7 | refresh
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
53 |
54 |
102 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
13 |
14 |
15 |
16 |
17 |
18 |
93 |
--------------------------------------------------------------------------------
/src/views/news/News.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
85 |
86 |
105 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/AuthMenuLinks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | face
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | local_library
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | group
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | settings
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
72 |
73 |
83 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | Popcorn.moe
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | This website require javascript to work
45 |
46 |
47 |
57 | ${require('!!html-loader!./src/assets/loader.svg')}
58 |
62 |
63 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/components/layout/SearchResults.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
15 |
16 | {{ user.login }}
17 | {{ user.status }}
18 |
19 |
20 |
21 |
22 |
23 | Animes
24 |
25 |
26 |
27 |
28 |
29 | {{ anime.names[0] }}
30 | {{ anime.release_date }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
114 |
115 |
118 |
--------------------------------------------------------------------------------
/src/components/layout/LayoutFooter.vue:
--------------------------------------------------------------------------------
1 |
2 |
63 |
64 |
65 |
103 |
104 |
106 |
--------------------------------------------------------------------------------
/src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueI18n from "vue-i18n";
3 |
4 | Vue.use(VueI18n);
5 |
6 | const i18n = new VueI18n({
7 | locale:
8 | localStorage.getItem("locale") ||
9 | (navigator.languages ? navigator.languages[0] : navigator.language),
10 | messages: {
11 | en: {
12 | route: {
13 | index: "Index",
14 | search: "Search",
15 | news: "News",
16 | auth: {
17 | profile: "Profile",
18 | library: "Library",
19 | follows: "Follows",
20 | followers: "Followers",
21 | settings: "Settings",
22 | logout: "Logout"
23 | },
24 | user_settings: {
25 | account: "My Account",
26 | connections: "Connections"
27 | },
28 | user_library: {
29 | watching: "Currently Watching",
30 | want_to_watch: "Want to watch",
31 | completed: "Completed",
32 | on_hold: "On Hold",
33 | dropped: "Dropped"
34 | }
35 | },
36 | anime_status: {
37 | PENDING: "Currently airing",
38 | FINISHED: "Finished",
39 | NOT_STARTED: "Not started"
40 | },
41 | navbar: {
42 | dark: "Dark"
43 | }
44 | },
45 | fr: {
46 | route: {
47 | index: "Accueil",
48 | search: "Rechercher",
49 | news: "Nouveautés",
50 | auth: {
51 | profile: "Profil",
52 | library: "Librairie",
53 | follows: "Abonnements",
54 | followers: "Abonnés",
55 | settings: "Paramètres",
56 | logout: "Se deconnecter"
57 | },
58 | user_settings: {
59 | account: "Mon Compte",
60 | connections: "Comptes"
61 | },
62 | user_library: {
63 | watching: "En cours",
64 | want_to_watch: "Want to watch",
65 | completed: "Completed",
66 | on_hold: "On Hold",
67 | dropped: "Dropped"
68 | }
69 | },
70 | anime_status: {
71 | PENDING: "En cours",
72 | FINISHED: "Terminé",
73 | NOT_STARTED: "Non démarré"
74 | },
75 | navbar: {
76 | dark: "Foncé"
77 | }
78 | }
79 | },
80 | fallbackLocale: "en"
81 | });
82 |
83 | export default i18n;
84 |
85 | export function isNull(val) {
86 | return val === null || val === undefined;
87 | }
88 |
89 | function _translate(
90 | messages,
91 | locale,
92 | fallback,
93 | key,
94 | host,
95 | interpolateMode,
96 | args
97 | ) {
98 | let res = this._interpolate(
99 | locale,
100 | messages[locale],
101 | key,
102 | host,
103 | interpolateMode,
104 | args
105 | );
106 | if (!isNull(res)) return res;
107 |
108 | if (locale.indexOf("-") !== -1)
109 | return this._translate(
110 | messages,
111 | locale.split("-", 1)[0],
112 | fallback,
113 | key,
114 | host,
115 | interpolateMode,
116 | args
117 | );
118 |
119 | res = this._interpolate(
120 | fallback,
121 | messages[fallback],
122 | key,
123 | host,
124 | interpolateMode,
125 | args
126 | );
127 | if (!isNull(res)) {
128 | if (process.env.NODE_ENV !== "production" && !this._silentTranslationWarn) {
129 | console.warn(
130 | `[vue-i18n] Fall back to translate the keypath '${key}' with '${fallback}' locale.`
131 | );
132 | }
133 | return res;
134 | } else {
135 | return null;
136 | }
137 | }
138 |
139 | Object.getPrototypeOf(i18n)._translate = _translate;
140 |
--------------------------------------------------------------------------------
/src/views/404.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | 404
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
105 |
106 |
133 |
--------------------------------------------------------------------------------
/src/utils/auth.js:
--------------------------------------------------------------------------------
1 | import { client } from "../graphql";
2 | import gql from "graphql-tag";
3 | import store from "../store";
4 |
5 | export function isLoggedIn() {
6 | return client
7 | .query({
8 | query: gql`
9 | {
10 | me {
11 | id
12 | }
13 | }
14 | `,
15 | fetchPolicy: "network-only"
16 | })
17 | .then(({ data: { me } }) => me !== null);
18 | }
19 |
20 | export const PROVIDERS = {
21 | google: "https://accounts.google.com",
22 | facebook: "https://www.facebook.com",
23 | twitter: "https://api.twitter.com",
24 | discord: "https://discordapp.com",
25 | kitsu: "https://kitsu.io"
26 | };
27 |
28 | export function exchangeSSOToken(token) {
29 | return fetch(`${process.env.AUTH_URL}/ssoExchange`, {
30 | method: "POST",
31 | headers: {
32 | "Content-Type": "application/json"
33 | },
34 | body: JSON.stringify({
35 | token
36 | }),
37 | credentials: "include"
38 | })
39 | .then(res => res.json())
40 | .then(({ csrf, ...fields }) => {
41 | localStorage.setItem("csrf", csrf);
42 |
43 | if (window.FederatedCredential) {
44 | const cred = new FederatedCredential({
45 | id: fields.email,
46 | name: fields.login,
47 | iconURL: fields.avatar.startsWith("https://")
48 | ? fields.avatar
49 | : undefined,
50 | provider: PROVIDERS[fields.provider]
51 | });
52 |
53 | navigator.credentials.store(cred);
54 | }
55 |
56 | store.dispatch("setIsAuth", true);
57 | return fields;
58 | });
59 | }
60 |
61 | export function login(username, password) {
62 | return fetch(`${process.env.AUTH_URL}/login`, {
63 | method: "POST",
64 | headers: {
65 | "Content-Type": "application/json"
66 | },
67 | body: JSON.stringify({
68 | username,
69 | password
70 | }),
71 | credentials: "include"
72 | })
73 | .then(res => {
74 | if (res.status === 200) return res.json();
75 | else return res.json().then(alert => Promise.reject({ alert }));
76 | })
77 | .then(({ csrf, ...fields }) => {
78 | localStorage.setItem("csrf", csrf);
79 |
80 | if (window.PasswordCredential) {
81 | const cred = new PasswordCredential({
82 | id: fields.email,
83 | password,
84 | name: fields.login,
85 | iconURL: fields.avatar.startsWith("https://")
86 | ? fields.avatar
87 | : undefined
88 | });
89 |
90 | navigator.credentials.store(cred);
91 | }
92 |
93 | store.dispatch("setIsAuth", true);
94 | return fields;
95 | });
96 | }
97 |
98 | export function ssoLogin(provider, callback = window.location.href) {
99 | window.location.assign(
100 | `${process.env.AUTH_URL}/login/${provider}?callback=${encodeURIComponent(
101 | callback
102 | )}`
103 | );
104 | }
105 |
106 | export function logout() {
107 | localStorage.removeItem("csrf");
108 | if (navigator.credentials && navigator.credentials.preventSilentAccess) {
109 | // Turn on the mediation mode so auto sign-in won't happen
110 | // until next time user intended to do so.
111 | navigator.credentials.preventSilentAccess();
112 | }
113 | return Promise.resolve();
114 | }
115 |
116 | export function signup(login, email, password, newsletter) {
117 | return fetch(`${process.env.AUTH_URL}/signup`, {
118 | method: "POST",
119 | headers: {
120 | "Content-Type": "application/json"
121 | },
122 | body: JSON.stringify({
123 | login,
124 | email,
125 | password,
126 | newsletter
127 | }),
128 | credentials: "include"
129 | })
130 | .then(res => res.json())
131 | .then(({ csrf }) => localStorage.setItem("csrf", csrf));
132 | }
133 |
--------------------------------------------------------------------------------
/src/components/media/MediaList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Season {{ si + 1}}{{ season.name && `: ${season.name}` }}
7 |
8 |
9 |
14 |
15 | Episode {{ ei + 1}}{{ ep.name && `: ${ep.name}` }}
16 |
17 |
18 |
19 |
20 |
21 |
22 | Musics
23 |
24 |
25 |
30 |
31 | {{ capitalize(media.type.toLowerCase()) }}: {{ media.name }}
32 |
33 |
34 |
35 |
36 |
37 |
38 | Trailers
39 |
40 |
41 |
46 |
47 | {{ capitalize(media.type.toLowerCase()) }}: {{ media.name }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
93 |
94 |
115 |
--------------------------------------------------------------------------------
/src/components/cover/Cover.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
{{ value.name }}
10 |
{{ value.subname }}
11 |
21 |
22 |
23 |
24 |
43 |
44 |
160 |
--------------------------------------------------------------------------------
/tests/e2e/Navbar.test.js:
--------------------------------------------------------------------------------
1 | import { ClientFunction } from "testcafe";
2 | import VueSelector from "testcafe-vue-selectors";
3 | import collectCoverage from "./utils/istanbul";
4 |
5 | fixture("Navbar")
6 | .page("http://localhost:8042/")
7 | .afterEach(collectCoverage);
8 |
9 | const navbar = VueSelector("navbar");
10 | const mobileNavbar = VueSelector("mobile-navbar");
11 | const app = VueSelector("v-app");
12 | const getWindowLocation = ClientFunction(() => window.location);
13 |
14 | test("Navbar > Large screen have classic navbar", async t => {
15 | await t.resizeWindow(1280, 720);
16 | await t.expect(navbar.exists).eql(true);
17 | });
18 |
19 | test("Navbar > Dark Theme input toogle Theme", async t => {
20 | await t.resizeWindow(1280, 720);
21 | const themeSwitch = navbar.find(".switch label").withText("Dark");
22 | await t
23 | .expect(app.hasClass("theme--light"))
24 | .eql(true)
25 | .expect(app.hasClass("theme--dark"))
26 | .eql(false);
27 | await t.click(themeSwitch);
28 | await t
29 | .expect(app.hasClass("theme--light"))
30 | .eql(false)
31 | .expect(app.hasClass("theme--dark"))
32 | .eql(true);
33 | await t.click(themeSwitch);
34 | await t
35 | .expect(app.hasClass("theme--light"))
36 | .eql(true)
37 | .expect(app.hasClass("theme--dark"))
38 | .eql(false);
39 | });
40 |
41 | test("Navbar > Language Switch change lang", async t => {
42 | await t.resizeWindow(1280, 720);
43 | const languageSelect = VueSelector("language-select");
44 | await t.expect(languageSelect.find("span").textContent).eql("en");
45 | await t
46 | .expect(navbar.find(".list__tile__content").withText("Index").exists)
47 | .eql(true);
48 | await t.click(languageSelect);
49 | await t.click(app.find(".list__tile__title").withText("fr"));
50 | await t.expect(languageSelect.find("span").textContent).eql("fr");
51 | await t
52 | .expect(navbar.find(".list__tile__content").withText("Accueil").exists)
53 | .eql(true);
54 | });
55 |
56 | test("Navbar > Button toogle navbar", async t => {
57 | await t.resizeWindow(1280, 720);
58 | const navigationDrawer = VueSelector("v-navigation-drawer");
59 | await t.expect(navigationDrawer.getVue(({ props }) => props.value)).eql(true);
60 | await t.click(navbar.find(".nav-content .btn"));
61 | await t
62 | .expect(navigationDrawer.getVue(({ props }) => props.value))
63 | .eql(false);
64 | await t.click(app.find(".el-float.btn"));
65 | await t.expect(navigationDrawer.getVue(({ props }) => props.value)).eql(true);
66 | });
67 |
68 | test("Navbar > Navigation links", async t => {
69 | await t.resizeWindow(1280, 720);
70 | await t.click(navbar.find(".list__tile__content").withText("Search"));
71 | await t
72 | .expect((await getWindowLocation()).href)
73 | .eql("http://localhost:8042/search");
74 | });
75 |
76 | test("MobileNavbar > Little screen have mobile navbar", async t => {
77 | await t.resizeWindow(480, 800);
78 | await t.expect(mobileNavbar.exists).eql(true);
79 | });
80 |
81 | test("MobileNavbar > Search", async t => {
82 | await t.resizeWindow(480, 800);
83 | await t
84 | .click(mobileNavbar.find(".toolbar__content > .btn"))
85 | .typeText(mobileNavbar.find(".toolbar__content input"), "Unicorn !")
86 | .expect(VueSelector("search-results").exists)
87 | .eql(true);
88 | });
89 |
90 | test("MobileNavbar > Navigation links", async t => {
91 | await t.resizeWindow(480, 800);
92 | await t.click(mobileNavbar.find(".btn span").withText("Search"));
93 | await t
94 | .expect((await getWindowLocation()).href)
95 | .eql("http://localhost:8042/search");
96 | });
97 |
98 | test("MobileNavbar > SignIn", async t => {
99 | await t.resizeWindow(480, 800);
100 | await t.click(mobileNavbar.find(".toolbar .icon").withText("account_circle"));
101 | await t
102 | .expect((await getWindowLocation()).href)
103 | .eql("http://localhost:8042/auth/login");
104 | });
105 |
--------------------------------------------------------------------------------
/src/components/Floating.vue:
--------------------------------------------------------------------------------
1 |
2 | onMouseDown({ target, clientX, clientY })"
7 | >
8 |
9 | close
10 |
11 |
12 | keyboard_tab
13 |
14 |
15 |
16 |
17 |
18 |
107 |
108 |
109 |
140 |
--------------------------------------------------------------------------------
/src/views/user/library/Follows.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cet utilisateur ne follow aucun anime
5 |
6 |
7 |
8 |
9 |
10 | {{ anime.names[0] }}
11 | remove_red_eye
12 | clear
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
126 |
127 |
165 |
--------------------------------------------------------------------------------
/src/views/user/settings/Connections.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
25 |
26 |
31 |
32 |
33 |
34 |
{{ account.format() }}
35 | {{ account.name }}
36 |
37 |
38 |
39 |
40 | close
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
148 |
149 |
158 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@popcorn.moe/web",
3 | "version": "1.0.0",
4 | "description": "",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/Popcorn-moe/Web.git"
8 | },
9 | "author": "Popcorn-moe",
10 | "license": "GPL-3.0",
11 | "bugs": {
12 | "url": "https://github.com/Popcorn-moe/Web/issues"
13 | },
14 | "homepage": "https://github.com/Popcorn-moe/Web#readme",
15 | "scripts": {
16 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
17 | "start": "npm run dev",
18 | "lint": "eslint --ext .js,.vue src",
19 | "build": "node build/build.js",
20 | "format": "prettier --write --use-tabs \"src/**/*.js\" \"src/**/*.vue\" \"src/**/*.json\" \"tests/**/*.js\" \"tests/**/*.json\"",
21 | "precommit": "lint-staged",
22 | "test": "npm run test:e2e",
23 | "test:e2e": "concurrently --kill-others \"npm run test:e2e:mock\" \"npm run test:e2e:run\"",
24 | "test:e2e:run": "cross-env NODE_ENV=test testcafe \"chromium\" \"tests/e2e/*.test.js\" --app \"npm start >/dev/null 2>&1\" --app-init-delay 25000 -S -s screenshots",
25 | "test:e2e:mock": "node tests/e2e/mock"
26 | },
27 | "lint-staged": {
28 | "*.{js,vue}": [
29 | "prettier --use-tabs --write",
30 | "git add"
31 | ]
32 | },
33 | "dependencies": {
34 | "@popcorn.moe/apollo": "^1.0.0",
35 | "dateformat": "^3.0.3",
36 | "ebml": "^2.2.4",
37 | "fullscreen-api-polyfill": "^1.1.2",
38 | "graphql-tag": "^2.9.2",
39 | "ismobilejs": "^0.4.1",
40 | "marked": "^0.3.19",
41 | "megajs": "^0.13.3",
42 | "vue": "^2.5.17",
43 | "vue-apollo": "^3.0.0-beta.19",
44 | "vue-head": "^2.0.12",
45 | "vue-i18n": "^7.8.1",
46 | "vue-router": "^3.0.1",
47 | "vue-simplemde": "^0.4.8",
48 | "vuetify": "^1.1.14",
49 | "vuex": "^3.0.1"
50 | },
51 | "devDependencies": {
52 | "autoprefixer": "^8.6.5",
53 | "babel-eslint": "^8.2.6",
54 | "babel-helper-vue-jsx-merge-props": "^2.0.3",
55 | "babel-loader": "^7.1.5",
56 | "babel-plugin-istanbul": "^4.1.6",
57 | "babel-plugin-syntax-jsx": "^6.18.0",
58 | "babel-plugin-transform-imports": "^1.5.0",
59 | "babel-plugin-transform-runtime": "^6.23.0",
60 | "babel-plugin-transform-vue-jsx": "^3.7.0",
61 | "babel-preset-env": "^1.7.0",
62 | "babel-preset-stage-2": "^6.24.1",
63 | "chalk": "^2.4.1",
64 | "concurrently": "^3.6.1",
65 | "copy-webpack-plugin": "^4.5.2",
66 | "cors": "^2.8.4",
67 | "cross-env": "^5.2.0",
68 | "css-loader": "^0.28.11",
69 | "eslint": "^4.19.1",
70 | "eslint-friendly-formatter": "^4.0.1",
71 | "eslint-loader": "^2.1.0",
72 | "eslint-plugin-html": "^4.0.5",
73 | "exit-hook": "^2.0.0",
74 | "express": "^4.16.3",
75 | "express-graphql": "^0.6.12",
76 | "faker": "^4.1.0",
77 | "file-loader": "^1.1.11",
78 | "friendly-errors-webpack-plugin": "^1.7.0",
79 | "git-revision-webpack-plugin": "^3.0.3",
80 | "graphql": "^0.13.2",
81 | "graphql-tools": "^3.1.1",
82 | "html-loader": "^0.5.5",
83 | "html-webpack-plugin": "^3.2.0",
84 | "husky": "^0.14.3",
85 | "istanbul": "^0.4.5",
86 | "lint-staged": "^7.2.2",
87 | "mini-css-extract-plugin": "^0.4.2",
88 | "node-notifier": "^5.2.1",
89 | "optimize-css-assets-webpack-plugin": "^4.0.3",
90 | "ora": "^2.1.0",
91 | "portfinder": "^1.0.17",
92 | "postcss-import": "^11.1.0",
93 | "postcss-loader": "^2.1.6",
94 | "postcss-url": "^7.3.2",
95 | "prettier": "1.12.1",
96 | "rimraf": "^2.6.2",
97 | "script-ext-html-webpack-plugin": "^2.0.1",
98 | "semver": "^5.5.1",
99 | "shelljs": "^0.8.2",
100 | "style-loader": "^0.21.0",
101 | "stylus": "^0.54.5",
102 | "stylus-loader": "^3.0.2",
103 | "testcafe": "^0.20.5",
104 | "testcafe-vue-selectors": "^3.0.0",
105 | "uglifyjs-webpack-plugin": "^1.3.0",
106 | "url-loader": "^1.1.1",
107 | "vue-loader": "^15.4.1",
108 | "vue-style-loader": "^4.1.2",
109 | "vue-svg-loader": "^0.5.0",
110 | "vue-template-compiler": "^2.5.17",
111 | "webpack": "^4.17.1",
112 | "webpack-bundle-analyzer": "^2.13.1",
113 | "webpack-cli": "^3.1.2",
114 | "webpack-dev-server": "^3.1.6",
115 | "webpack-merge": "^4.1.4",
116 | "workbox-webpack-plugin": "^3.4.1"
117 | },
118 | "engines": {
119 | "node": ">= 4.0.0",
120 | "npm": ">= 3.0.0"
121 | },
122 | "browserslist": [
123 | "> 1%",
124 | "last 2 versions",
125 | "not ie <= 8"
126 | ]
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/room/Chatbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Chat
8 |
9 |
10 | more_vert
11 |
12 |
13 |
14 |
15 |
16 |
17 | info
18 |
19 |
20 | Bla
21 |
22 |
23 |
24 | more_vert
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | info
33 |
34 |
35 | Bla
36 |
37 |
38 |
39 | more_vert
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | info
48 |
49 |
50 | Bla
51 |
52 |
53 |
54 | more_vert
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | info
63 |
64 |
65 | Bla
66 |
67 |
68 |
69 | more_vert
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | send
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
130 |
131 |
141 |
--------------------------------------------------------------------------------
/src/views/Author.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ v.names[0] }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ author.name }}
16 |
17 |
18 |
19 |
20 |
Animes
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
114 |
115 |
186 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import Router from "vue-router";
3 | import store from "../store";
4 | import { IS_LOADING } from "../store/app";
5 | Vue.use(Router);
6 |
7 | export const routes = [
8 | {
9 | icon: "home",
10 | path: "/",
11 | name: "Index",
12 | t: "route.index",
13 | component: () => import("../views/Index")
14 | },
15 | {
16 | icon: "search",
17 | path: "/search",
18 | name: "Search",
19 | t: "route.search",
20 | component: () => import("../views/Search")
21 | },
22 | {
23 | icon: "equalizer",
24 | path: "/news",
25 | name: "News",
26 | t: "route.news",
27 | component: () => import("../views/news/NewsList.vue")
28 | },
29 | {
30 | hide: true,
31 | path: "/news/:id",
32 | name: "NewsPage",
33 | component: () => import("../views/news/News.vue"),
34 | props: true
35 | },
36 | {
37 | hide: true,
38 | path: "/anime/:id",
39 | name: "Anime",
40 | component: () => import("../views/Anime"),
41 | props: true
42 | },
43 | {
44 | hide: true,
45 | path: "/anime/:id/:mediaId",
46 | name: "Media",
47 | component: () => import("../views/Media"),
48 | meta: { hasPlayer: true, removeMobileNavbar: true },
49 | props: true
50 | },
51 | {
52 | hide: true,
53 | path: "/anime/:id/:season/:episode",
54 | name: "Episode",
55 | component: () => import("../views/Media"),
56 | meta: { hasPlayer: true, removeMobileNavbar: true },
57 | props: true
58 | },
59 | {
60 | hide: true,
61 | path: "/author/:id",
62 | name: "Author",
63 | component: () => import("../views/Author"),
64 | meta: { hasPlayer: true },
65 | props: true
66 | },
67 | {
68 | hide: true,
69 | name: "Login",
70 | path: "/auth/login",
71 | component: () => import("../views/auth/Login")
72 | },
73 | {
74 | hide: true,
75 | name: "SignUp",
76 | path: "/auth/signup",
77 | component: () => import("../views/auth/SignUp")
78 | },
79 | {
80 | hide: true,
81 | name: "User",
82 | path: "/user/:userLogin/:page(profile|follows|followers)",
83 | component: () => import("../views/user/User"),
84 | props: true
85 | },
86 | {
87 | hide: true,
88 | name: "UserLibrary",
89 | path: "/user/:userLogin/:page(library)",
90 | component: () => import("../views/user/User"),
91 | props: true,
92 | children: [
93 | {
94 | path: ":status(watching|want-to-watch|completed|on-hold|dropped)",
95 | name: "LibraryAnimeStatus",
96 | props: true,
97 | component: () => import("../views/user/library/AnimeStatus")
98 | },
99 | {
100 | path: "search/:search",
101 | name: "LibrarySearch",
102 | props: true,
103 | component: () => import("../views/user/library/Search")
104 | }
105 | ]
106 | },
107 | {
108 | hide: true,
109 | name: "UserSettings",
110 | path: "/user/:userLogin/:page(settings)",
111 | component: () => import("../views/user/User"),
112 | props: true,
113 | children: [
114 | {
115 | path: "account",
116 | name: "UserSettingsAccount",
117 | t: "route.user_settings.account",
118 | icon: "face",
119 | component: () => import("../views/user/settings/Account")
120 | },
121 | {
122 | path: "connections",
123 | name: "UserSettingsConnections",
124 | t: "route.user_settings.connections",
125 | icon: "supervisor_account",
126 | component: () => import("../views/user/settings/Connections"),
127 | divider: true
128 | }
129 | ]
130 | },
131 | {
132 | hide: true,
133 | name: "Translations",
134 | path: "/translations",
135 | component: () => import("../views/Translations")
136 | },
137 | {
138 | hide: true,
139 | name: "404",
140 | path: "/404",
141 | component: () => import("../views/404")
142 | },
143 | {
144 | hide: true,
145 | path: "*",
146 | redirect: "/404"
147 | }
148 | ];
149 |
150 | const router = new Router({
151 | routes,
152 | mode: "history",
153 | scrollBehavior(to, from, savedPosition) {
154 | if (savedPosition) {
155 | return savedPosition;
156 | } else if (to.hash) {
157 | return { selector: to.hash };
158 | } else if (to.name.startsWith("User")) {
159 | return null;
160 | } else if (from.name != to.name) {
161 | return new Promise(r => setTimeout(() => r({ x: 0, y: 0 }), 300)); // 300 ms = slide transition time
162 | }
163 | }
164 | });
165 |
166 | let timeout;
167 |
168 | router.afterEach((to, from) => {
169 | if (!from.path.startsWith("/auth/")) {
170 | router.last = from.path;
171 | }
172 | if (timeout) clearTimeout(timeout);
173 | store.commit(IS_LOADING, false);
174 | });
175 |
176 | router.beforeEach((to, from, next) => {
177 | timeout = setTimeout(() => {
178 | store.commit(IS_LOADING, true);
179 | timeout = null;
180 | }, 500); //Debouce
181 | next();
182 | });
183 |
184 | export default router;
185 |
--------------------------------------------------------------------------------
/src/views/auth/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
42 | {{ alert.text }}
43 |
44 |
48 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
131 |
132 |
168 |
--------------------------------------------------------------------------------
/src/views/auth/SignUp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
27 |
28 |
29 |
30 |
31 |
32 |
(hidePassword = !hidePassword)"
36 | :type="hidePassword ? 'password' : 'text'">
37 |
(hidePasswordConfirm = !hidePasswordConfirm)"
41 | :type="hidePasswordConfirm ? 'password' : 'text'">
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
126 |
127 |
163 |
--------------------------------------------------------------------------------
/src/views/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ v.title }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
197 |
198 |
231 |
--------------------------------------------------------------------------------
/src/components/cover/CoverList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 | keyboard_arrow_left
19 |
20 |
21 | keyboard_arrow_right
22 |
23 |
24 |
25 |
26 |
27 |
136 |
137 |
138 |
198 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/Notifications.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | menu
7 |
8 |
9 |
10 |
11 | clear
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
25 | messages
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
{{ notif.message }}
40 |
43 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
175 |
176 |
199 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 | $emit('input', value)"
6 | class="elevation-2"
7 | disable-route-watcher
8 | app
9 | style="max-height: 100%"
10 | >
11 |
12 |
13 | $emit('input', value)" v-if="notifications">
14 |
15 |
16 |
17 |
18 | menu
19 |
20 |
21 |
22 | Logo
23 |
24 |
25 | 0" v-if="isAuth" :disabled="notifs_count == 0">
26 |
27 |
28 | notifications
29 |
30 | notifications
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
172 |
173 |
211 |
--------------------------------------------------------------------------------
/src/components/layout/navbar/MobileNavbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | arrow_back
8 |
9 |
15 |
16 |
17 |
Logo
18 |
19 |
20 | search
21 |
22 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | close
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | account_circle
47 |
48 |
49 |
50 | more_vert
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
198 |
199 |
215 |
--------------------------------------------------------------------------------
/src/views/user/Library.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 | arrow_back
15 |
16 |
17 |
18 |
19 |
20 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Animes Status
45 |
47 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | {{ animesCount(key) }}
61 |
62 |
63 |
64 |
65 |
66 | Others
67 |
70 |
71 | Follows
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
228 |
229 |
233 |
--------------------------------------------------------------------------------
/src/stylus/theme.styl:
--------------------------------------------------------------------------------
1 | /** Stylus Styles */
2 | @require '~vuetify/src/stylus/settings/_colors'
3 |
4 | // Popcorn
5 | // ============================================================
6 | $main-color = #f6416c
7 | $secondary-color = #fddd84
8 |
9 | // Theme
10 | // ============================================================
11 | $body-font-family := 'Roboto', sans-serif
12 | $font-size-root := 16px
13 | $line-height-root := 1.5
14 | $navigation-drawer-width = 280px;
15 |
16 | $theme := {
17 | primary: $main-color
18 | accent: $main-color
19 | secondary: $secondary-color
20 | info: $blue.base
21 | warning: $amber.base
22 | error: $red.accent-2
23 | success: $green.base
24 | }
25 |
26 | // Material Design - https://material.io/guidelines/style/color.html#color-usability
27 | // ============================================================
28 | $material-light := {
29 | status-bar: {
30 | regular: #E0E0E0,
31 | lights-out: rgba(#fff, .7)
32 | },
33 | app-bar: #F0F0F0,
34 | background: #FAFAFA,
35 | cards: #F0F0F0,
36 | chips: {
37 | background: $grey.lighten-2,
38 | color: rgba(#000, .87)
39 | },
40 | dividers: rgba(#000, .12),
41 | text: {
42 | theme: #fff,
43 | primary: rgba(#000, .87),
44 | secondary: rgba(#000, .54),
45 | disabled: rgba(#000, .38),
46 | link: $blue.darken-2,
47 | link-hover: $grey.darken-3
48 | },
49 | icons: {
50 | active: rgba(#000, .54),
51 | inactive: rgba(#000, .38)
52 | },
53 | inputs: {
54 | box: rgba(#000, .04),
55 | solo-inverted: rgba(#000, .16),
56 | solo-inverted-focused: #424242,
57 | solo-inverted-focused-text: #fff
58 | },
59 | buttons: {
60 | disabled: rgba(#000, .26),
61 | focused: rgba(#000, .12),
62 | focused-alt: rgba(#fff, .6),
63 | pressed: rgba(#999, .4)
64 | },
65 | expansion-panels: {
66 | focus: #EEEEEE
67 | },
68 | list-tile: {
69 | hover: rgba(#000, .04)
70 | },
71 | selection-controls: {
72 | active: $main-color
73 | thumb: {
74 | inactive: $grey.lighten-5,
75 | disabled: $grey.lighten-1
76 | },
77 | track: {
78 | inactive: rgba(#000, .38),
79 | disabled: rgba(#000, .12)
80 | }
81 | },
82 | slider: {
83 | active: rgba(#000, .38),
84 | inactive: rgba(#000, .26),
85 | disabled: rgba(#000, .26),
86 | discrete: #000
87 | },
88 | tabs: {
89 | active: rgba(#000, .87),
90 | inactive: rgba(#000, .7)
91 | },
92 | text-fields: {
93 | box: rgba(#000, .06),
94 | box-hover: rgba(#000, .12)
95 | },
96 | input-bottom-line: rgba(#000, .42),
97 | stepper: {
98 | active: rgba(#fff, 1),
99 | completed: rgba(#000, 0.87),
100 | hover: rgba(#000, 0.54)
101 | },
102 | table: {
103 | active: $grey.lighten-4
104 | hover: $grey.lighten-3
105 | },
106 | picker: {
107 | body: $theme.primary,
108 | clock: $grey.lighten-2,
109 | indeterminateTime: $grey.lighten-1,
110 | title: $grey.lighten-2
111 | },
112 | bg-color: #fff
113 | fg-color: #000
114 | text-color: #000
115 | primary-text-percent: .87
116 | secondary-text-percent: .54
117 | disabledORhints-text-percent: .38
118 | divider-percent: .12
119 | active-icon-percent: .54
120 | inactive-icon-percent: .38
121 | }
122 |
123 | $material-dark := {
124 | status-bar: {
125 | regular: #000000,
126 | lights-out: rgba(#000, .2)
127 | },
128 | app-bar: #1C1C1C,
129 | background: #111111,
130 | cards: #1C1C1C,
131 | chips: {
132 | background: #FFF,
133 | color: $material-light.text.primary
134 | },
135 | dividers: rgba(#fff, .12),
136 | text: {
137 | theme: #fff,
138 | primary: #fff,
139 | secondary: rgba(#fff, .70),
140 | disabled: rgba(#fff, .50),
141 | link: $blue.accent-1,
142 | link-hover: $grey.lighten-3
143 | },
144 | icons: {
145 | active: #fff,
146 | inactive: rgba(#fff, .5)
147 | },
148 | inputs: {
149 | box: #fff,
150 | solo-inverted: rgba(#fff, .16),
151 | solo-inverted-focused: #fff,
152 | solo-inverted-focused-text: $material-light.text.primary
153 | },
154 | list-tile: {
155 | hover: rgba(#fff, .08)
156 | },
157 | buttons: {
158 | disabled: rgba(#fff, .3),
159 | focused: rgba(#fff, .12),
160 | focused-alt: rgba(#fff, .1),
161 | pressed: rgba(#ccc, .25)
162 | },
163 | expansion-panels: {
164 | focus: rgba(#000, .7)
165 | },
166 | selection-controls: {
167 | active: $main-color,
168 | thumb: {
169 | inactive: $grey.lighten-1,
170 | disabled: $grey.darken-3
171 | },
172 | track: {
173 | inactive: rgba(#fff, .3),
174 | disabled: rgba(#fff, .1)
175 | }
176 | },
177 | slider: {
178 | active: rgba(#fff, .3),
179 | inactive: rgba(#fff, .2),
180 | disabled: rgba(#fff, .2),
181 | discrete: #fff
182 | },
183 | tabs: {
184 | active: #fff
185 | inactive: rgba(#fff, .7)
186 | },
187 | text-fields: {
188 | box: rgba(#000, .1),
189 | box-hover: rgba(#000, .2),
190 | box-focus: rgba(#000, .3)
191 | },
192 | input-bottom-line: rgba(#fff, .7),
193 |
194 | stepper: {
195 | active: rgba(#fff, 1),
196 | completed: rgba(#fff, 0.87),
197 | hover: rgba(#fff, 0.75)
198 | },
199 | table: {
200 | active: $grey.darken-1
201 | hover: $grey.darken-2
202 | },
203 | picker: {
204 | body: ,
205 | clock: $grey.darken-2,
206 | indeterminateTime: $grey.darken-1,
207 | title: $grey.darken-2
208 | },
209 | bg-color: #303030
210 | fg-color: #fff
211 | text-color: #fff
212 | primary-text-percent: 1
213 | secondary-text-percent: .70
214 | disabledORhints-text-percent: .50
215 | divider-percent: .12
216 | active-icon-percent: 1
217 | inactive-icon-percent: .50
218 | }
219 |
220 | // Default Material Theme
221 | // ============================================================
222 | $material-theme := $material-light
223 | $material-twelve-percent-light := rgba($material-dark.fg-color, $material-dark.divider-percent)
224 | $material-twelve-percent-dark := rgba($material-light.fg-color, $material-light.divider-percent)
225 |
226 |
227 | // Theme
228 | // ============================================================
229 | $body-bg-light := $material-light.background
230 | $body-bg-dark := $material-dark.background
231 | $body-color-light := rgba($material-dark.text-color, $material-dark.primary-text-percent)
232 | $body-color-dark := rgba($material-light.text-color, $material-light.primary-text-percent)
233 | $color-theme := rgba($material-theme.text-color, $material-theme.primary-text-percent)
--------------------------------------------------------------------------------
/static/logo-animated.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/assets/loader.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/src/components/media/Comment.vue:
--------------------------------------------------------------------------------
1 |
2 |
68 |
69 |
70 |
189 |
190 |
249 |
--------------------------------------------------------------------------------
/src/mse/MegaMediaSource.js:
--------------------------------------------------------------------------------
1 | import { File } from "megajs/dist/main.browser-es.js";
2 | import ebml from "ebml";
3 |
4 | // From https://www.webmproject.org/docs/container/
5 | const INIT_EBML_ARRAYS = ["SeekHead", "Tracks", "Cues"];
6 | const ROOT = Symbol("root");
7 |
8 | function appendBuffer(buffer1, buffer2) {
9 | const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
10 | tmp.set(new Uint8Array(buffer1), 0);
11 | tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
12 | return tmp;
13 | }
14 |
15 | function byteArrayToLong(byteArray) {
16 | let value = 0;
17 | for (let i = 0; i < byteArray.length; i++) {
18 | value = value * 256 + byteArray[i];
19 | }
20 | return value;
21 | }
22 |
23 | export default class MegaMediaSource {
24 | constructor(url, video) {
25 | this.mediaSource = new MediaSource();
26 | this.video = video;
27 | this.video.addEventListener("error", () => console.error(this.video.error));
28 | this.video.src = URL.createObjectURL(this.mediaSource);
29 | this.mediaSource.addEventListener("sourceopen", () => this.sourceOpen());
30 | this.video.addEventListener("loadedmetadata", () => this.loadedMetadata());
31 | this.video.addEventListener("seeking", e => this.seeking(e));
32 | this.buffers = [];
33 | this.seekQueue = Promise.resolve();
34 | this.lastSeek = 0;
35 | this.file = File.fromURL(url);
36 | }
37 |
38 | destroy() {
39 | this.stopDownload();
40 | }
41 |
42 | sourceOpen() {
43 | this.readInitSegment().then(([data, segment]) => {
44 | /*for (const seek of data.Segment.SeekHead) {
45 | if (seek.SeekID && byteArrayToLong(seek.SeekID.data) == 0x1c53bb6b)
46 | console.log("Cue Offset", seek);
47 | }*/
48 | this.cues = MegaMediaSource.getCues(data);
49 | this.sourceBuffer = this.mediaSource.addSourceBuffer(
50 | MegaMediaSource.getCodec(data)
51 | );
52 | this.sourceBuffer.addEventListener("updateend", _ => this.updateEnd());
53 | this.sourceBuffer.addEventListener("error", e => console.error(e));
54 | this.appendBuffer(segment);
55 | this.lastByte = segment.byteLength;
56 | });
57 | }
58 |
59 | loadedMetadata() {
60 | this.startDownload(this.lastByte);
61 | }
62 |
63 | seeking(e) {
64 | this.seekQueue = this.seekQueue.then(
65 | _ =>
66 | new Promise(resolve => {
67 | let time = this.video.currentTime;
68 | if (this.lastSeek == time) return resolve();
69 | this.lastSeek = time;
70 |
71 | const buffered = this.sourceBuffer.buffered;
72 | for (let i = 0; i < buffered.length; i++)
73 | if (time >= buffered.start(i) && time <= buffered.end(i)) {
74 | time = buffered.end(i);
75 | break;
76 | }
77 |
78 | this.stopDownload();
79 | const next = this.cues.find(e => e.offset >= this.lastByte);
80 |
81 | const seek = () => {
82 | const next = this.cues
83 | .slice(0)
84 | .reverse()
85 | .find(e => e.time / 1000 <= time);
86 | this.startDownload(next.offset + 1);
87 | resolve();
88 | };
89 | if (next && next.offset == this.lastByte) seek();
90 | else {
91 | const stream = this.file.download({
92 | start: this.lastByte,
93 | end: next ? next.offset : undefined
94 | });
95 | stream.on("data", data => this.appendBuffer(data));
96 | stream.once("end", seek);
97 | }
98 | })
99 | );
100 | }
101 |
102 | startDownload(start) {
103 | if (this.currentStream) throw new Error("Download alrealy running");
104 | this.lastByte = start;
105 | this.currentStream = this.file.download({ start, maxConnections: 2 });
106 |
107 | this.currentStream.on("data", buffer => {
108 | if (this.currentStream) {
109 | this.appendBuffer(buffer);
110 | this.lastByte += buffer.byteLength;
111 | }
112 | });
113 | this.currentStream.on("end", _ => this.appendBuffer(null));
114 | }
115 |
116 | stopDownload() {
117 | this.currentStream.emit("close");
118 | this.currentStream = null;
119 | }
120 |
121 | appendBuffer(buffer) {
122 | if (this.buffers.length > 0 || this.sourceBuffer.updating)
123 | this.buffers.push(buffer);
124 | else {
125 | if (buffer === null) this.mediaSource.endOfStream();
126 | else this.sourceBuffer.appendBuffer(buffer);
127 | }
128 | }
129 |
130 | updateEnd() {
131 | //console.log("Update End");
132 | const buffered = this.sourceBuffer.buffered;
133 | //for (let i = 0; i < buffered.length; i++)
134 | // console.log("\t => Buffered", i, buffered.start(i), buffered.end(i));
135 | if (this.buffers.length && !this.sourceBuffer.updating) {
136 | const buffer = this.buffers.shift();
137 | if (buffer === null) this.mediaSource.endOfStream();
138 | else this.sourceBuffer.appendBuffer(buffer);
139 | }
140 | }
141 |
142 | readInitSegment() {
143 | return new Promise((resolve, reject) => {
144 | const stream = this.file.download({
145 | maxChunkSize: 128 * 1024,
146 | maxConnections: 2
147 | });
148 | const decoder = new ebml.Decoder();
149 |
150 | let node = {};
151 | let buffer = new Uint8Array();
152 |
153 | stream.on("data", data => (buffer = appendBuffer(buffer, data)));
154 |
155 | decoder.on("data", ([type, data]) => {
156 | switch (type) {
157 | case "start":
158 | if (data.name == "Cluster") {
159 | resolve([node[ROOT], buffer.slice(0, data.start)]);
160 | stream.emit("close");
161 | break;
162 | }
163 |
164 | if (Array.isArray(node)) {
165 | const tmp = INIT_EBML_ARRAYS.includes(data.name) ? [] : {};
166 | tmp[ROOT] = node;
167 | node.push(tmp);
168 | node = tmp;
169 | } else {
170 | if (!node[data.name]) {
171 | node[data.name] = INIT_EBML_ARRAYS.includes(data.name)
172 | ? []
173 | : {};
174 | node[data.name][ROOT] = node;
175 | }
176 | node = node[data.name];
177 | }
178 | node.dataOffset = data.end - data.dataSize;
179 | break;
180 | case "tag":
181 | if (Array.isArray(node)) node.push(data);
182 | else node[data.name] = data;
183 | break;
184 | case "end":
185 | node = node[ROOT];
186 | break;
187 | }
188 | });
189 | decoder.on("error", reject);
190 |
191 | stream.pipe(decoder);
192 | });
193 | }
194 |
195 | static getCodec({ Segment: { Tracks } }) {
196 | let vCodec;
197 | let aCodec;
198 | for (const track of Tracks) {
199 | if (track.CodecID) {
200 | const codec = track.CodecID.data.toString().toLowerCase();
201 | if (codec.startsWith("v_")) vCodec = codec.slice(2);
202 | else if (codec.startsWith("a_")) aCodec = codec.slice(2);
203 | }
204 | }
205 | if (vCodec && aCodec) return `video/webm;codecs="${vCodec},${aCodec}"`;
206 | else if (vCodec) return `video/webm;codecs="${vCodec}"`;
207 | else if (aCodec) return `audio/webm;codecs="${aCodec}"`;
208 | }
209 |
210 | static getCues({
211 | Segment: {
212 | dataOffset,
213 | Cues,
214 | Info: { TimecodeScale }
215 | }
216 | }) {
217 | if (Cues === undefined) return;
218 | const timeBase = TimecodeScale
219 | ? byteArrayToLong(TimecodeScale.data) / 1000000
220 | : 1;
221 | const cues = [];
222 | for (const cue of Cues) {
223 | if (cue.CueTime)
224 | cues.push({
225 | time: byteArrayToLong(cue["CueTime"].data) * timeBase,
226 | offset:
227 | byteArrayToLong(
228 | cue["CueTrackPositions"]["CueClusterPosition"].data
229 | ) + dataOffset
230 | });
231 | }
232 | return cues;
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/views/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ tag.name }}
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {{ tag.name }}
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
274 |
275 |
318 |
--------------------------------------------------------------------------------
{{ value.posted }}
13 |