├── .editorconfig
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── .gitlab-ci.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── config
├── dev.proxy.js.example
├── helpers.js
├── resource-override.js
├── webpack.common.js
├── webpack.dev.js
└── webpack.prod.js
├── package.json
├── scripts
└── font-fix.js
├── src
├── app
│ ├── RefreshSameRouteStrategy.ts
│ ├── _center-box.less
│ ├── admin
│ │ ├── admin-navbar
│ │ │ ├── admin-navbar.component.ts
│ │ │ ├── admin-navbar.html
│ │ │ └── admin-navbar.less
│ │ ├── admin.component.ts
│ │ ├── admin.html
│ │ ├── admin.less
│ │ ├── admin.module.ts
│ │ ├── admin.routes.ts
│ │ ├── admin.service.ts
│ │ ├── announce
│ │ │ ├── announce.component.ts
│ │ │ ├── announce.html
│ │ │ ├── announce.less
│ │ │ ├── announce.service.ts
│ │ │ ├── edit-announce
│ │ │ │ ├── edit-announce.component.ts
│ │ │ │ ├── edit-announce.html
│ │ │ │ └── edit-announce.less
│ │ │ └── edit-bangumi-recommend
│ │ │ │ ├── edit-bangumi-recommend.component.ts
│ │ │ │ ├── edit-bangumi-recommend.html
│ │ │ │ └── edit-bangumi-recommend.less
│ │ ├── bangumi-card
│ │ │ ├── bangumi-card.component.ts
│ │ │ ├── bangumi-card.html
│ │ │ └── bangumi-card.less
│ │ ├── bangumi-detail
│ │ │ ├── bangumi-basic
│ │ │ │ ├── bangumi-basic.component.ts
│ │ │ │ ├── bangumi-basic.html
│ │ │ │ └── bangumi-basic.less
│ │ │ ├── bangumi-detail.component.ts
│ │ │ ├── bangumi-detail.html
│ │ │ ├── bangumi-detail.less
│ │ │ ├── bangumi-moe-builder
│ │ │ │ ├── bangum-moe-entity.ts
│ │ │ │ ├── bangumi-moe-builder.component.ts
│ │ │ │ ├── bangumi-moe-builder.html
│ │ │ │ ├── bangumi-moe-builder.less
│ │ │ │ └── bangumi-moe.service.ts
│ │ │ ├── episode-detail
│ │ │ │ ├── episode-detail.component.ts
│ │ │ │ ├── episode-detail.html
│ │ │ │ └── episode-detail.less
│ │ │ ├── feed.service.ts
│ │ │ ├── index.ts
│ │ │ ├── keyword-builder
│ │ │ │ ├── keyword-builder.component.ts
│ │ │ │ ├── keyword-builder.html
│ │ │ │ └── keyword-builder.less
│ │ │ ├── universal-builder
│ │ │ │ ├── universal-builder.component.ts
│ │ │ │ ├── universal-builder.html
│ │ │ │ └── universal-builder.less
│ │ │ └── video-file-modal
│ │ │ │ ├── video-file-list.less
│ │ │ │ ├── video-file-modal.component.ts
│ │ │ │ └── video-file-modal.html
│ │ ├── bangumi-pipes
│ │ │ ├── libyk-pipe.ts
│ │ │ ├── nyaa-pipe.ts
│ │ │ ├── parse-json.pipe.ts
│ │ │ ├── status-name-pipe.ts
│ │ │ └── type-name-pipe.ts
│ │ ├── index.ts
│ │ ├── list-bangumi
│ │ │ ├── list-bangumi.component.ts
│ │ │ ├── list-bangumi.html
│ │ │ ├── list-bangumi.less
│ │ │ └── list-bangumi.service.ts
│ │ ├── search-bangumi
│ │ │ ├── index.ts
│ │ │ ├── result-detail
│ │ │ │ ├── result-detail.component.ts
│ │ │ │ ├── result-detail.html
│ │ │ │ └── result-detail.less
│ │ │ ├── search-bangumi.component.ts
│ │ │ ├── search-bangumi.html
│ │ │ └── search-bangumi.less
│ │ ├── task-manager
│ │ │ ├── task-manager.component.ts
│ │ │ ├── task-manager.html
│ │ │ ├── task-manager.less
│ │ │ └── task.service.ts
│ │ ├── user-manager
│ │ │ ├── user-manager.component.ts
│ │ │ ├── user-manager.html
│ │ │ ├── user-manager.less
│ │ │ ├── user-manager.service.ts
│ │ │ └── user-promote-modal
│ │ │ │ ├── user-promote-modal.component.ts
│ │ │ │ ├── user-promote-modal.html
│ │ │ │ └── user-promote-modal.less
│ │ └── web-hook
│ │ │ ├── edit-web-hook
│ │ │ ├── edit-web-hook.component.ts
│ │ │ ├── edit-web-hook.html
│ │ │ └── edit-web-hook.less
│ │ │ ├── web-hook-card
│ │ │ ├── web-hook-card.component.ts
│ │ │ ├── web-hook-card.html
│ │ │ └── web-hook-card.less
│ │ │ ├── web-hook.component.ts
│ │ │ ├── web-hook.html
│ │ │ ├── web-hook.less
│ │ │ └── web-hook.service.ts
│ ├── alert-dialog
│ │ ├── alert-dialog.component.ts
│ │ ├── alert-dialog.html
│ │ └── alert-dialog.module.ts
│ ├── analytics.service.ts
│ ├── app.component.ts
│ ├── app.less
│ ├── app.module.ts
│ ├── app.routes.ts
│ ├── browser-extension
│ │ ├── browser-extension.module.ts
│ │ ├── chrome-extension.service.ts
│ │ └── extension-rpc.service.ts
│ ├── confirm-dialog
│ │ ├── confirm-dialog-modal.component.ts
│ │ ├── confirm-dialog-modal.html
│ │ ├── confirm-dialog.directive.ts
│ │ └── index.ts
│ ├── email-confirm
│ │ ├── email-confirm.component.ts
│ │ ├── email-confirm.html
│ │ ├── email-confirm.less
│ │ ├── email-confirm.module.ts
│ │ └── email-confirm.service.ts
│ ├── entity
│ │ ├── announce.ts
│ │ ├── bangumi-raw.ts
│ │ ├── bangumi.ts
│ │ ├── constants.ts
│ │ ├── episode.ts
│ │ ├── image.ts
│ │ ├── index.ts
│ │ ├── item-type.ts
│ │ ├── item.ts
│ │ ├── publisher.ts
│ │ ├── rating.ts
│ │ ├── team.ts
│ │ ├── user.ts
│ │ ├── video-file.ts
│ │ ├── watch-progress.ts
│ │ └── web-hook.ts
│ ├── environment.ts
│ ├── error
│ │ ├── error.component.ts
│ │ └── error.html
│ ├── forget-pass
│ │ ├── forget-pass.component.ts
│ │ ├── forget-pass.html
│ │ ├── forget-pass.less
│ │ └── forget-pass.module.ts
│ ├── form-utils
│ │ ├── index.ts
│ │ └── validators.ts
│ ├── home
│ │ ├── bangumi-account-binding
│ │ │ ├── bangumi-account-binding.component.ts
│ │ │ ├── bangumi-account-binding.html
│ │ │ └── bangumi-account-binding.less
│ │ ├── bangumi-card
│ │ │ ├── bangumi-card.component.ts
│ │ │ ├── bangumi-card.html
│ │ │ ├── bangumi-card.less
│ │ │ └── image-loading-strategy.service.ts
│ │ ├── bangumi-detail
│ │ │ ├── bangumi-detail.components.ts
│ │ │ ├── bangumi-detail.html
│ │ │ └── bangumi-detail.less
│ │ ├── bangumi-extra-info
│ │ │ ├── bangumi-character
│ │ │ │ ├── bangumi-character.component.ts
│ │ │ │ ├── bangumi-character.html
│ │ │ │ └── bangumi-character.less
│ │ │ ├── bangumi-staff-info
│ │ │ │ ├── bangumi-staff-info.component.ts
│ │ │ │ ├── bangumi-staff-info.html
│ │ │ │ └── bangumi-staff-info.less
│ │ │ └── interfaces.ts
│ │ ├── bangumi-list
│ │ │ ├── bangumi-list.component.ts
│ │ │ ├── bangumi-list.html
│ │ │ ├── bangumi-list.less
│ │ │ └── bangumi-list.service.ts
│ │ ├── bottom-float-banner
│ │ │ ├── bottom-float-banner.component.ts
│ │ │ ├── bottom-float-banner.html
│ │ │ └── bottom-float-banner.less
│ │ ├── default
│ │ │ ├── default.component.ts
│ │ │ ├── default.html
│ │ │ └── default.less
│ │ ├── favorite-chooser
│ │ │ ├── conflict-dialog
│ │ │ │ ├── conflict-dialog.component.ts
│ │ │ │ ├── conflict-dialog.html
│ │ │ │ └── conflict-dialog.less
│ │ │ ├── favorite-chooser.component.ts
│ │ │ ├── favorite-chooser.html
│ │ │ └── favorite-chooser.less
│ │ ├── favorite-list
│ │ │ ├── favorite-list.component.ts
│ │ │ ├── favorite-list.html
│ │ │ └── favorite-list.less
│ │ ├── favorite-manager.service.ts
│ │ ├── home.component.ts
│ │ ├── home.html
│ │ ├── home.less
│ │ ├── home.module.ts
│ │ ├── home.routes.ts
│ │ ├── home.service.ts
│ │ ├── index.ts
│ │ ├── my-bangumi
│ │ │ ├── my-bangumi.component.ts
│ │ │ ├── my-bangumi.html
│ │ │ └── my-bangumi.less
│ │ ├── play-episode
│ │ │ ├── comment
│ │ │ │ ├── comment-form
│ │ │ │ │ ├── comment-form.component.ts
│ │ │ │ │ ├── comment-form.html
│ │ │ │ │ └── comment-form.less
│ │ │ │ ├── comment.component.ts
│ │ │ │ ├── comment.html
│ │ │ │ ├── comment.less
│ │ │ │ └── edit-comment
│ │ │ │ │ ├── edit-comment.component.ts
│ │ │ │ │ ├── edit-comment.html
│ │ │ │ │ └── edit-comment.less
│ │ │ ├── feedback
│ │ │ │ ├── feedback.component.ts
│ │ │ │ ├── feedback.html
│ │ │ │ └── feedback.less
│ │ │ ├── play-episode.component.ts
│ │ │ ├── play-episode.html
│ │ │ ├── play-episode.less
│ │ │ └── reveal-extra
│ │ │ │ ├── reveal-extra.component.ts
│ │ │ │ ├── reveal-extra.html
│ │ │ │ └── reveal-extra.less
│ │ ├── preview-video
│ │ │ ├── preview-video.component.ts
│ │ │ ├── preview-video.html
│ │ │ └── preview-video.less
│ │ ├── rating
│ │ │ ├── edit-review-dialog
│ │ │ │ ├── edit-review-dialog.component.ts
│ │ │ │ ├── edit-review-dialog.html
│ │ │ │ └── edit-review-dialog.less
│ │ │ ├── my-review
│ │ │ │ ├── my-review.component.ts
│ │ │ │ ├── my-review.html
│ │ │ │ └── my-review.less
│ │ │ ├── rating.component.ts
│ │ │ ├── rating.html
│ │ │ └── rating.less
│ │ ├── synchronize.service.ts
│ │ ├── user-action
│ │ │ ├── browser-extension-tip
│ │ │ │ ├── browser-extension-tip.component.ts
│ │ │ │ ├── browser-extension-tip.html
│ │ │ │ └── browser-extension-tip.less
│ │ │ ├── user-action-panel
│ │ │ │ ├── user-action-panel.component.ts
│ │ │ │ ├── user-action-panel.html
│ │ │ │ └── user-action-panel.less
│ │ │ ├── user-action.component.ts
│ │ │ ├── user-action.html
│ │ │ └── user-action.less
│ │ ├── user-center
│ │ │ ├── user-center.component.ts
│ │ │ ├── user-center.html
│ │ │ ├── user-center.less
│ │ │ └── user-center.service.ts
│ │ ├── watch.service.ts
│ │ └── web-hook
│ │ │ ├── web-hook.component.ts
│ │ │ ├── web-hook.html
│ │ │ └── web-hook.less
│ ├── index.ts
│ ├── login
│ │ ├── login.component.ts
│ │ ├── login.html
│ │ └── login.less
│ ├── pipes
│ │ ├── index.ts
│ │ ├── user-level-name.pipe.ts
│ │ └── weekday.pipe.ts
│ ├── register
│ │ ├── register.component.ts
│ │ ├── register.html
│ │ └── register.less
│ ├── reset-pass
│ │ ├── reset-pass.component.ts
│ │ ├── reset-pass.html
│ │ ├── reset-pass.less
│ │ └── reset-pass.module.ts
│ ├── responsive-image
│ │ ├── responsive-image-wrapper.ts
│ │ ├── responsive-image.directive.ts
│ │ ├── responsive-image.module.ts
│ │ └── responsive.service.ts
│ ├── static-content
│ │ ├── apps
│ │ │ ├── apps.component.ts
│ │ │ ├── apps.html
│ │ │ └── apps.less
│ │ ├── developers
│ │ │ ├── developers.component.ts
│ │ │ └── developers.html
│ │ ├── privacy
│ │ │ ├── privacy.component.ts
│ │ │ └── privacy.html
│ │ ├── static-content.component.ts
│ │ ├── static-content.html
│ │ ├── static-content.less
│ │ ├── static-content.module.ts
│ │ ├── static-content.routes.ts
│ │ └── tos
│ │ │ ├── tos.component.ts
│ │ │ └── tos.html
│ ├── user-service
│ │ ├── authentication.service.ts
│ │ ├── index.ts
│ │ ├── persist-storage.ts
│ │ └── user.service.ts
│ └── video-player
│ │ ├── controls
│ │ ├── capture-button
│ │ │ └── capture-button.component.ts
│ │ ├── captured-frame-list
│ │ │ ├── captured-frame-list.component.ts
│ │ │ ├── captured-frame-list.html
│ │ │ ├── captured-frame-list.less
│ │ │ └── operation-dialog
│ │ │ │ ├── operation-dialog.component.ts
│ │ │ │ ├── operation-dialog.html
│ │ │ │ └── operation-dialog.less
│ │ ├── config-button
│ │ │ ├── config-button.component.ts
│ │ │ └── config-panel
│ │ │ │ ├── config-panel.component.ts
│ │ │ │ ├── config-panel.html
│ │ │ │ └── config-panel.less
│ │ ├── controls.component.ts
│ │ ├── controls.html
│ │ ├── controls.less
│ │ ├── fullscreen-button
│ │ │ └── fullscreen-button.component.ts
│ │ ├── help-button
│ │ │ └── help-button.component.ts
│ │ ├── play-button
│ │ │ └── play-button.component.ts
│ │ ├── scrub-bar
│ │ │ ├── scrub-bar.component.ts
│ │ │ ├── scrub-bar.html
│ │ │ └── scrub-bar.less
│ │ ├── time-indicator
│ │ │ └── time-indicator.component.ts
│ │ └── volume-control
│ │ │ ├── volume-control.component.ts
│ │ │ ├── volume-control.html
│ │ │ └── volume-control.less
│ │ ├── core
│ │ ├── full-screen-api.ts
│ │ ├── helpers.ts
│ │ ├── settings.ts
│ │ ├── shortcuts.ts
│ │ ├── state.ts
│ │ └── video-capture.service.ts
│ │ ├── float-controls
│ │ ├── float-controls.component.ts
│ │ ├── float-controls.html
│ │ ├── float-controls.less
│ │ └── non-interactive-progress-bar
│ │ │ ├── non-interactive-progress-bar.component.ts
│ │ │ ├── non-interactive-progress-bar.html
│ │ │ └── non-interactive-progress-bar.less
│ │ ├── help-dialog
│ │ ├── help-dialog.component.ts
│ │ ├── help-dialog.html
│ │ └── help-dialog.less
│ │ ├── next-episode-overlay
│ │ ├── next-episode-overlay.component.ts
│ │ ├── next-episode-overlay.html
│ │ └── next-episode-overlay.less
│ │ ├── touch-controls
│ │ ├── touch-controls.component.ts
│ │ ├── touch-controls.html
│ │ └── touch-controls.less
│ │ ├── video-player.component.ts
│ │ ├── video-player.html
│ │ ├── video-player.less
│ │ ├── video-player.module.ts
│ │ └── video-player.service.ts
├── assets
│ ├── css
│ │ ├── .gitkeep
│ │ └── main.css
│ ├── data.json
│ ├── google-fonts
│ │ ├── 1KWMyx7m-L0fkQGwYhWwuuvvDin1pK8aKteLpeZ5c0A.woff2
│ │ ├── 8qcEw_nrk_5HEcCpYdJu8BTbgVql8nDJpwnrE27mub0.woff2
│ │ ├── AcvTq8Q0lyKKNxRlL28Rn4X0hVgzZQUfRDuZrPvH3D8.woff2
│ │ ├── HkF_qI1x_noxlxhrhMQYEJBw1xU1rKptJj_0jans920.woff2
│ │ ├── MDadn8DQ_3oT6kvnUq_2r_esZW2xOQ-xsNqO47m55DA.woff2
│ │ ├── MgNNr5y1C_tIEuLEmicLmwLUuEpTyoUstqEm5AMlJo4.woff2
│ │ ├── cT2GN3KRBUX69GVJ2b2hxn-_kf6ByYO6CLYdB4HQE-Y.woff2
│ │ └── rZPI2gHXi8zxUjnybc2ZQFKPGs1ZzpMvnHX-7fPOuAc.woff2
│ ├── humans.txt
│ ├── icon
│ │ ├── android-icon-144x144.png
│ │ ├── android-icon-192x192.png
│ │ ├── android-icon-36x36.png
│ │ ├── android-icon-48x48.png
│ │ ├── android-icon-72x72.png
│ │ ├── android-icon-96x96.png
│ │ ├── apple-icon-114x114.png
│ │ ├── apple-icon-120x120.png
│ │ ├── apple-icon-144x144.png
│ │ ├── apple-icon-152x152.png
│ │ ├── apple-icon-180x180.png
│ │ ├── apple-icon-57x57.png
│ │ ├── apple-icon-60x60.png
│ │ ├── apple-icon-72x72.png
│ │ ├── apple-icon-76x76.png
│ │ ├── apple-icon-precomposed.png
│ │ ├── apple-icon.png
│ │ ├── browserconfig.xml
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── favicon.ico
│ │ ├── ms-icon-144x144.png
│ │ ├── ms-icon-150x150.png
│ │ ├── ms-icon-310x310.png
│ │ └── ms-icon-70x70.png
│ ├── img
│ │ ├── 24.gif
│ │ ├── angular-logo.png
│ │ ├── angularclass-avatar.png
│ │ ├── angularclass-logo.png
│ │ ├── christmas-tree.svg
│ │ ├── mana-logo.webp
│ │ ├── mana-preview-1.webp
│ │ ├── mana-preview-2.webp
│ │ ├── megumin-logo.webp
│ │ ├── megumin-preview-1.webp
│ │ ├── megumin-preview-2.webp
│ │ ├── newyear2018.png
│ │ ├── play_badge_new.png
│ │ └── rate_emo.gif
│ ├── manifest.json
│ ├── mock-data
│ │ └── mock-data.json
│ ├── robots.txt
│ ├── semantic-ui.ts
│ ├── service-worker.js
│ └── site
│ │ ├── elements
│ │ └── flag.variables
│ │ ├── globals
│ │ └── site.variables
│ │ └── theme.less
├── custom-typings.d.ts
├── helpers
│ ├── base.service.ts
│ ├── browser-detect.ts
│ ├── dom.ts
│ ├── error
│ │ ├── AuthError.ts
│ │ ├── BaseError.ts
│ │ ├── ClientError.ts
│ │ ├── ServerError.ts
│ │ └── index.ts
│ ├── localstorage.ts
│ └── url.ts
├── index.html
├── main.browser.ts
├── polyfills.browser.ts
├── robots.txt
└── service-worker
│ └── register.ts
├── tsconfig.json
├── tsconfig.webpack.json
├── tslint.json
├── webpack.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | # @AngularClass
2 | # http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_style = space
9 | end_of_line = lf
10 |
11 | [*.{ts,js}]
12 | indent_size = 4
13 | insert_final_newline = true
14 | trim_trailing_whitespace = true
15 |
16 | [*.{yml,json}]
17 | indent_size = 2
18 | trim_trailing_whitespace = true
19 |
20 | [*.html]
21 | indent_size = 4
22 | trim_trailing_whitespace = true
23 |
24 | [*.less]
25 | indent_size = 2
26 | trim_trailing_whitespace = true
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | push:
9 | branches: [ master ]
10 | # pull_request:
11 | # branches: [ master ]
12 |
13 | # Allows you to run this workflow manually from the Actions tab
14 | # workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | to_gitlab:
19 | runs-on: ubuntu-18.04
20 | steps: # <-- must use actions/checkout@v1 before mirroring!
21 | - uses: actions/checkout@v1
22 | - uses: pixta-dev/repository-mirroring-action@v1
23 | with:
24 | target_repo_url:
25 | git@gitlab.com:iroha/Deneb.git
26 | ssh_private_key: # <-- use 'secrets' to pass credential information.
27 | ${{ secrets.GITLAB_SSH_PRIVATE_KEY }}
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # @AngularClass
2 |
3 | # Logs
4 | logs
5 | *.log
6 |
7 | # Runtime data
8 | pids
9 | *.pid
10 | *.seed
11 |
12 | # Directory for instrumented libs generated by jscoverage/JSCover
13 | lib-cov
14 |
15 | # Coverage directory used by tools like istanbul
16 | coverage
17 |
18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
19 | .grunt
20 |
21 | # Compiled binary addons (http://nodejs.org/api/addons.html)
22 | build/Release
23 |
24 | # Users Environment Variables
25 | .lock-wscript
26 |
27 | # OS generated files #
28 | .DS_Store
29 | ehthumbs.db
30 | Icon?
31 | Thumbs.db
32 |
33 | # Node Files #
34 | /node_modules
35 | /bower_components
36 | npm-debug.log
37 |
38 | # Coverage #
39 | /coverage/
40 |
41 | # Typing #
42 | /src/typings/tsd/
43 | /typings/
44 | /tsd_typings/
45 |
46 | # Dist #
47 | /dist
48 | /public/__build__/
49 | /src/*/__build__/
50 | /__build__/**
51 | /public/dist/
52 | /src/*/dist/
53 | /dist/**
54 | .webpack.json
55 | /deploy
56 | deploy.sh
57 |
58 | # AOT #
59 | /compiled
60 |
61 | # Doc #
62 | /doc/
63 |
64 | # IDE #
65 | .idea/
66 | *.swp
67 | .vscode
68 |
69 | # proxy settings #
70 | config/dev.proxy.js
71 |
72 | # custom login/register background #
73 | src/assets/img/background.jpg
74 | src/assets/img/background.png
75 | src/assets/css/login.css
76 |
77 | # bundle report #
78 | /report
79 |
80 | # service worker #
81 | .sw-dist
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: "node:alpine"
2 | build_and_deploy:
3 | only:
4 | refs:
5 | - master
6 | artifacts:
7 | paths:
8 | - dist/
9 | expire_in: 1 day
10 | script:
11 | - yarn install
12 | - npm run build:aot:prod
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-2016 Nyasoft
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/config/dev.proxy.js.example:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | '/api/*': {
3 | target: 'http://localhost:5000'
4 | },
5 | '/pic/*': {
6 | target: 'http://localhost:8000'
7 | },
8 | '/video/*': {
9 | target: 'http://localhost:8000'
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/config/resource-override.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by nene on 2/18/17.
3 | */
4 |
--------------------------------------------------------------------------------
/scripts/font-fix.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const chalk = require('chalk');
3 | // fix well known bug with default distribution
4 | fixFontPath('node_modules/semantic-ui-less/themes/default/globals/site.variables');
5 | fixFontPath('node_modules/semantic-ui-less/themes/flat/globals/site.variables');
6 | fixFontPath('node_modules/semantic-ui-less/themes/material/globals/site.variables');
7 |
8 | function fixFontPath(filename) {
9 | let content = fs.readFileSync(filename, 'utf8');
10 | let newContent = content.replace(
11 | "@fontPath : '../../themes/",
12 | "@fontPath : '../../../themes/"
13 | );
14 | fs.writeFileSync(filename, newContent, 'utf8');
15 | }
16 |
17 | console.log(chalk.green('semantic ui font config has been fixed.'));
18 |
--------------------------------------------------------------------------------
/src/app/RefreshSameRouteStrategy.ts:
--------------------------------------------------------------------------------
1 | import { ActivatedRouteSnapshot, DetachedRouteHandle, RouteReuseStrategy } from '@angular/router';
2 | import { Injectable } from '@angular/core';
3 |
4 | @Injectable()
5 | export class RefreshSameRouteStrategy extends RouteReuseStrategy {
6 |
7 | shouldDetach(route: ActivatedRouteSnapshot): boolean {
8 | return false;
9 | }
10 |
11 | store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void {
12 | }
13 |
14 | shouldAttach(route: ActivatedRouteSnapshot): boolean {
15 | return false;
16 | }
17 |
18 | retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle {
19 | return null;
20 | }
21 |
22 | shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
23 | let refresh = !!future.data && !!future.data.refresh;
24 | return future.routeConfig === curr.routeConfig && !refresh;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/_center-box.less:
--------------------------------------------------------------------------------
1 |
2 | .center-box {
3 | .center-box-header {
4 | text-align: center;
5 | }
6 | .center-box-body {
7 | padding: 15px;
8 | }
9 | }
10 |
11 | @media (max-width: 723px) {
12 | .center-box {
13 | flex-grow: 1;
14 | flex-shrink: 1;
15 | flex-basis: 100%;
16 | max-width: 100%;
17 | max-height: 100%;
18 | .center-box-header {
19 | margin-top: 40px;
20 | }
21 | }
22 | }
23 |
24 | @media (min-width: 723px) {
25 | .center-box {
26 | width: 400px;
27 | }
28 |
29 | .center-box-frame {
30 | //height: 550px;
31 | display: flex;
32 | flex-direction: row;
33 | justify-content: center;
34 | }
35 |
36 | .center-box-background {
37 | position: fixed;
38 | top: 0;
39 | left: 0;
40 | right: 0;
41 | bottom: 0;
42 | display: flex;
43 | flex-direction: column;
44 | justify-content: center;
45 | }
46 | }
47 |
48 | .error-message {
49 | color: #f44336;
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/admin/admin-navbar/admin-navbar.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, ViewEncapsulation} from '@angular/core';
2 |
3 | @Component({
4 | selector: 'admin-navbar',
5 | templateUrl: './admin-navbar.html',
6 | styleUrls: ['./admin-navbar.less'],
7 | encapsulation: ViewEncapsulation.None
8 | })
9 | export class AdminNavbar {
10 | @Input()
11 | navTitle: string;
12 |
13 | @Input()
14 | backLink: string;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/admin/admin-navbar/admin-navbar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/admin/admin-navbar/admin-navbar.less:
--------------------------------------------------------------------------------
1 | .nav-admin {
2 | position: fixed;
3 | top: 0;
4 | left: 260px;
5 | right: 0;
6 | height: 4.8rem;
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: flex-start;
10 | background-color: #ffffff;
11 | border-bottom: 1px solid #cccccc;
12 | z-index: 1000;
13 | .back-button {
14 | display: block;
15 | font-size: 1.2rem;
16 | align-self: center;
17 | padding: 1.8rem;
18 | color: #333333;
19 | cursor: pointer;
20 | &:hover,
21 | &:focus {
22 | color: #5f94c5;
23 | }
24 | }
25 | .title {
26 | font-size: 1.6rem;
27 | font-weight: 500;
28 | color: #656565;
29 | line-height: 4.8rem;
30 | margin: 0 4rem 0 2rem;
31 | }
32 | .action-container {
33 | flex: 1 1;
34 | display: flex;
35 | flex-direction: row;
36 | justify-content: flex-start;
37 | align-items: center;
38 | font-size: 1rem;
39 | > .action-item {
40 | margin: 0 1rem;
41 | }
42 | }
43 | .accessor-action-container {
44 | flex: 1 1;
45 | display: flex;
46 | flex-direction: row;
47 | justify-content: flex-end;
48 | align-items: center;
49 | font-size: 1rem;
50 | margin-right: 3rem;
51 | > .accessory-action {
52 | margin: 0 0.5rem;
53 | &.ui.inline.dropdown {
54 | > .text {
55 | font-weight: normal;
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/admin/admin.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy } from '@angular/core';
2 | import { UserService } from '../user-service/user.service';
3 | import { User } from '../entity/user';
4 | import { Subscription } from 'rxjs';
5 |
6 | @Component({
7 | selector: 'admin',
8 | templateUrl: './admin.html',
9 | styleUrls: ['./admin.less']
10 | })
11 | export class Admin implements OnDestroy {
12 | private _subscription = new Subscription();
13 |
14 | site_title = SITE_TITLE;
15 |
16 | user: User;
17 |
18 | constructor(private _userService: UserService) {
19 | this._subscription.add(
20 | this._userService.userInfo
21 | .subscribe((u) => {
22 | this.user = u;
23 | })
24 | );
25 | }
26 |
27 |
28 | ngOnDestroy(): void {
29 | this._subscription.unsubscribe();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/admin/admin.html:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/admin/admin.less:
--------------------------------------------------------------------------------
1 | @import "./bangumi-detail/bangumi-detail.less";
2 |
3 | .admin-container {
4 | display: block;
5 | .sidebar-header {
6 | height: 10rem;
7 | border-bottom: 1px solid rgba(225, 225, 225, 0.08);
8 | }
9 | .brand {
10 | font-size: 2.2rem;
11 | color: #fff;
12 | display: block;
13 | width: 100%;
14 | line-height: 5rem;
15 | padding-left: 1.1428rem;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/app/admin/admin.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { Admin } from './admin.component';
3 | import { BangumiDetail } from './bangumi-detail/bangumi-detail.component';
4 | import { ListBangumi } from './list-bangumi/list-bangumi.component';
5 | import { TaskManager } from './task-manager/task-manager.component';
6 | import { UserManager } from './user-manager/user-manager.component';
7 | import { AnnounceComponent } from './announce/announce.component';
8 | import { WebHookComponent } from './web-hook/web-hook.component';
9 |
10 |
11 | export const adminRoutes: Routes = [
12 | {
13 | path: '',
14 | component: Admin,
15 | children: [
16 | {
17 | path: 'bangumi/:id',
18 | component: BangumiDetail
19 | },
20 | {
21 | path: 'bangumi',
22 | component: ListBangumi
23 | },
24 | {
25 | path: 'user',
26 | component: UserManager
27 | },
28 | {
29 | path: 'task',
30 | component: TaskManager
31 | },
32 | {
33 | path: 'announce',
34 | component: AnnounceComponent
35 | },
36 | {
37 | path: 'web-hook',
38 | component: WebHookComponent
39 | },
40 | {
41 | path: '',
42 | redirectTo: 'bangumi',
43 | pathMatch: 'full'
44 | }
45 | ]
46 | }
47 | ];
48 |
--------------------------------------------------------------------------------
/src/app/admin/announce/announce.less:
--------------------------------------------------------------------------------
1 | .content-area {
2 | position: fixed;
3 | top: 4.8rem;
4 | left: 260px;
5 | bottom: 0;
6 | right: 0;
7 | overflow-x: hidden;
8 | overflow-y: auto;
9 | padding: 0.5rem 1rem;
10 | background-color: #f0f0f0;
11 | }
12 |
13 | .table {
14 | td {
15 | overflow: hidden;
16 | white-space: normal;
17 | -ms-word-break: break-all;
18 | word-break: break-all;
19 | }
20 | }
21 |
22 | .anchor-button {
23 | display: inline-block;
24 | margin: 0 0.2rem;
25 | cursor: pointer;
26 | color: #333333;
27 | padding: 0.3em;
28 | &:hover,
29 | &:focus {
30 | color: #5f94c5;
31 | }
32 | }
--------------------------------------------------------------------------------
/src/app/admin/announce/announce.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 | import { BaseService } from '../../../helpers/base.service';
6 | import { Announce } from '../../entity/announce';
7 |
8 | @Injectable()
9 | export class AnnounceService extends BaseService {
10 |
11 | private _baseUrl = '/api/announce';
12 |
13 | constructor(private _http: HttpClient) {
14 | super();
15 | }
16 |
17 | listAnnounce(position: number, offset: number, count: number, content?: string): Observable<{data: Announce[], total: number}> {
18 | return this._http.get<{data: Announce[], total: number}>(this._baseUrl, {
19 | params: {
20 | position: position + '',
21 | offset: offset + '',
22 | count: count + '',
23 | content: content
24 | }
25 | }).pipe(
26 | catchError(this.handleError),);
27 | }
28 |
29 | addAnnounce(announce: Announce): Observable {
30 | return this._http.post(this._baseUrl, announce).pipe(
31 | catchError(this.handleError),);
32 | }
33 |
34 | updateAnnounce(announce_id: string, announce: Announce): Observable {
35 | return this._http.put(`${this._baseUrl}/${announce_id}`, announce).pipe(
36 | catchError(this.handleError),);
37 | }
38 |
39 | deleteAnnounce(announce_id: string): Observable {
40 | return this._http.delete(`${this._baseUrl}/${announce_id}`).pipe(
41 | catchError(this.handleError),);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/admin/announce/edit-announce/edit-announce.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 767px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 768px) {
9 | width: 768px;
10 | }
11 | @media(min-height: 640px) {
12 | height: 640px;
13 | }
14 | @media(max-height: 639px) {
15 | height: 100%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | }
27 |
28 | .edit-announce-form {
29 | height: 100%;
30 | }
31 |
32 | .announce-info {
33 | width: 100%;
34 | height: 100%;
35 | overflow-x: hidden;
36 | overflow-y: auto;
37 | padding: 1.5rem 1.5rem 6rem 1.5rem;
38 | }
39 |
40 | .footer {
41 | position: absolute;
42 | width: 100%;
43 | height: 5rem;
44 | left: 0;
45 | bottom: 0;
46 | border-top: 1px solid #e2e2e2;
47 | background-color: #fff;
48 | display: flex;
49 | flex-direction: row;
50 | justify-content: flex-end;
51 | align-items: center;
52 | padding-right: 2rem;
53 | > .ui.button {
54 | margin-right: 2rem;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/app/admin/announce/edit-bangumi-recommend/edit-bangumi-recommend.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 489px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 490px) {
9 | width: 490px;
10 | }
11 | @media(min-height: 440px) {
12 | height: 450px;
13 | }
14 | @media(max-height: 399px) {
15 | height: 100%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | }
27 |
28 | .bangumi-recommend-form {
29 | height: 100%;
30 | }
31 |
32 | .bangumi-info {
33 | height: 4rem;
34 | padding: 0.5rem 1.5rem;
35 | }
36 |
37 | .announce-info {
38 | width: 100%;
39 | height: 100%;
40 | overflow-x: hidden;
41 | overflow-y: auto;
42 | padding: 1.5rem 1.5rem 6rem 1.5rem;
43 | }
44 |
45 | .footer {
46 | position: absolute;
47 | width: 100%;
48 | height: 5rem;
49 | left: 0;
50 | bottom: 0;
51 | border-top: 1px solid #e2e2e2;
52 | background-color: #fff;
53 | display: flex;
54 | flex-direction: row;
55 | justify-content: flex-end;
56 | align-items: center;
57 | padding-right: 2rem;
58 | > .ui.button {
59 | margin-right: 2rem;
60 | }
61 | }
--------------------------------------------------------------------------------
/src/app/admin/bangumi-card/bangumi-card.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
![image]()
16 |
17 |
18 |
{{bangumi.name_cn || '暂无'}}已添加
19 |
20 | 日文名
21 | {{bangumi.name || '暂无'}}
22 |
23 |
24 | 简介
25 | {{bangumi.summary || '暂无'}}
26 |
27 |
28 | 放送开始
29 | {{bangumi.air_date || '未知'}}
30 |
31 |
32 | 放送星期
33 | {{bangumi.air_weekday || '未知'}}
34 |
35 |
36 | 话数
37 | {{bangumi.eps || '0'}}
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-card/bangumi-card.less:
--------------------------------------------------------------------------------
1 | @image-width: 10rem;
2 |
3 | bangumi-card {
4 | display: block;
5 | box-sizing: border-box;
6 | padding: 0.5rem 0;
7 | height: 16rem;
8 | > .ui.segment {
9 | width: 100%;
10 | height: 100%;
11 | margin: 0;
12 | }
13 | }
14 |
15 | .bangumi-list-item {
16 | position: relative;
17 | padding-left: @image-width;
18 | width: 100%;
19 | height: 100%;
20 | .cover-wrapper {
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | width: @image-width;
25 | height: 100%;
26 | overflow: hidden;
27 | > .bangumi-image {
28 | display: block;
29 | object-fit: cover;
30 | width: 100%;
31 | height: 100%;
32 | }
33 | }
34 |
35 | .list-item-text {
36 | width: 100%;
37 | padding-right: 1rem;
38 | .bangumi-title {
39 | width: 100%;
40 | overflow: hidden;
41 | margin: 0 1.5rem 1rem 1.5rem;
42 | white-space: nowrap;
43 | text-overflow: ellipsis;
44 | .ui.label {
45 | margin-left: 1em;
46 | position: relative;
47 | top: -0.1rem;
48 | }
49 | }
50 | h4.entry {
51 | margin: 0 0 0.6rem 0;
52 | .entry-value {
53 | padding-left: 7.4em;
54 | }
55 | }
56 | .entry {
57 | position: relative;
58 | margin: 0 0 0.6em 0;
59 | .entry-key {
60 | width: 7em;
61 | display: block;
62 | position: absolute;
63 | left: 0;
64 | top: 0;
65 | text-align: right;
66 | padding-right: 1em;
67 | font-size: 1rem;
68 | color: #828574;
69 | font-weight: normal;
70 | }
71 | .entry-value {
72 | display: block;
73 | padding-left: 8em;
74 | width: 100%;
75 | white-space: nowrap;
76 | overflow: hidden;
77 | text-overflow: ellipsis;
78 | }
79 | }
80 | }
81 |
82 | .exist-indicator {
83 | color: #f44336 !important;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/bangumi-basic/bangumi-basic.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { Bangumi } from '../../../entity/bangumi';
3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms';
4 | import { UIDialogRef } from 'deneb-ui';
5 | import { User } from '../../../entity/user';
6 |
7 | @Component({
8 | selector: 'bangumi-basic',
9 | templateUrl: './bangumi-basic.html',
10 | styleUrls: ['./bangumi-basic.less']
11 | })
12 | export class BangumiBasic implements OnInit {
13 |
14 | @Input()
15 | bangumi: Bangumi;
16 |
17 | bangumiForm: FormGroup;
18 |
19 | adminList: User[];
20 |
21 | constructor(private _fb: FormBuilder,
22 | private _dialogRef: UIDialogRef) {
23 | }
24 |
25 | ngOnInit(): void {
26 |
27 |
28 | this.bangumiForm = this._fb.group({
29 | name: [this.bangumi.name, Validators.required],
30 | name_cn: [this.bangumi.name_cn, Validators.required],
31 | summary: this.bangumi.summary,
32 | air_date: [this.bangumi.air_date, Validators.required],
33 | air_weekday: this.bangumi.air_weekday,
34 | eps_no_offset: this.bangumi.eps_no_offset,
35 | status: this.bangumi.status,
36 | maintained_by_uid: this.bangumi.maintained_by ? this.bangumi.maintained_by.id: '',
37 | alert_timeout: this.bangumi.alert_timeout
38 | });
39 | }
40 |
41 | cancel() {
42 | this._dialogRef.close(null);
43 | }
44 |
45 | save() {
46 | this._dialogRef.close(this.bangumiForm.value);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/bangumi-basic/bangumi-basic.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 767px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 768px) {
9 | width: 768px;
10 | }
11 | @media(min-height: 640px) {
12 | height: 640px;
13 | }
14 | @media(max-height: 639px) {
15 | height: 100%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | }
27 |
28 | .bangumi-form {
29 | height: 100%;
30 | }
31 |
32 | .bangumi-basic-info {
33 | width: 100%;
34 | height: 100%;
35 | overflow-x: hidden;
36 | overflow-y: auto;
37 | padding: 1.5rem 1.5rem 6rem 1.5rem;
38 | }
39 |
40 | .footer {
41 | position: absolute;
42 | width: 100%;
43 | height: 5rem;
44 | left: 0;
45 | bottom: 0;
46 | border-top: 1px solid #e2e2e2;
47 | background-color: #fff;
48 | display: flex;
49 | flex-direction: row;
50 | justify-content: flex-end;
51 | align-items: center;
52 | padding-right: 2rem;
53 | > .ui.button {
54 | margin-right: 2rem;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/bangumi-moe-builder/bangum-moe-entity.ts:
--------------------------------------------------------------------------------
1 | export class Tag {
2 | _id: string;
3 | activity: number;
4 | locale: { ja: string, zh_cn: string, zh_tw: string, en: string };
5 | name: string;
6 | syn_lowercase: string[];
7 | synonyms: string[];
8 | type: string
9 | }
10 |
11 | export class Torrent {
12 | btskey: string;
13 | category_tag_id: string;
14 | comments: number;
15 | content: string[][];
16 | downloads: number;
17 | file_id: string;
18 | finished: 807;
19 | infoHash: string;
20 | introduction: string;
21 | leechers: 15;
22 | magnet: string;
23 | publish_time: string;
24 | seeders: number;
25 | size: string;
26 | tag_ids: string[];
27 | team_id: string;
28 | team_sync: any;
29 | title: string;
30 | uploader_id: string;
31 | _id: string;
32 | // add by Albireo
33 | eps_no_list: number[]
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/episode-detail/episode-detail.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 639px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 640px) {
9 | width: 500px;
10 | }
11 | @media(min-height: 650px) {
12 | height: 600px;
13 | }
14 | @media(max-height: 649px) {
15 | height: 90%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | padding-bottom: 5rem;
27 | }
28 | .episode-detail-modal {
29 | width: 100%;
30 | height: 100%;
31 | padding: 1.2rem 1.5rem 0.5rem 1.5rem;
32 | overflow-x: hidden;
33 | overflow-y: auto;
34 | }
35 |
36 | .footer {
37 | position: absolute;
38 | width: 100%;
39 | height: 5rem;
40 | left: 0;
41 | bottom: 0;
42 | border-top: 1px solid #e2e2e2;
43 | background-color: #fff;
44 | display: flex;
45 | flex-direction: row;
46 | justify-content: flex-end;
47 | align-items: center;
48 | padding-right: 1.6rem;
49 | > .ui.button {
50 | margin-right: 2rem;
51 | }
52 | .episode-status-label {
53 | position: absolute;
54 | top: 1.6rem;
55 | left: 2rem;
56 | }
57 | }
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/index.ts:
--------------------------------------------------------------------------------
1 | export * from './bangumi-detail.component';
2 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-detail/video-file-modal/video-file-list.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 659px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 660px) {
9 | width: 650px;
10 | }
11 | @media(min-height: 760px) {
12 | height: 750px;
13 | }
14 | @media(max-height: 759px) {
15 | height: 90%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | }
27 |
28 | .video-file-modal {
29 | width: 100%;
30 | height: 100%;
31 | padding: 6rem 1.5rem 2rem 1.5rem;
32 | overflow-x: hidden;
33 | overflow-y: auto;
34 | }
35 |
36 | .modal-header {
37 | padding-left: 1rem;
38 | position: absolute;
39 | top: 0;
40 | left: 0;
41 | width: 100%;
42 | height: 3rem;
43 | > h4 {
44 | margin: 0;
45 | height: 3rem;
46 | line-height: 3rem;
47 | }
48 | .close-button {
49 | position: absolute;
50 | right: 0.5rem;
51 | top: 0.4rem;
52 | color: #9f9f9f;
53 | cursor: pointer;
54 | &:hover,
55 | &:focus {
56 | color: #4f7ba4;
57 | }
58 | }
59 | }
60 |
61 | .control-bar {
62 | padding-left: 1rem;
63 | position: absolute;
64 | top: 3rem;
65 | left: 0;
66 | width: 100%;
67 | height: 3rem;
68 | line-height: 3rem;
69 | .add-button {
70 | display: inline-block;
71 | cursor: pointer;
72 | }
73 | }
74 |
75 | .footer {
76 | position: absolute;
77 | left: 0;
78 | bottom: 0;
79 | width: 100%;
80 | font-size: 0.8rem;
81 | color: #656565;
82 | padding: 0.6rem 1rem;
83 | }
--------------------------------------------------------------------------------
/src/app/admin/bangumi-pipes/libyk-pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | @Pipe({name: 'libykFormat'})
4 | export class LibykPipe implements PipeTransform {
5 | transform(value: string, key?: string): any {
6 | if (value) {
7 | let obj = JSON.parse(value);
8 | if (key) {
9 | return obj[key];
10 | }
11 | return `[${obj.t}] ${obj.q}`;
12 | }
13 | return '';
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-pipes/nyaa-pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | export const AVAILABLE_FILTER = ['No filter', 'No remakes', 'Trusted only'];
4 | export const AVAILABLE_CATEGORY = {'1_2': 'Anime - English-translated', '1_3': 'Anime - Non-English-translated', '1_4': 'Anime - Raw'};
5 |
6 | @Pipe({name: 'NyaaPipe'})
7 | export class NyaaPipe implements PipeTransform {
8 |
9 | transform(value: string, key: string): any {
10 | if (value) {
11 | let params = new URLSearchParams(value);
12 | let v = params.get(key);
13 | if (key === 'f') {
14 | return AVAILABLE_FILTER[v];
15 | } else if (key === 'c') {
16 | return AVAILABLE_CATEGORY[v];
17 | }
18 | return v;
19 | }
20 | return '';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-pipes/parse-json.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | @Pipe({name: 'parseJson'})
4 | export class ParseJsonPipe implements PipeTransform {
5 |
6 | transform(json: string): any {
7 | try {
8 | return JSON.parse(json);
9 | } catch (e) {
10 | return [];
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-pipes/status-name-pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | export const BANGUMI_STATUS = {
4 | 0: 'Pending',
5 | 1: 'On Air',
6 | 2: 'Finished'
7 | };
8 |
9 | @Pipe({name: 'bangumiStatusName'})
10 | export class BangumiStatusNamePipe implements PipeTransform {
11 | transform(value: number): any {
12 | return BANGUMI_STATUS[value];
13 | }
14 | }
15 |
16 | export const VIDEO_FILE_STATUS = {
17 | 1: '未下载',
18 | 2: '下载中',
19 | 3: '已下载'
20 | };
21 |
22 | @Pipe({name: 'videoFileStatusName'})
23 | export class VideoFileStatusNamePipe implements PipeTransform {
24 |
25 | transform(value: number): any {
26 | return VIDEO_FILE_STATUS[value];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/admin/bangumi-pipes/type-name-pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | export const BANGUMI_TYPES = {
4 | 2: '动画',
5 | 6: '电视剧'
6 | };
7 |
8 | @Pipe({name: 'bangumiTypeName'})
9 | export class BangumiTypeNamePipe implements PipeTransform {
10 |
11 | transform(value: number | string): any {
12 | return BANGUMI_TYPES[value] || '未知类型';
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/admin/index.ts:
--------------------------------------------------------------------------------
1 | export {AdminModule} from './admin.module';
2 |
--------------------------------------------------------------------------------
/src/app/admin/list-bangumi/list-bangumi.less:
--------------------------------------------------------------------------------
1 | .search-action {
2 | width: 20rem;
3 | height: 2.8rem;
4 | }
5 | .sort-button {
6 | padding: 0 0.4rem;
7 | }
8 | .anchor-button {
9 | cursor: pointer;
10 | color: #333333;
11 | }
12 | .content-area {
13 | position: fixed;
14 | top: 4.8rem;
15 | left: 260px;
16 | bottom: 0;
17 | right: 0;
18 | overflow: hidden;
19 | background-color: #f0f0f0;
20 | }
21 | .all-bangumi-container {
22 | width: 100%;
23 | height: 100%;
24 | padding: 0 2rem;
25 | }
26 | .search-result-container {
27 | width: 100%;
28 | height: 100%;
29 | padding: 0 2rem;
30 | overflow-x: hidden;
31 | overflow-y: auto;
32 | .bangumi-list {
33 | margin-top: 1.2rem;
34 | margin-bottom: 2rem;
35 | }
36 | }
37 | .no-result-container {
38 | width: 100%;
39 | height: 100%;
40 | overflow: hidden;
41 | position: relative;
42 | .no-result-tips {
43 | position: absolute;
44 | top: 0;
45 | left: 0;
46 | right: 0;
47 | bottom: 0;
48 | margin: auto;
49 | width: 8em;
50 | height: 2em;
51 | font-size: 4rem;
52 | color: #9f9f9f;
53 | }
54 | }
55 | .searching-container {
56 | width: 100%;
57 | height: 100%;
58 | overflow: hidden;
59 | position: relative;
60 | .loader {
61 | color: #9f9f9f;
62 | }
63 | }
--------------------------------------------------------------------------------
/src/app/admin/list-bangumi/list-bangumi.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable()
4 | export class ListBangumiService {
5 | scrollPosition: number;
6 | orderBy: string;
7 | sort: string;
8 | type: number;
9 | isMovie: boolean;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/admin/search-bangumi/index.ts:
--------------------------------------------------------------------------------
1 | export * from './search-bangumi.component';
2 |
--------------------------------------------------------------------------------
/src/app/admin/search-bangumi/result-detail/result-detail.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/admin/search-bangumi/result-detail/result-detail.less:
--------------------------------------------------------------------------------
1 | .result-detail {
2 | width: 100%;
3 | height: 100%;
4 | top: 0;
5 | left: 0;
6 | padding-top: 4rem;
7 | position: absolute;
8 | transition: transform 300ms ease-out;
9 | transform: translate3d(100%, 0, 0);
10 | &.show-detail {
11 | transform: translate3d(0, 0, 0);
12 | }
13 | }
14 | .action-bar {
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | width: 100%;
19 | height: 4rem;
20 | border-bottom: 1px solid #cccccc;
21 | border-top-left-radius: 4px;
22 | border-top-right-radius: 4px;
23 | background-color: #ffffff;
24 | display: flex;
25 | flex-direction: row;
26 | justify-content: flex-start;
27 | align-items: center;
28 | .back-button {
29 | display: inline-block;
30 | padding: 0.6rem;
31 | color: #333333;
32 | cursor: pointer;
33 | margin-left: 2rem;
34 | margin-right: 4rem;
35 | }
36 | .title {
37 | flex: 1 1 auto;
38 | font-size: 1.2rem;
39 | }
40 | .confirm-button {
41 | flex: 0 0 auto;
42 | display: inline-block;
43 | padding: 0.6rem;
44 | color: #333333;
45 | cursor: pointer;
46 | margin-left: 2rem;
47 | margin-right: 2rem;
48 | }
49 | }
50 | .detail-container {
51 | width: 100%;
52 | height: 100%;
53 | background-color: #fff;
54 | border-bottom-left-radius: 4px;
55 | border-bottom-right-radius: 4px;
56 | overflow-x: hidden;
57 | overflow-y: auto;
58 | }
59 | .bangumi-form {
60 | display: flex;
61 | flex-direction: row;
62 | justify-content: flex-start;
63 | align-items: flex-start;
64 | .bangumi-image {
65 | width: 30%;
66 | padding: 1rem;
67 | > img {
68 | display: block;
69 | width: 100%;
70 | height: auto;
71 | }
72 | }
73 | .bangumi-basic-info {
74 | flex: 1 0 auto;
75 | display: flex;
76 | flex-direction: column;
77 | justify-content: flex-start;
78 | align-items: stretch;
79 | padding: 1rem 1rem 1rem 0.5rem;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/admin/task-manager/task-manager.less:
--------------------------------------------------------------------------------
1 | .content-area {
2 | position: fixed;
3 | top: 4.8rem;
4 | left: 260px;
5 | bottom: 0;
6 | right: 0;
7 | overflow: hidden;
8 | padding: 0.5rem 1rem;
9 | background-color: #f0f0f0;
10 | .task-list {
11 | width: 100%;
12 | height: 100%;
13 | overflow-x: hidden;
14 | overflow-y: auto;
15 | }
16 | }
17 |
18 | .anchor-button {
19 | cursor: pointer;
20 | color: #333333;
21 | padding: 0.4em;
22 | &:hover,
23 | &:focus {
24 | color: #5f94c5;
25 | }
26 | }
--------------------------------------------------------------------------------
/src/app/admin/task-manager/task.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 | import { BaseService } from '../../../helpers/base.service';
6 | import { Bangumi, Episode } from '../../entity';
7 |
8 | @Injectable()
9 | export class TaskService extends BaseService {
10 | private _baseUrl = '/api/task';
11 |
12 | constructor(private _http: HttpClient) {
13 | super();
14 | }
15 |
16 | listPendingDeleteBangumi(): Observable<{data: Bangumi[], delete_delay: number}> {
17 | return this._http.get<{data: Bangumi[], delete_delay: number}>(`${this._baseUrl}/bangumi`).pipe(
18 | catchError(this.handleError),);
19 | }
20 |
21 | listPendingDeleteEpisode(): Observable<{data: Episode[], delete_delay: number}> {
22 | return this._http.get<{data: Episode[], delete_delay: number}>(`${this._baseUrl}/episode`).pipe(
23 | catchError(this.handleError),);
24 | }
25 |
26 | restoreBangumi(bangumi_id: string): Observable {
27 | return this._http.post(`${this._baseUrl}/restore/bangumi/${bangumi_id}`, null).pipe(
28 | catchError(this.handleError),);
29 | }
30 |
31 | restoreEpisode(episode_id: string): Observable {
32 | return this._http.post(`${this._baseUrl}/restore/episode/${episode_id}`, null).pipe(
33 | catchError(this.handleError),);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/app/admin/user-manager/user-manager.less:
--------------------------------------------------------------------------------
1 | .content-area {
2 | position: fixed;
3 | top: 4.8rem;
4 | left: 260px;
5 | bottom: 0;
6 | right: 0;
7 | overflow-x: hidden;
8 | overflow-y: auto;
9 | padding: 0.5rem 1rem;
10 | background-color: #f0f0f0;
11 | }
12 |
13 | .pagination-container {
14 | display: flex;
15 | flex-direction: row;
16 | justify-content: center;
17 | }
18 |
19 | .anchor-button {
20 | cursor: pointer;
21 | color: #333333;
22 | padding: 0.4em;
23 | &:hover,
24 | &:focus {
25 | color: #5f94c5;
26 | }
27 | }
--------------------------------------------------------------------------------
/src/app/admin/user-manager/user-manager.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 | import { BaseService } from '../../../helpers/base.service';
6 | import { queryString } from '../../../helpers/url'
7 | import { User } from '../../entity';
8 |
9 | @Injectable()
10 | export class UserManagerSerivce extends BaseService {
11 | private _baseUrl = '/api/user-manage';
12 |
13 | constructor(private _http: HttpClient) {
14 | super()
15 | }
16 |
17 | listUser(params: {
18 | count: number,
19 | offset: number,
20 | minlevel?: number,
21 | query_field?: string,
22 | query_value?: string
23 | }): Observable<{data: User[], total: number}> {
24 | let queryParams = queryString(params);
25 | return this._http.get<{data: User[], total: number}>(`${this._baseUrl}/?${queryParams}`).pipe(
26 | catchError(this.handleError),);
27 | }
28 |
29 | promoteUser(user_id: string, toLevel: number): Observable {
30 | return this._http.post(`${this._baseUrl}/promote`, {id: user_id, to_level: toLevel}).pipe(
31 | catchError(this.handleError),);
32 | }
33 |
34 | listUnusedInviteCode(): Observable {
35 | return this._http.get<{data: string[]}>(`${this._baseUrl}/invite/unused`).pipe(
36 | map(res => res.data),
37 | catchError(this.handleError),);
38 | }
39 |
40 | createInviteCode(num: number = 1): Observable {
41 | return this._http.post<{data: string[]}>(`${this._baseUrl}/invite?num=${num}`, null).pipe(
42 | map(res => res.data),
43 | catchError(this.handleError),);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/admin/user-manager/user-promote-modal/user-promote-modal.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {UIDialogRef} from 'deneb-ui';
3 |
4 | @Component({
5 | selector: 'user-promote-modal',
6 | templateUrl: './user-promote-modal.html',
7 | styleUrls: ['./user-promote-modal.less']
8 | })
9 | export class UserPromoteModal {
10 |
11 | @Input() level: number;
12 |
13 | constructor(private _dialogRef: UIDialogRef){}
14 |
15 | cancel() {
16 | this._dialogRef.close(null);
17 | }
18 |
19 | save() {
20 | this._dialogRef.close({level: this.level});
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/admin/user-manager/user-promote-modal/user-promote-modal.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/admin/user-manager/user-promote-modal/user-promote-modal.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 465px) {
6 | width: 90%;
7 | }
8 | @media(min-width: 466px) {
9 | width: 470px;
10 | }
11 | @media(min-height: 300px) {
12 | height: 280px;
13 | }
14 | @media(max-height: 299px) {
15 | height: 90%;
16 | }
17 | margin: auto;
18 | top: 0;
19 | left: 0;
20 | right: 0;
21 | bottom: 0;
22 | position: absolute;
23 | border-radius: 4px;
24 | background-color: #fff;
25 | overflow: hidden;
26 | padding-bottom: 5rem;
27 | }
28 |
29 | .user-promote-modal {
30 | padding: 2rem;
31 | text-align: center;
32 | }
33 |
34 | .footer {
35 | position: absolute;
36 | width: 100%;
37 | height: 5rem;
38 | left: 0;
39 | bottom: 0;
40 | border-top: 1px solid #e2e2e2;
41 | background-color: #fff;
42 | display: flex;
43 | flex-direction: row;
44 | justify-content: flex-end;
45 | align-items: center;
46 | padding-right: 1.6rem;
47 | > .ui.button {
48 | margin-right: 2rem;
49 | }
50 | }
--------------------------------------------------------------------------------
/src/app/admin/web-hook/web-hook-card/web-hook-card.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, ViewEncapsulation } from '@angular/core';
2 | import { WebHook } from '../../../entity/web-hook';
3 |
4 | @Component({
5 | selector: 'web-hook-card',
6 | templateUrl: './web-hook-card.html',
7 | styleUrls: ['./web-hook-card.less'],
8 | encapsulation: ViewEncapsulation.None
9 | })
10 | export class WebHookCardComponent {
11 | @Input()
12 | webHook: WebHook;
13 |
14 | onClickId(event: Event) {
15 | event.preventDefault();
16 | event.stopPropagation();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/admin/web-hook/web-hook-card/web-hook-card.html:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 | URL:
17 | {{webHook.url}}
18 |
19 |
20 | {{webHook.description}}
21 |
22 |
23 | 创建者: {{webHook.created_by?.name}}
24 | 创建于: {{webHook.register_time | date:'fullDate'}}
25 |
26 |
--------------------------------------------------------------------------------
/src/app/admin/web-hook/web-hook-card/web-hook-card.less:
--------------------------------------------------------------------------------
1 | web-hook-card {
2 | display: block;
3 | box-sizing: border-box;
4 | padding: 0.5rem 0;
5 | height: 12rem;
6 | > .ui.segment {
7 | width: 100%;
8 | height: 100%;
9 | margin: 0;
10 |
11 | .url {
12 | font-size: 0.9em;
13 | color: #555555;
14 | margin-bottom: 0.5em;
15 | }
16 |
17 | .desc {
18 | overflow: hidden;
19 | text-overflow: ellipsis;
20 | -webkit-box-orient: vertical;
21 | -webkit-line-clamp: 2;
22 | font-size: 1rem;
23 | line-height: 1rem;
24 | max-height: 2rem;
25 | }
26 | }
27 |
28 | .created_by > .detail {
29 | padding-right: 1em;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/app/admin/web-hook/web-hook.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 添加Web Hook
4 |
5 |
6 |
7 |
0">
8 |
9 |
12 |
13 |
14 |
17 |
20 |
--------------------------------------------------------------------------------
/src/app/admin/web-hook/web-hook.less:
--------------------------------------------------------------------------------
1 | .content-area {
2 | position: fixed;
3 | top: 4.8rem;
4 | left: 260px;
5 | bottom: 0;
6 | right: 0;
7 | overflow-x: hidden;
8 | overflow-y: auto;
9 | padding: 0.5rem 1rem;
10 | background-color: #f0f0f0;
11 | }
12 |
13 | .anchor-button {
14 | cursor: pointer;
15 | color: #333333;
16 | }
17 |
18 | .all-web-hook-container {
19 | width: 100%;
20 | height: 100%;
21 | padding: 0 2rem;
22 | }
23 |
24 | .no-result-container {
25 | width: 100%;
26 | height: 100%;
27 | overflow: hidden;
28 | position: relative;
29 | .no-result-tips {
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | right: 0;
34 | bottom: 0;
35 | margin: auto;
36 | width: 12em;
37 | height: 2em;
38 | font-size: 3rem;
39 | color: #9f9f9f;
40 | }
41 | }
42 | .searching-container {
43 | width: 100%;
44 | height: 100%;
45 | overflow: hidden;
46 | position: relative;
47 | .loader {
48 | color: #9f9f9f;
49 | }
50 | }
--------------------------------------------------------------------------------
/src/app/alert-dialog/alert-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { UIDialogRef } from 'deneb-ui';
3 |
4 | @Component({
5 | selector: 'alert-dialog',
6 | templateUrl: './alert-dialog.html'
7 | })
8 | export class AlertDialog {
9 | @Input()
10 | confirmButtonText: string;
11 |
12 | @Input()
13 | title: string;
14 |
15 | @Input()
16 | content: string;
17 |
18 |
19 | constructor(private _dialogRef: UIDialogRef) {}
20 |
21 | confirm() {
22 | this._dialogRef.close('confirm');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/alert-dialog/alert-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 | {{confirmButtonText}}
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/app/alert-dialog/alert-dialog.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { AlertDialog } from './alert-dialog.component';
3 | import { UIDialogModule } from 'deneb-ui';
4 | @NgModule({
5 | declarations: [AlertDialog],
6 | imports: [UIDialogModule],
7 | exports: [AlertDialog],
8 | entryComponents: [AlertDialog]
9 | })
10 | export class AlertDialogModule {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/analytics.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {Router, NavigationEnd} from '@angular/router';
3 |
4 | @Injectable()
5 | export class AnalyticsService {
6 |
7 | constructor(private router: Router) {
8 | this.router.events
9 | .subscribe(
10 | (event) => {
11 | if(event instanceof NavigationEnd) {
12 | this.routeChanged(this.getPath(event.url));
13 | }
14 | }
15 | )
16 | }
17 |
18 | private getPath(url: string): string {
19 | return url.split(';')[0];
20 | }
21 |
22 | private routeChanged(route: string): void {
23 | if(typeof ga === 'undefined') {
24 | ga_events.push(
25 | ['set', 'page', route],
26 | ['send', 'pageview']
27 | )
28 | } else {
29 | ga('set', 'page', route);
30 | ga('send', 'pageview');
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * Angular 2 decorators and services
3 | */
4 | import { Component, ViewEncapsulation } from '@angular/core';
5 |
6 | import { AnalyticsService } from './analytics.service';
7 | import { Router, NavigationEnd, NavigationStart } from '@angular/router';
8 | import { Subscription } from 'rxjs';
9 |
10 | /*
11 | * App Component
12 | * Top Level Component
13 | */
14 | @Component({
15 | selector: 'app',
16 | template: `
17 |
18 |
19 | `,
20 | encapsulation: ViewEncapsulation.None
21 | })
22 | export class App {
23 |
24 | private routeEventsSubscription: Subscription;
25 |
26 | private removePreLoader() {
27 | if (document) {
28 | let $body = document.body;
29 | let preloader = document.getElementById('preloader');
30 | if (preloader) {
31 | $body.removeChild(preloader);
32 | this.routeEventsSubscription.unsubscribe();
33 | }
34 | $body.classList.remove('loading');
35 | }
36 | }
37 |
38 | constructor(analyticsSerivce: AnalyticsService, router: Router) {
39 | this.routeEventsSubscription = router.events
40 | .subscribe(
41 | (event) => {
42 | if (event instanceof NavigationEnd) {
43 | this.removePreLoader();
44 | }
45 | }
46 | )
47 | }
48 | }
49 |
50 | /*
51 | * Please review the https://github.com/AngularClass/angular2-examples/ repo for
52 | * more angular app examples that you may copy/paste
53 | * (The examples may not be updated as quickly. Please open an issue on github for us to update it)
54 | * For help or questions please contact us at @AngularClass on twitter
55 | * or our chat on Slack at https://AngularClass.com/slack-join
56 | */
57 |
--------------------------------------------------------------------------------
/src/app/app.less:
--------------------------------------------------------------------------------
1 | app {
2 | display: block;
3 | height: 100%;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { homeRoutes } from './home/home.routes';
2 | import { Register } from './register/register.component';
3 | import { Login } from './login/login.component';
4 | import { ErrorComponent } from './error/error.component';
5 | import { Routes } from '@angular/router';
6 | import { Authentication } from './user-service';
7 | import { EmailConfirm } from './email-confirm/email-confirm.component';
8 | import { ForgetPass } from './forget-pass/forget-pass.component';
9 | import { ResetPass } from './reset-pass/reset-pass.component';
10 | import { staticContentRoutes } from './static-content/static-content.routes';
11 |
12 |
13 | export const appRoutes: Routes = [
14 | ...homeRoutes,
15 | {
16 | path: 'admin',
17 | data: {level: 2},
18 | canActivate: [Authentication],
19 | loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
20 | },
21 | {
22 | path: 'register',
23 | component: Register
24 | },
25 | {
26 | path: 'login',
27 | component: Login
28 | },
29 | {
30 | path: 'forget',
31 | component: ForgetPass
32 | },
33 | {
34 | path: 'reset-pass',
35 | component: ResetPass
36 | },
37 | {
38 | path: 'error',
39 | component: ErrorComponent
40 | },
41 | {
42 | path: 'email-confirm',
43 | canActivate: [Authentication],
44 | component: EmailConfirm
45 | },
46 | ...staticContentRoutes
47 | ];
48 |
--------------------------------------------------------------------------------
/src/app/browser-extension/browser-extension.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ChromeExtensionService } from './chrome-extension.service';
3 | import { ExtensionRpcService } from './extension-rpc.service';
4 |
5 | @NgModule({
6 | providers: [ExtensionRpcService, ChromeExtensionService]
7 | })
8 | export class BrowserExtensionModule {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/confirm-dialog/confirm-dialog-modal.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input} from '@angular/core';
2 | import {UIDialogRef} from 'deneb-ui';
3 | @Component({
4 | selector: 'confirm-dialog-modal',
5 | templateUrl: './confirm-dialog-modal.html',
6 | styles: [`
7 | .ui.modal.active {
8 | transform: translate3d(0, -50%, 0);
9 | }
10 | `]
11 | })
12 | export class ConfirmDialogModal {
13 |
14 | @Input()
15 | title;
16 |
17 | @Input()
18 | content;
19 |
20 | constructor(private _dialogRef: UIDialogRef) {}
21 |
22 | cancel() {
23 | this._dialogRef.close('cancel');
24 | }
25 |
26 | confirm() {
27 | this._dialogRef.close('confirm');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/confirm-dialog/confirm-dialog-modal.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 | No
13 |
14 |
15 |
16 | Yes
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/app/confirm-dialog/confirm-dialog.directive.ts:
--------------------------------------------------------------------------------
1 | import {Directive, EventEmitter, HostListener, Input, OnDestroy, Output} from '@angular/core';
2 | import {UIDialog} from 'deneb-ui';
3 | import {ConfirmDialogModal} from './confirm-dialog-modal.component';
4 | import {Subscription} from 'rxjs';
5 |
6 | @Directive({
7 | selector: '[confirmDialog]'
8 | })
9 | export class ConfirmDialogDirective implements OnDestroy {
10 |
11 | private _subscription = new Subscription();
12 |
13 | @Input()
14 | dialogTitle: string;
15 |
16 | @Input()
17 | dialogContent: string;
18 |
19 | @Output()
20 | onConfirm = new EventEmitter();
21 |
22 | @Output()
23 | onCancel = new EventEmitter();
24 |
25 | constructor(private _dialog: UIDialog) {}
26 |
27 | @HostListener('click', ['$event'])
28 | onClickHandler($event: MouseEvent) {
29 | $event.preventDefault();
30 |
31 | let _dialogRef = this._dialog.open(ConfirmDialogModal, {stickyDialog: true, backdrop: true});
32 | _dialogRef.componentInstance.title = this.dialogTitle;
33 | _dialogRef.componentInstance.content = this.dialogContent;
34 | this._subscription.add(
35 | _dialogRef.afterClosed()
36 | .subscribe(
37 | (result: string) => {
38 | if (result === 'confirm') {
39 | this.onConfirm.emit('confirm');
40 | } else {
41 | this.onCancel.emit('cancel');
42 | }
43 | }
44 | )
45 | );
46 | }
47 |
48 | ngOnDestroy(): void {
49 | this._subscription.unsubscribe();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/confirm-dialog/index.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {ConfirmDialogDirective} from './confirm-dialog.directive';
3 | import {UIDialogModule} from 'deneb-ui';
4 | import {ConfirmDialogModal} from './confirm-dialog-modal.component';
5 |
6 | @NgModule({
7 | declarations: [ConfirmDialogDirective, ConfirmDialogModal],
8 | imports: [UIDialogModule],
9 | exports: [ConfirmDialogDirective],
10 | entryComponents: [ConfirmDialogModal]
11 | })
12 | export class ConfirmDialogModule {
13 |
14 | }
15 |
16 | export * from './confirm-dialog.directive';
17 |
--------------------------------------------------------------------------------
/src/app/email-confirm/email-confirm.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { Subscription } from 'rxjs';
4 | import { mergeMap } from 'rxjs/operators';
5 | import { UserService } from '../user-service/user.service';
6 | import { EmailConfirmService } from './email-confirm.service';
7 |
8 | @Component({
9 | selector: 'email-confirm',
10 | templateUrl: './email-confirm.html',
11 | styleUrls: ['./email-confirm.less'],
12 | encapsulation: ViewEncapsulation.None
13 | })
14 | export class EmailConfirm implements OnInit, OnDestroy {
15 | private _subscription = new Subscription();
16 |
17 | isLoading = true;
18 |
19 | emailValid = false;
20 |
21 | constructor(private _confirmService: EmailConfirmService,
22 | private _userService: UserService,
23 | private _router: Router) {
24 | }
25 |
26 | ngOnInit(): void {
27 | this.isLoading = true;
28 | let searchString = window.location.search;
29 | if (searchString) {
30 | this._subscription.add(
31 | this._confirmService.confirmEmail(searchString.substring(1, searchString.length)).pipe(
32 | mergeMap(() => {
33 | return this._userService.getUserInfo();
34 | }))
35 | .subscribe(() => {
36 | this.isLoading = false;
37 | this.emailValid = true;
38 | }, () => {
39 | this.isLoading = false;
40 | this.emailValid = false;
41 | })
42 | );
43 | } else {
44 | this.isLoading = false;
45 | this.emailValid = false;
46 | }
47 | }
48 |
49 | ngOnDestroy(): void {
50 | this._subscription.unsubscribe();
51 | }
52 |
53 | returnToHome() {
54 | this._router.navigateByUrl('/');
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/email-confirm/email-confirm.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Just wait a little moment...
4 |
5 |
6 |
Congratulations! You have completed email address confirmation.
7 |
8 |
9 |
10 |
Fail to confirm email!
11 |
12 |
--------------------------------------------------------------------------------
/src/app/email-confirm/email-confirm.less:
--------------------------------------------------------------------------------
1 | :host {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | bottom: 0;
6 | right: 0;
7 | display: block;
8 | box-sizing: border-box;
9 | background-color: #f0f0f0;
10 | }
11 |
12 | .email-confirm {
13 | width: 700px;
14 | height: 200px;
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | right: 0;
19 | bottom: 0;
20 | margin: auto;
21 | background-color: #ffffff;
22 | border: 1px solid #ccc;
23 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.2);
24 | text-align: center;
25 | padding: 2rem;
26 | p {
27 | font-size: 1.2rem;
28 | }
29 | .confirmed {
30 | .ui.button {
31 | margin-top: 2rem;
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/app/email-confirm/email-confirm.module.ts:
--------------------------------------------------------------------------------
1 | import { HttpClientModule } from '@angular/common/http';
2 | import { NgModule } from '@angular/core';
3 | import { CommonModule } from '@angular/common';
4 | import { EmailConfirm } from './email-confirm.component';
5 | import { EmailConfirmService } from "./email-confirm.service";
6 |
7 | @NgModule({
8 | declarations: [EmailConfirm],
9 | imports: [CommonModule, HttpClientModule],
10 | providers: [EmailConfirmService]
11 | })
12 | export class EmailConfirmModule {
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/email-confirm/email-confirm.service.ts:
--------------------------------------------------------------------------------
1 | import { HttpClient } from '@angular/common/http';
2 | import { Injectable } from '@angular/core';
3 | import { Observable } from 'rxjs';
4 | import { catchError, map } from 'rxjs/operators';
5 | import { BaseService } from '../../helpers/base.service';
6 |
7 | @Injectable()
8 | export class EmailConfirmService extends BaseService {
9 | constructor(private _http: HttpClient) {
10 | super()
11 | }
12 |
13 | confirmEmail(querystring: string): Observable {
14 | // URLSearchParams is a WHATWG spec
15 | let params = new URLSearchParams(querystring);
16 | let token = params.get('token');
17 | return this._http.post('/api/user/email/confirm', {token: token}).pipe(
18 | map(res => res.json()),
19 | catchError(this.handleError),);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/entity/announce.ts:
--------------------------------------------------------------------------------
1 | import { Bangumi } from './bangumi';
2 |
3 | export class Announce {
4 | id?: string;
5 | content: string;
6 | bangumi?: Bangumi;
7 | image_url?: string;
8 | position: number;
9 | sort_order: number;
10 | start_time: number;
11 | end_time: number;
12 |
13 | static POSITION_BANNER = 1;
14 | static POSITION_BANGUMI = 2;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/entity/bangumi-raw.ts:
--------------------------------------------------------------------------------
1 | import {Bangumi} from "./bangumi";
2 | import {Episode} from "./episode";
3 | export class BangumiRaw extends Bangumi{
4 | public episodes: Episode[];
5 | constructor(rawData: any) {
6 | super();
7 |
8 | this.bgm_id = rawData.id;
9 | this.name = rawData.name;
10 | this.name_cn = rawData.name_cn;
11 | this.type = rawData.type;
12 | this.summary = rawData.summary;
13 | this.image = rawData.images.large;
14 | this.air_date = rawData.air_date;
15 | this.air_weekday = rawData.air_weekday;
16 |
17 |
18 | if(Array.isArray(rawData.eps) && rawData.eps.length > 0) {
19 | this.episodes = rawData.eps.filter(item => item.type === Episode.EPISODE_TYPE_NORMAL).map(item => Episode.fromRawData(item, item.sort));
20 | this.eps = this.episodes.length;
21 | } else {
22 | this.episodes = [];
23 | this.eps = 0;
24 | }
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/entity/constants.ts:
--------------------------------------------------------------------------------
1 | import {Bangumi} from './bangumi';
2 |
3 | export const FAVORITE_LABEL = {
4 | 1: '想看',
5 | 2: '看过',
6 | 3: '在看',
7 | 4: '搁置',
8 | 5: '抛弃'
9 | };
10 |
11 | export const BANGUMI_TYPE = {
12 | 2: '动画',
13 | 6: '日剧'
14 | };
15 |
--------------------------------------------------------------------------------
/src/app/entity/episode.ts:
--------------------------------------------------------------------------------
1 | import {Bangumi} from './bangumi';
2 | import {WatchProgress} from './watch-progress';
3 | import {VideoFile} from './video-file';
4 | import { Image } from './image';
5 |
6 | export class Episode {
7 |
8 | static EPISODE_TYPE_NORMAL: number = 0;
9 | static EPISODE_TYPE_SPECIAL: number = 1;
10 |
11 | id: string;
12 | bangumi_id: string;
13 | bgm_eps_id: number;
14 | episode_no: number;
15 | name: string;
16 | name_cn: string;
17 | duration: string;
18 | airdate: string;
19 | status: number;
20 | torrent_id: string;
21 | create_time: number;
22 | update_time: number;
23 | bangumi: Bangumi; // optional
24 | // @deprecated
25 | thumbnail: string; // optional
26 | video_files: VideoFile[]; // optional
27 |
28 | // @Optional
29 | delete_mark: number;
30 | // @Optional
31 | delete_eta: number;
32 |
33 | // optional
34 | watch_progress: WatchProgress;
35 |
36 | // deprecated
37 | thumbnail_color: string;
38 |
39 | thumbnail_image: Image | null;
40 |
41 |
42 | static fromRawData(rawData: any, episode_no?: number) {
43 | let episode = new Episode();
44 | episode.bgm_eps_id = rawData.id;
45 | episode.episode_no = episode_no;
46 | episode.name = rawData.name;
47 | episode.name_cn = rawData.name_cn;
48 | episode.duration = rawData.duration;
49 | episode.airdate = rawData.airdate;
50 | return episode;
51 | }
52 |
53 | static STATUS_NOT_DOWNLOADED = 0;
54 | static STATUS_DOWNLOADING = 1;
55 | static STATUS_DOWNLOADED = 2;
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/entity/image.ts:
--------------------------------------------------------------------------------
1 | export class Image {
2 | url: string;
3 | dominant_color: string;
4 | width: number;
5 | height: number;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/entity/index.ts:
--------------------------------------------------------------------------------
1 | export {Bangumi} from './bangumi';
2 | export {Episode} from './episode';
3 | export {BangumiRaw} from './bangumi-raw';
4 | export {User} from './user';
5 |
--------------------------------------------------------------------------------
/src/app/entity/item-type.ts:
--------------------------------------------------------------------------------
1 | export class ItemType {
2 | id: any;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/entity/item.ts:
--------------------------------------------------------------------------------
1 | import { ItemType } from './item-type';
2 | import { Publisher } from './publisher';
3 | import { Team } from './team';
4 |
5 | export class Item {
6 | id: any;
7 | title: string;
8 | eps_no_list: number[];
9 | type: ItemType;
10 | team?: Team;
11 | timestampe: Date;
12 | uri?: string;
13 | publisher: Publisher;
14 | torrent_url?: string;
15 | magnet_uri?: string;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/entity/publisher.ts:
--------------------------------------------------------------------------------
1 | export class Publisher {
2 | id: any;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/entity/rating.ts:
--------------------------------------------------------------------------------
1 | export class Rating {
2 | count: {[rate: string]: number};
3 | score: number;
4 | total: number;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/entity/team.ts:
--------------------------------------------------------------------------------
1 | export class Team {
2 | id: any;
3 | name: string;
4 | }
5 |
--------------------------------------------------------------------------------
/src/app/entity/user.ts:
--------------------------------------------------------------------------------
1 | export class User {
2 | id: string;
3 | name: string;
4 | password: string;
5 | password_repeat: string;
6 | level: number;
7 | invite_code: string;
8 | remember: boolean;
9 | email: string;
10 | email_confirmed: boolean;
11 |
12 | static LEVEL_DEFAULT = 0;
13 | static LEVEL_USER = 1;
14 | static LEVEL_ADMIN = 2;
15 | static LEVEL_SUPER_USER = 3;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/entity/video-file.ts:
--------------------------------------------------------------------------------
1 | export class VideoFile {
2 | id?: string;
3 | bangumi_id?: string;
4 | episode_id?: string;
5 | file_name?: string;
6 | file_path?: string;
7 | torrent_id?: string;
8 | download_url?: string;
9 | status?: number;
10 |
11 | resolution_w?: number;
12 | resolution_h?: number;
13 | duration?: number;
14 | label?: string;
15 | // optional, only available at end-user api
16 | url: string;
17 |
18 |
19 | static STATUS_DOWNLOAD_PENDING = 1;
20 | static STATUS_DOWNLOADING = 2;
21 | static STATUS_DOWNLOADED = 3;
22 | }
23 |
--------------------------------------------------------------------------------
/src/app/entity/watch-progress.ts:
--------------------------------------------------------------------------------
1 | export class WatchProgress {
2 | id: string;
3 | bangumi_id: string;
4 | episode_id: string;
5 | user_id: string;
6 | watch_status: number;
7 | last_watch_position: number;
8 | last_watch_time: number;
9 | percentage: number;
10 |
11 | static WISH = 1;
12 | static WATCHED = 2;
13 | static WATCHING = 3;
14 | static PAUSE = 4;
15 | static ABANDONED = 5;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/entity/web-hook.ts:
--------------------------------------------------------------------------------
1 | import { User } from './user';
2 |
3 | export class WebHook {
4 | id: string;
5 | name: string;
6 | description: string;
7 | url: string;
8 | status?: number;
9 | consecutive_failure_count?: number;
10 | register_time?: number;
11 | created_by: User;
12 | shared_secret?: string;
13 | permissions: string[];
14 |
15 |
16 | static STATUS_IS_ALIVE = 1;
17 | static STATUS_HAS_ERROR = 2;
18 | static STATUS_IS_DEAD = 3;
19 | static STATUS_INITIAL = 4;
20 |
21 | static PERMISSION_FAVORITE = 'PERM_FAVORITE';
22 | static PERMISSION_EMAIL = 'PERM_EMAIL';
23 | }
24 |
25 | export const PERM_NAME = {
26 | [WebHook.PERMISSION_FAVORITE]: '用户收藏',
27 | [WebHook.PERMISSION_EMAIL]: '用户邮件地址'
28 | };
29 |
--------------------------------------------------------------------------------
/src/app/environment.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationRef, enableProdMode } from '@angular/core';
2 | // Angular 2
3 | import { disableDebugTools, enableDebugTools } from '@angular/platform-browser';
4 | // Environment Providers
5 | let PROVIDERS: any[] = [
6 | // common env directives
7 | ];
8 |
9 | // Angular debug tools in the dev console
10 | // https://github.com/angular/angular/blob/86405345b781a9dc2438c0fbe3e9409245647019/TOOLS_JS.md
11 | let _decorateModuleRef = (value: T): T => {
12 | return value;
13 | };
14 |
15 | if ('production' === ENV) {
16 | enableProdMode();
17 |
18 | // Production
19 | _decorateModuleRef = (modRef: any) => {
20 | disableDebugTools();
21 |
22 | return modRef;
23 | };
24 |
25 | PROVIDERS = [
26 | ...PROVIDERS,
27 | // custom providers in production
28 | ];
29 |
30 | } else {
31 |
32 | _decorateModuleRef = (modRef: any) => {
33 | const appRef = modRef.injector.get(ApplicationRef);
34 | const cmpRef = appRef.components[0];
35 |
36 | let _ng = ( window).ng;
37 | enableDebugTools(cmpRef);
38 | ( window).ng.probe = _ng.probe;
39 | ( window).ng.coreTokens = _ng.coreTokens;
40 | return modRef;
41 | };
42 |
43 | // Development
44 | PROVIDERS = [
45 | ...PROVIDERS,
46 | // custom providers in development
47 | ];
48 |
49 | }
50 |
51 | export const decorateModuleRef = _decorateModuleRef;
52 |
53 | export const ENV_PROVIDERS = [
54 | ...PROVIDERS
55 | ];
56 |
--------------------------------------------------------------------------------
/src/app/error/error.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit, OnDestroy} from '@angular/core';
2 | // import {BaseError} from '../../helpers/error';
3 | import {Title} from '@angular/platform-browser';
4 | import {ActivatedRoute} from '@angular/router';
5 | import {Subscription} from 'rxjs';
6 |
7 |
8 | @Component({
9 | selector: 'error-page',
10 | templateUrl: './error.html'
11 | })
12 | export class ErrorComponent implements OnInit, OnDestroy {
13 |
14 | constructor(titleService: Title, private route: ActivatedRoute) {
15 | titleService.setTitle('出错了!');
16 | }
17 |
18 | errorMessage: string;
19 |
20 | errorStatus: string;
21 |
22 | private routeParamsSubscription: Subscription;
23 |
24 | ngOnInit(): any {
25 | this.routeParamsSubscription = this.route.params.subscribe(
26 | (params) => {
27 | this.errorMessage = params['message'];
28 | this.errorStatus = params['status'];
29 | }
30 | );
31 | return null;
32 | }
33 |
34 | ngOnDestroy(): any {
35 | if(this.routeParamsSubscription) {
36 | this.routeParamsSubscription.unsubscribe();
37 | }
38 | return null;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/error/error.html:
--------------------------------------------------------------------------------
1 | this is an error
2 | {{errorStatus}}
{{errorMessage}}
3 |
--------------------------------------------------------------------------------
/src/app/forget-pass/forget-pass.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/forget-pass/forget-pass.less:
--------------------------------------------------------------------------------
1 | .forget-pass {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | .form-wrapper {
8 | width: 40rem;
9 | height: 12rem;
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | margin: auto;
16 | text-align: center;
17 | }
18 | .email-input {
19 | width: 80%;
20 | }
21 | }
--------------------------------------------------------------------------------
/src/app/forget-pass/forget-pass.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ForgetPass } from './forget-pass.component';
3 | import {CommonModule} from '@angular/common';
4 |
5 | @NgModule({
6 | declarations: [ForgetPass],
7 | imports: [CommonModule]
8 | })
9 | export class ForgetPassModule {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/form-utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './validators';
2 |
--------------------------------------------------------------------------------
/src/app/form-utils/validators.ts:
--------------------------------------------------------------------------------
1 | import {FormGroup} from "@angular/forms";
2 |
3 |
4 | export function passwordMatch(passwordKey:string, passwordConfirmKey:string) {
5 | return (group:FormGroup):{[key:string]:any} => {
6 | let password = group.get(passwordKey);
7 | let passwordConfirm = group.get(passwordConfirmKey);
8 | return password.value !== passwordConfirm.value ? {mismatchedPasswords: true}: null;
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-account-binding/bangumi-account-binding.less:
--------------------------------------------------------------------------------
1 | :host {
2 | background-color: #f0f0f0;
3 | display: block;
4 | box-sizing: border-box;
5 | position: fixed;
6 | top: 3.5rem;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | overflow-x: hidden;
11 | overflow-y: auto;
12 | }
13 | .bangumi-account-binding-container {
14 | width: 700px;
15 | padding-top: 2rem;
16 | padding-bottom: 3rem;
17 |
18 | @media (max-width: 479px) {
19 | width: 100%;
20 | }
21 | @media (min-width: 480px) and (max-width: 767px) {
22 | width: 480px;
23 | }
24 | @media (min-width: 768px) {
25 | width: 700px;
26 | }
27 | }
28 | .bangumi-bound {
29 | display: flex;
30 | flex-direction: row;
31 | justify-content: flex-start;
32 | padding: 1rem 1.5rem;
33 | .avatar {
34 | width: 75px;
35 | height: 75px;
36 | border-radius: 4px;
37 | border: 1px solid #f0f0f0;
38 | > img {
39 | width: 100%;
40 | height: 100%;
41 | border-radius: 4px;
42 | }
43 | }
44 | .info {
45 | margin: 0 1.5rem;
46 | > .nickname {
47 | font-size: 1.2rem;
48 | color: #383838;
49 | font-weight: 600;
50 | padding: 0 0 0.5rem 0;
51 | }
52 | > .username {
53 | font-size: 1.1rem;
54 | color: #606060;
55 | padding: 0 0 0.5rem 0;
56 | }
57 | > .user-space-url {
58 | font-size: 1.1rem;
59 | padding-top: 0.8rem;
60 | }
61 | > .actions {
62 | margin-top: 1.5rem;
63 | }
64 | }
65 | }
66 |
67 | .bangumi-login-status {
68 | height: 4em;
69 | line-height: 2em;
70 | .status-icon {
71 | background: transparent url('/assets/img/rate_emo.gif') no-repeat;
72 | height: 37px;
73 | width: 30px;
74 | display: inline-block;
75 | vertical-align: middle;
76 | margin: 0 1rem;
77 | }
78 |
79 | .status-logon {
80 | background-position: -153px 0;
81 | }
82 |
83 | .status-logout {
84 | background-position: 0;
85 | }
86 | .status-text {
87 | display: inline-block;
88 | margin: 0 1rem 0 0;
89 | }
90 | }
--------------------------------------------------------------------------------
/src/app/home/bangumi-card/image-loading-strategy.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 |
3 | @Injectable()
4 | export class ImageLoadingStrategy {
5 | imageUrl = {};
6 |
7 | hasLoaded(url) {
8 | return !!this.imageUrl[url];
9 | }
10 |
11 | addLoadedUrl(url: string) {
12 | this.imageUrl[url] = true;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-character/bangumi-character.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Bangumi } from '../../../entity';
3 | import { Character } from '../interfaces';
4 |
5 |
6 | @Component({
7 | selector: 'bangumi-character',
8 | templateUrl: './bangumi-character.html',
9 | styleUrls: ['./bangumi-character.less']
10 | })
11 | export class BangumiCharacterComponent {
12 | @Input()
13 | bangumi: Bangumi;
14 | @Input()
15 | characterList: Character[];
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-character/bangumi-character.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
![avatar]()
8 |
9 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-character/bangumi-character.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | }
7 |
8 | .crt-header {
9 | margin-top: 1.8rem;
10 | padding-bottom: 0.4rem;
11 | border-bottom: 1px solid #ccc;
12 | display: block;
13 | font-size: 0.8rem;
14 | color: #adadad;
15 | }
16 |
17 | .chara-grid {
18 | margin-top: 0.4rem;
19 | }
20 |
21 | .character-wrapper {
22 | display: flex;
23 | flex-direction: row;
24 | justify-content: flex-start;
25 | .chara-avatar {
26 | flex: 0 0 auto;
27 | width: 4rem;
28 | height: 4rem;
29 | border-radius: 0.2rem;
30 | border: 1px solid #e0e0e0;
31 | > img {
32 | width: 100%;
33 | height: 100%;
34 | }
35 | }
36 | .chara-info {
37 | flex: 1 1 auto;
38 | margin-left: 0.5rem;
39 | > .main-info {
40 | padding-bottom: 0.2rem;
41 | border-bottom: 1px solid #d6d6d6;
42 | }
43 | > .extra-info {
44 | margin-top: 0.2rem;
45 | > .name-cn {
46 | font-size: 0.9rem;
47 | color: #606060;
48 | }
49 | }
50 | > .actor-info {
51 | font-size: 0.9rem;
52 | margin-top: 0.2rem;
53 | .actor-key {
54 | color: #474747;
55 | }
56 | .actor-name-link {
57 | color: #5f5f5f;
58 | &:hover,
59 | &:focus {
60 | color: #1e70bf;
61 | }
62 | }
63 | }
64 | }
65 | }
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-staff-info/bangumi-staff-info.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { Staff } from '../interfaces';
3 |
4 | @Component({
5 | selector: 'bangumi-staff-info',
6 | templateUrl: './bangumi-staff-info.html',
7 | styleUrls: ['./bangumi-staff-info.less']
8 | })
9 | export class BangumiStaffInfoComponent implements OnInit {
10 | @Input()
11 | staffList: Staff[];
12 |
13 | staffMap: {[job: string]: Staff[]};
14 |
15 | jobs: string[];
16 |
17 | constructor() {
18 | this.staffMap = {};
19 | this.jobs = [];
20 | }
21 |
22 | ngOnInit(): void {
23 | this.staffList.forEach(staff => {
24 | staff.jobs.forEach(job => {
25 | if (!this.staffMap[job]) {
26 | this.jobs.push(job);
27 | this.staffMap[job] = [];
28 | }
29 | this.staffMap[job].push(staff);
30 | });
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-staff-info/bangumi-staff-info.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/bangumi-staff-info/bangumi-staff-info.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | width: 100%;
4 | margin-top: 2rem;
5 | }
6 | .staff-header {
7 | margin-top: 1.8rem;
8 | padding-bottom: 0.4rem;
9 | border-bottom: 1px solid #ccc;
10 | display: block;
11 | font-size: 0.8rem;
12 | color: #adadad;
13 | }
14 | .staff-info {
15 | width: 100%;
16 | margin-top: 0.4rem;
17 | .staff-item {
18 | margin-bottom: 0.5rem;
19 | }
20 | }
--------------------------------------------------------------------------------
/src/app/home/bangumi-extra-info/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface BgmImage {
2 | large: string;
3 | medium: string;
4 | small: string;
5 | grid: string;
6 | }
7 |
8 | export interface Actor {
9 | id: number;
10 | url: string;
11 | name: string;
12 | images: BgmImage;
13 | }
14 |
15 | export interface Person {
16 | id: number;
17 | url: string;
18 | name: string;
19 | name_cn?: string;
20 | role_name: string;
21 | images: BgmImage | null;
22 | comment: number;
23 | collects: number;
24 | info: {name_cn: string, alias: {en: string}, gender: string};
25 | }
26 |
27 | export interface Character extends Person {
28 | actors: Actor[];
29 | }
30 |
31 | export interface Staff extends Person {
32 | jobs: string[];
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/home/bangumi-list/bangumi-list.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | @Injectable()
4 | export class BangumiListService {
5 | scrollPosition = 0;
6 | sort: string;
7 | type: number;
8 | isMovie: boolean;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/home/bottom-float-banner/bottom-float-banner.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, HostBinding } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'bottom-float-banner',
5 | templateUrl: './bottom-float-banner.html',
6 | styleUrls: ['./bottom-float-banner.less'],
7 | })
8 | export class BottomFloatBannerComponent {
9 |
10 | @HostBinding('class.show')
11 | showBanner = false;
12 |
13 | constructor() {
14 | const ua = navigator.userAgent;
15 | this.showBanner = ua.toLowerCase().indexOf('android') > -1;
16 | }
17 |
18 | closeBanner(event: Event) {
19 | event.preventDefault();
20 | event.stopPropagation();
21 | this.showBanner = false;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/home/bottom-float-banner/bottom-float-banner.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/home/bottom-float-banner/bottom-float-banner.less:
--------------------------------------------------------------------------------
1 | @bannerHeight: 3rem;
2 |
3 | :host {
4 | display: none;
5 | box-sizing: border-box;
6 | position: fixed;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | height: @bannerHeight;
11 | background: #383838;
12 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
13 | z-index: 2000;
14 | &.show {
15 | display: block;
16 | }
17 | .banner {
18 | height: @bannerHeight;
19 | line-height: @bannerHeight;
20 | font-size: 1.2rem;
21 | font-weight: 600;
22 | font-family: "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
23 | padding: 0 @bannerHeight 0 1rem;
24 | .app-link {
25 | .app-logo {
26 | display: inline-block;
27 | height: @bannerHeight - 0.8rem;
28 | width: auto;
29 | border: none;
30 | vertical-align: middle;
31 | margin-right: 1rem;
32 | }
33 | color: #f0f0f0;
34 | }
35 | .close-button {
36 | position: absolute;
37 | display: inline-block;
38 | width: @bannerHeight;
39 | height: @bannerHeight;
40 | top: 0;
41 | right: 0;
42 | font-size: 1.4rem;
43 | color: #f0f0f0;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/app/home/default/default.less:
--------------------------------------------------------------------------------
1 | .on-air-loading-wrapper {
2 | height: 21rem;
3 | position: static !important;
4 | }
5 | .default-container {
6 | padding-top: 2rem;
7 | }
8 |
9 | .recommend-header {
10 | font-size: 1.1rem;
11 | font-weight: bold;
12 | padding: 0.6rem 1rem;
13 | margin: 1rem 0 1.8rem 0;
14 | border-bottom: 1px solid #f0f0f0;
15 | }
16 |
17 | .bangumi-card {
18 | overflow: hidden;
19 | .ui.ribbon.label {
20 | position: absolute;
21 | top: 0.5rem;
22 | left: -1rem;
23 | }
24 | .image-wrapper {
25 | width: 100%;
26 | height: 0;
27 | padding-bottom: 140.5152224824%;
28 | position: relative;
29 | overflow: hidden;
30 | > img {
31 | position: absolute;
32 | top: 0;
33 | left: 0;
34 | object-fit: cover;
35 | width: 100%;
36 | height: 100%;
37 | }
38 | }
39 | }
40 |
41 | .announce-container {
42 | padding: 0 0.2rem;
43 | .announcement {
44 | display: block;
45 | position: relative;
46 | width: 100%;
47 | height: 0;
48 | padding-bottom: 20.43%;
49 | > img {
50 | display: block;
51 | position: absolute;
52 | top: 0;
53 | left: 0;
54 | width: 100%;
55 | height: 100%;
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/app/home/favorite-chooser/conflict-dialog/conflict-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { UIDialogRef } from 'deneb-ui';
3 |
4 | @Component({
5 | selector: 'conflict-dialog',
6 | templateUrl: './conflict-dialog.html',
7 | styleUrls: ['./conflict-dialog.less']
8 | })
9 | export class ConflictDialogComponent {
10 | siteTitle = SITE_TITLE;
11 |
12 | STATUS_TEXT = ['', '想看', '看过', '在看', '搁置', '抛弃'];
13 |
14 | @Input()
15 | bangumiName: string;
16 |
17 | @Input()
18 | siteStatus: number;
19 |
20 | @Input()
21 | bgmStatus: number;
22 |
23 | constructor(private _dialogRef: UIDialogRef) {}
24 |
25 | chooseStatus(which: string) {
26 | this._dialogRef.close(which);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/home/favorite-chooser/conflict-dialog/conflict-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
发现{{siteTitle}}的状态与Bangumi的状态不同,请选择保留何种状态
8 |
9 |
{{siteTitle}}的收藏状态:{{STATUS_TEXT[siteStatus]}}
10 |
Bangumi的收藏状态:{{STATUS_TEXT[bgmStatus]}}
11 |
12 |
13 |
14 |
15 | 使用{{siteTitle}}的状态
16 |
17 |
18 | 使用Bangumi的状态
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/app/home/favorite-chooser/conflict-dialog/conflict-dialog.less:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/app/home/favorite-chooser/conflict-dialog/conflict-dialog.less
--------------------------------------------------------------------------------
/src/app/home/favorite-chooser/favorite-chooser.less:
--------------------------------------------------------------------------------
1 | favorite-chooser {
2 | display: block;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | margin-top: 1.8rem;
7 |
8 | .facade-item {
9 | margin-top: 2.8rem;
10 | padding-top: 0.6rem;
11 | border-top: 1px solid #ccc;
12 | position: relative;
13 | &:before {
14 | display: block;
15 | font-size: 0.8rem;
16 | color: #adadad;
17 | position: absolute;
18 | top: -1.8rem;
19 | left: 0;
20 | }
21 | }
22 | .my-review {
23 | &:before {
24 | content: "我的评价";
25 | }
26 | }
27 |
28 | .sync-disabled-tip {
29 | display: inline-block;
30 | margin-left: 1rem;
31 | color: #da9003
32 | }
33 |
34 | .sync-complete-tip {
35 | color: #6cbb72;
36 | }
37 | }
--------------------------------------------------------------------------------
/src/app/home/favorite-list/favorite-list.less:
--------------------------------------------------------------------------------
1 | :host {
2 | position: fixed;
3 | top: 3.5rem;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | }
8 |
9 | :host-context(.home-content.sidebar-active) {
10 | position: fixed;
11 | top: 3.5rem;
12 | left: 15rem;
13 | right: 0;
14 | bottom: 0;
15 | @media (max-width: 1330px) {
16 | left: 0;
17 | }
18 | }
19 |
20 | .anchor-button {
21 | cursor: pointer;
22 | color: #333333;
23 | &:hover,
24 | &:focus {
25 | color: #4f7ba4
26 | }
27 | }
28 |
29 | .favorite-list-container {
30 | padding-bottom: 1rem;
31 | padding-top: 4rem;
32 | width: 100%;
33 | height: 100%;
34 | background-color: #f0f0f0;
35 | .filter-container {
36 | position: absolute;
37 | top: 0;
38 | left: 0;
39 | width: 100%;
40 | font-size: 1rem;
41 | padding: 1rem 4rem 1rem 0;
42 | text-align: right;
43 | .ui.dropdown {
44 | cursor: pointer;
45 | > .text {
46 | font-weight: normal;
47 | }
48 | &:hover,
49 | &:focus {
50 | color: #4f7ba4
51 | }
52 | }
53 | }
54 | .timeline-container {
55 | width: 100%;
56 | height: 100%;
57 | }
58 | }
59 | .no-result-container {
60 | width: 100%;
61 | height: 100%;
62 | overflow: hidden;
63 | position: relative;
64 | .no-result-tips {
65 | position: absolute;
66 | top: 0;
67 | left: 0;
68 | right: 0;
69 | bottom: 0;
70 | margin: auto;
71 | width: 8em;
72 | height: 2em;
73 | font-size: 4rem;
74 | color: #9f9f9f;
75 | }
76 | }
77 | .loading-container {
78 | width: 100%;
79 | height: 100%;
80 | overflow: hidden;
81 | position: relative;
82 | background-color: #f0f0f0;
83 | .loader {
84 | color: #9f9f9f;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/app/home/index.ts:
--------------------------------------------------------------------------------
1 | export {HomeModule} from './home.module';
2 |
--------------------------------------------------------------------------------
/src/app/home/my-bangumi/my-bangumi.html:
--------------------------------------------------------------------------------
1 |
4 |
13 |
14 | 还没有{{favoriteLabel[currentStatus]}}的番組
15 |
--------------------------------------------------------------------------------
/src/app/home/my-bangumi/my-bangumi.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | }
5 |
6 | .status-picker {
7 | float: right;
8 | color: rgba(0, 0, 0, 0.87);
9 | font-weight: 500;
10 | }
11 |
12 | .header {
13 | margin: 0;
14 | font-size: 0.9rem;
15 | font-weight: 700;
16 | line-height: 2;
17 | color: #2185d0;
18 | }
19 |
20 | .menu.favorite-list {
21 | margin: .3em -1.14285714em 0;
22 | padding-top: 1rem;
23 | > .item.favorite-item {
24 | font-size: 1rem;
25 | padding: 0.5em 1.14em;
26 | white-space: nowrap;
27 | > .bangumi-name {
28 | display: inline-block;
29 | width: 9em;
30 | color: #696969;
31 | overflow: hidden;
32 | text-overflow: ellipsis;
33 | white-space: nowrap;
34 | }
35 | > .ui.label {
36 | position: absolute;
37 | top: 0.63em;
38 | right: 1.14em;
39 | &.undecorated {
40 | background: transparent;
41 | color: #999999;
42 | }
43 | }
44 | }
45 | }
46 |
47 | .empty-placeholder {
48 | margin: .5em -1.14285714em 0;
49 | font-size: 1rem;
50 | padding: 1.5em 1.14em 0.5em;
51 | color: #4c4c4c;
52 | }
--------------------------------------------------------------------------------
/src/app/home/play-episode/comment/comment-form/comment-form.html:
--------------------------------------------------------------------------------
1 |
2 |
![avatar]()
3 |
4 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/comment/comment-form/comment-form.less:
--------------------------------------------------------------------------------
1 | :host {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: flex-start;
6 | -webkit-box-sizing: border-box;
7 | -moz-box-sizing: border-box;
8 | box-sizing: border-box;
9 | }
10 | .user-avatar {
11 | width: 2.5em;
12 | height: 2.5em;
13 | border-radius: 0.25rem;
14 | margin-right: 1rem;
15 | > img {
16 | width: 100%;
17 | height: 100%;
18 | border-radius: 0.25rem;
19 | }
20 | }
21 | .ui.form {
22 | flex: 1 1 auto;
23 | textarea {
24 | height: 8em;
25 | }
26 | .buttons.field {
27 | display: flex;
28 | flex-direction: row;
29 | justify-content: flex-end;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/app/home/play-episode/comment/edit-comment/edit-comment.html:
--------------------------------------------------------------------------------
1 |
2 |
![avatar]()
3 |
4 |
16 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/comment/edit-comment/edit-comment.less:
--------------------------------------------------------------------------------
1 | :host {
2 | width: 100%;
3 | display: flex;
4 | flex-direction: row;
5 | justify-content: flex-start;
6 | -webkit-box-sizing: border-box;
7 | -moz-box-sizing: border-box;
8 | box-sizing: border-box;
9 | }
10 | .user-avatar {
11 | width: 2.5em;
12 | height: 2.5em;
13 | border-radius: 0.25rem;
14 | margin-right: 1rem;
15 | > img {
16 | width: 100%;
17 | height: 100%;
18 | border-radius: 0.25rem;
19 | }
20 | }
21 | .ui.form {
22 | flex: 1 1 auto;
23 | textarea {
24 | height: 8em;
25 | }
26 | .buttons.field {
27 | display: flex;
28 | flex-direction: row;
29 | justify-content: flex-end;
30 | }
31 | }
--------------------------------------------------------------------------------
/src/app/home/play-episode/feedback/feedback.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input, OnInit } from '@angular/core';
2 | import { UIDialogRef } from 'deneb-ui';
3 | import { FormBuilder, FormGroup } from '@angular/forms';
4 |
5 | @Component({
6 | selector: 'feedback-dialog',
7 | templateUrl: './feedback.html',
8 | styleUrls: ['./feedback.less']
9 | })
10 | export class FeedbackComponent implements OnInit {
11 |
12 | feedbackForm: FormGroup;
13 |
14 | issueList = ['有画面无声音', '有声音无画面', '无声音无画面', '其他'];
15 |
16 | pickedIndex = -1;
17 |
18 | constructor(private _dialogRef: UIDialogRef,
19 | private _fb: FormBuilder) {
20 | }
21 |
22 | pickIssue(index: number) {
23 | this.pickedIndex = index;
24 | }
25 |
26 | submit() {
27 | if (this.pickedIndex === -1) {
28 | return;
29 | }
30 | let desc = this.feedbackForm.value.desc;
31 | if (!desc) {
32 | desc = '无';
33 | }
34 | this._dialogRef.close(`问题:${this.issueList[this.pickedIndex]}, 附加描述: ${desc}`);
35 | }
36 |
37 | cancel() {
38 | this._dialogRef.close(null);
39 | }
40 |
41 | ngOnInit(): void {
42 | this.feedbackForm = this._fb.group({
43 | desc: ['']
44 | });
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/feedback/feedback.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/feedback/feedback.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 639px) and (min-height: 400px) {
6 | width: 90%;
7 | height: 360px;
8 | }
9 | @media(min-width: 640px) and (min-height: 400px) {
10 | width: 600px;
11 | height: 300px;
12 | }
13 | @media(min-height: 400px) and (min-width: 425px) {
14 | height: 300px;
15 | }
16 | @media(max-height: 399px) {
17 | height: 90%;
18 | }
19 | margin: auto;
20 | top: 0;
21 | left: 0;
22 | right: 0;
23 | bottom: 0;
24 | position: absolute;
25 | border-radius: 4px;
26 | background-color: #fff;
27 | overflow: hidden;
28 | -webkit-box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
29 | -moz-box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
30 | box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.3);
31 | }
32 |
33 | .ui.form {
34 | padding: 1rem 1.5rem;
35 |
36 | .ui.questions.buttons {
37 | flex-wrap: wrap;
38 | }
39 |
40 | .footer {
41 | display: flex;
42 | flex-direction: row;
43 | justify-content: flex-end;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/reveal-extra/reveal-extra.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/app/home/play-episode/reveal-extra/reveal-extra.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | margin-top: 1rem;
7 | }
8 |
9 | //.reveal-off {
10 | // text-align: center;
11 | //}
12 |
13 | .reveal-on {
14 | > .buttons {
15 | margin-top: 1rem;
16 | //text-align: center;
17 | }
18 | }
--------------------------------------------------------------------------------
/src/app/home/preview-video/preview-video.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
![]()
11 |
12 |
13 |
16 |
19 |
{{currentPV.name}}
20 |
21 |
22 |
放送开始:{{currentPV.air_date}}
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/app/home/rating/my-review/my-review.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ratingScore}}
5 |
{{ratingText}}
6 |
7 |
8 |
9 | rating, 'half': s > rating && s - rating < 1}">
11 |
12 |
13 |
14 |
26 |
27 |
--------------------------------------------------------------------------------
/src/app/home/rating/rating.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Input } from '@angular/core';
2 | import { Rating } from '../../entity/rating';
3 |
4 | export const RATING_TEXT = ['无评分', '不忍直视', '很差', '差', '较差', '不过不失', '还行', '推荐', '力荐', '神作', '超神作'];
5 | export const RATING_COLOR = [
6 | '#cccccc',
7 | '#a80407',
8 | '#ec4017',
9 | '#df641d',
10 | '#da9003',
11 | '#ffd400',
12 | '#c2e008',
13 | '#65e615',
14 | '#72e7a3',
15 | '#71dddf',
16 | '#04afff'
17 | ];
18 |
19 | @Component({
20 | selector: 'bangumi-rating',
21 | templateUrl: './rating.html',
22 | styleUrls: ['./rating.less']
23 | })
24 | export class RatingComponent {
25 | @Input()
26 | rating: Rating;
27 |
28 | get countDist(): {r: number, c: number}[] {
29 | if (this.rating) {
30 | return Object.keys(this.rating.count)
31 | .map(r => parseInt(r, 10))
32 | .sort((r1, r2) => r2 - r1)
33 | .map(r => {
34 | return {r: r, c: this.rating.count[r]};
35 | });
36 | } else {
37 | return [];
38 | }
39 | }
40 |
41 | get ratingScore(): string {
42 | if (!this.rating) {
43 | return '0.0';
44 | }
45 | if (Math.floor(this.rating.score) === this.rating.score) {
46 | return this.rating.score + '.0';
47 | }
48 | return this.rating.score + '';
49 | }
50 |
51 | get roundedScore(): number {
52 | if (!this.rating) {
53 | return 0;
54 | }
55 | return Math.round(this.rating.score);
56 | }
57 |
58 | get ratingText(): string {
59 | if (!this.rating) {
60 | return RATING_TEXT[0];
61 | }
62 | return RATING_TEXT[this.roundedScore];
63 | }
64 |
65 | get ratingColor(): string {
66 | if (!this.rating) {
67 | return RATING_COLOR[0];
68 | }
69 | return RATING_COLOR[this.roundedScore];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/home/rating/rating.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{rating.score}}
5 |
{{ratingText}}
6 |
7 |
8 |
9 | rating.score, 'half': s > rating.score && s - rating.score < 1}">
11 |
12 |
13 |
14 | {{rating.total}}人评分
15 |
16 |
17 |
18 |
19 |
20 |
{{c.r}}
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/home/user-action/browser-extension-tip/browser-extension-tip.html:
--------------------------------------------------------------------------------
1 |
2 |
Bangumi整合功能
3 |
4 | 现在你可以通过安装{{browserType}}扩展的方式绑定Bangumi帐号,
5 | 这项增强的功能提供了评分到评论的一系列基于Bangumi帐号的社交服务。
6 |
前往{{browserType}}商店
7 |
8 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/home/user-action/browser-extension-tip/browser-extension-tip.less:
--------------------------------------------------------------------------------
1 | @arrowOffset: 0.30714286em;
2 |
3 | @arrowStroke: 1px;
4 | @arrowColor: #bababc;
5 |
6 | @arrowBoxShadow: @arrowStroke @arrowStroke 0px 0px @arrowColor;
7 | @leftArrowBoxShadow: @arrowStroke -@arrowStroke 0px 0px @arrowColor;
8 | @rightArrowBoxShadow: -@arrowStroke @arrowStroke 0px 0px @arrowColor;
9 | @bottomArrowBoxShadow: -@arrowStroke -@arrowStroke 0px 0px @arrowColor;
10 |
11 |
12 | :host {
13 | display: block;
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | -webkit-box-sizing: border-box;
18 | -moz-box-sizing: border-box;
19 | box-sizing: border-box;
20 | z-index: 1900;
21 | }
22 |
23 | .ui-basic-popover {
24 | position: relative;
25 | border: 1px solid #d6d6d6;
26 | border-radius: 4px;
27 | background: #ffffff;
28 | margin: 0.5rem;
29 | -webkit-box-shadow: 0 2px 4px 0 rgba(34,36,38,.12), 0 2px 10px 0 rgba(34,36,38,.15);
30 | -moz-box-shadow: 0 2px 4px 0 rgba(34,36,38,.12), 0 2px 10px 0 rgba(34,36,38,.15);
31 | box-shadow: 0 2px 4px 0 rgba(34,36,38,.12), 0 2px 10px 0 rgba(34,36,38,.15);
32 | .popover-title {
33 | padding: 0.6rem 0.8rem 0.2rem 0.8rem;
34 | font-weight: bold;
35 | font-size: 1rem;
36 | }
37 | .popover-content {
38 | padding: 0.5rem 0.8rem 0.6rem 0.8rem;
39 | font-size: 1rem;
40 | }
41 |
42 | &:before {
43 | top: -@arrowOffset;
44 | right: 1em;
45 | bottom: auto;
46 | left: auto;
47 | margin-left: 0em;
48 | box-shadow: @bottomArrowBoxShadow;
49 | background-color: #ffffff;
50 | position: absolute;
51 | content: ' ';
52 | width: .71428571em;
53 | height: .71428571em;
54 | -webkit-transform: rotate(45deg);
55 | -moz-transform: rotate(45deg);
56 | -ms-transform: rotate(45deg);
57 | -o-transform: rotate(45deg);
58 | transform: rotate(45deg);
59 | z-index: 2;
60 | }
61 | .popover-footer {
62 | text-align: right;
63 | padding: 0 0.5rem 0.5rem 0;
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/home/user-action/user-action-panel/user-action-panel.component.ts:
--------------------------------------------------------------------------------
1 |
2 | import {fromEvent as observableFromEvent, Subscription , Observable } from 'rxjs';
3 |
4 | import {skip} from 'rxjs/operators';
5 | import { UIPopoverContent, UIPopoverRef } from 'deneb-ui';
6 | import { Component, Input, OnDestroy } from '@angular/core';
7 | import { User } from '../../../entity';
8 |
9 | @Component({
10 | selector: 'user-action-panel',
11 | templateUrl: './user-action-panel.html',
12 | styleUrls: ['./user-action-panel.less']
13 | })
14 | export class UserActionPanelComponent extends UIPopoverContent implements OnDestroy {
15 | private _subscription = new Subscription();
16 |
17 | @Input()
18 | user: User;
19 |
20 | @Input()
21 | isBangumiEnabled: boolean;
22 |
23 | @Input()
24 | bgmAccountInfo: {
25 | nickname: string,
26 | avatar: {large: string, medium: string, small: string},
27 | username: string,
28 | id: string,
29 | url: string
30 | };
31 |
32 | constructor(popoverRef: UIPopoverRef) {
33 | super(popoverRef);
34 | }
35 |
36 | logout(event: Event) {
37 | event.preventDefault();
38 | event.stopPropagation();
39 | this.popoverRef.close('logout');
40 | }
41 |
42 | ngAfterViewInit() {
43 | super.ngAfterViewInit();
44 | this._subscription.add(
45 | observableFromEvent(document.body, 'click').pipe(
46 | skip(1))
47 | .subscribe(() => {
48 | this.popoverRef.close(null);
49 | })
50 | );
51 | }
52 |
53 | ngOnDestroy() {
54 | this._subscription.unsubscribe();
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/home/user-action/user-action-panel/user-action-panel.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/src/app/home/user-action/user-action.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
![bangumi-avatar]()
4 | {{user?.name}}
5 |
({{bgmAccountInfo.nickname}})
6 |
(未关联Bangumi)
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/app/home/user-action/user-action.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | -webkit-box-sizing: border-box;
4 | -moz-box-sizing: border-box;
5 | box-sizing: border-box;
6 | }
7 |
8 | .user-action {
9 | margin-right: 2rem;
10 | @media (max-width: 639px) {
11 | margin-right: 1rem;
12 | }
13 | cursor: pointer;
14 | .text {
15 | font-weight: 700;
16 | display: inline-block;
17 | -webkit-transition: none;
18 | -moz-transition: none;
19 | -ms-transition: none;
20 | -o-transition: none;
21 | transition: none;
22 | > img {
23 | display: inline-block;
24 | vertical-align: top;
25 | width: 2em;
26 | height: 2em;
27 | margin: -.4em .78571429rem -.4em 0;
28 | float: none;
29 | max-height: 2em;
30 | border-radius: 4px;
31 | border: 1px solid #dfdfdf;
32 | }
33 | }
34 | .dropdown.icon {
35 | margin: 0 0.5em 0 0.21428571em;
36 | vertical-align: baseline;
37 | font-family: Dropdown;
38 | width: auto;
39 | height: 1em;
40 | line-height: 1;
41 | display: inline-block;
42 | font-weight: 400;
43 | font-style: normal;
44 | text-align: center;
45 | position: relative;
46 | font-size: 0.85714286em;
47 | opacity: 1;
48 | speak: none;
49 | }
50 | }
--------------------------------------------------------------------------------
/src/app/home/user-center/user-center.less:
--------------------------------------------------------------------------------
1 | :host {
2 | background-color: #f0f0f0;
3 | display: block;
4 | box-sizing: border-box;
5 | position: fixed;
6 | top: 3.5rem;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | overflow-x: hidden;
11 | overflow-y: auto;
12 | }
13 | .user-center-container {
14 | width: 700px;
15 | padding-top: 2rem;
16 | padding-bottom: 3rem;
17 |
18 | @media (max-width: 479px) {
19 | width: 100%;
20 | }
21 | @media (min-width: 480px) and (max-width: 767px) {
22 | width: 480px;
23 | }
24 | @media (min-width: 768px) {
25 | width: 700px;
26 | }
27 |
28 | .ui.form {
29 | .inline.field {
30 | > label {
31 | width: 8em;
32 | text-align: right;
33 | }
34 | .warning.text {
35 | display: inline-block;
36 | margin-left: 1em;
37 | color: #573a08;
38 | }
39 | > .ui.button {
40 | margin-left: 1em;
41 | }
42 | }
43 | .ui.button {
44 | margin-left: 8em;
45 | }
46 | .inline.field + .ui.error.message {
47 | margin-left: 8.4em;
48 | }
49 | }
50 | }
51 |
52 |
53 | .web-hook-list {
54 | .name {
55 | font-size: 1.2rem;
56 | padding: 0.4rem 1rem 0.8rem 0;
57 | border-bottom: 1px solid #cccccc;
58 | margin-bottom: 0.6rem;
59 | }
60 | .desc {
61 | margin-top: 1rem;
62 | color: #4c4c4c;
63 | }
64 | .permission-tags {
65 | padding: 0.5rem 0;
66 | > .key {
67 | font-size: 0.9rem;
68 | padding-right: 1rem;
69 | }
70 | }
71 | }
72 |
73 | .action-row {
74 | margin-top: 2rem;
75 | }
--------------------------------------------------------------------------------
/src/app/home/web-hook/web-hook.html:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
可用的Web Hook
9 |
10 | 下面列出了{{siteTitle}}的所有可用的Web Hook。如果你想开发Web Hook并在本站使用,请联系管理员注册Web Hook,开发之前请参考
11 |
Web Hook Guide
12 |
13 |
14 |
15 |
{{webHook.name}}
16 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/app/home/web-hook/web-hook.less:
--------------------------------------------------------------------------------
1 | :host {
2 | background-color: #f0f0f0;
3 | display: block;
4 | box-sizing: border-box;
5 | position: fixed;
6 | top: 3.5rem;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | overflow-x: hidden;
11 | overflow-y: auto;
12 | }
13 |
14 | .web-hook-list-container {
15 | width: 700px;
16 | padding-top: 2rem;
17 | padding-bottom: 3rem;
18 |
19 | @media (max-width: 479px) {
20 | width: 100%;
21 | }
22 | @media (min-width: 480px) and (max-width: 767px) {
23 | width: 480px;
24 | }
25 | @media (min-width: 768px) {
26 | width: 700px;
27 | }
28 |
29 | }
30 |
31 |
32 | .web-hook-list {
33 | .name {
34 | font-size: 1.2rem;
35 | padding: 0.4rem 1rem 0.8rem 0;
36 | border-bottom: 1px solid #cccccc;
37 | margin-bottom: 0.6rem;
38 | }
39 | .desc {
40 | margin-top: 1rem;
41 | color: #4c4c4c;
42 | }
43 | .permission-tags {
44 | padding: 0.5rem 0;
45 | > .key {
46 | font-size: 0.9rem;
47 | padding-right: 1rem;
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/src/app/index.ts:
--------------------------------------------------------------------------------
1 | // APP
2 | export * from './app.module';
3 |
--------------------------------------------------------------------------------
/src/app/login/login.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
28 |
31 |
34 |
35 |
{{errorMessage}}
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/app/login/login.less:
--------------------------------------------------------------------------------
1 | @import '../_center-box.less';
2 |
3 | .forget-link {
4 | margin-top: 20px;
5 | margin-bottom: 15px;
6 | }
7 |
8 | .additional-info {
9 | text-align: center;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/pipes/index.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {UserLevelNamePipe} from './user-level-name.pipe';
3 | import {WeekdayPipe} from './weekday.pipe';
4 |
5 | export const PIPES = [
6 | UserLevelNamePipe,
7 | WeekdayPipe
8 | ];
9 |
10 | @NgModule({
11 | declarations: PIPES,
12 | exports: PIPES
13 | })
14 | export class DenebCommonPipes {
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/pipes/user-level-name.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | export const LEVEL_NAMES = ['默认等级Level 0', '用户Level 1', '管理员Level 2', '超级管理员Level 3'];
4 |
5 | @Pipe({name: 'userLevelName'})
6 | export class UserLevelNamePipe implements PipeTransform {
7 |
8 | transform(level: number): any {
9 | return LEVEL_NAMES[level] || '未知';
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/pipes/weekday.pipe.ts:
--------------------------------------------------------------------------------
1 | import {PipeTransform, Pipe} from "@angular/core";
2 |
3 | @Pipe({name: 'weekday'})
4 | export class WeekdayPipe implements PipeTransform {
5 |
6 | weekday_cn = ['', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'];
7 |
8 | transform(value:any, ...args):any {
9 | if(Number.isInteger(value) && value < this.weekday_cn.length) {
10 | return this.weekday_cn[value];
11 | } else {
12 | return '';
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/register/register.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/register/register.less:
--------------------------------------------------------------------------------
1 | @import '../_center-box.less';
2 |
3 | .login-link {
4 | margin-top: 30px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/reset-pass/reset-pass.less:
--------------------------------------------------------------------------------
1 | .reset-pass {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 |
8 | .reset-pass-wrapper {
9 | width: 30rem;
10 | height: 22rem;
11 | position: absolute;
12 | top: 0;
13 | left: 0;
14 | right: 0;
15 | bottom: 0;
16 | margin: auto;
17 | padding: 0.8rem;
18 | .reset-header {
19 | text-align: center;
20 | }
21 | .ui.form {
22 | margin: 1rem 0 2rem 0;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/app/reset-pass/reset-pass.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ResetPass } from './reset-pass.component';
3 | import { ReactiveFormsModule } from '@angular/forms';
4 | import { CommonModule } from '@angular/common';
5 |
6 | @NgModule({
7 | declarations: [ResetPass],
8 | imports: [ReactiveFormsModule, CommonModule]
9 | })
10 | export class ResetPassModule {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/responsive-image/responsive-image.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { ResponsiveImage } from './responsive-image.directive';
3 | import { ResponsiveService } from './responsive.service';
4 | import { ResponsiveImageWrapper } from './responsive-image-wrapper';
5 |
6 | @NgModule({
7 | declarations: [ResponsiveImage, ResponsiveImageWrapper],
8 | providers: [ResponsiveService],
9 | exports: [ResponsiveImage, ResponsiveImageWrapper]
10 | })
11 | export class ResponsiveImageModule {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/responsive-image/responsive.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 |
3 | export interface ObservableStub {
4 | target: Element;
5 | callback(rect: ClientRect): void;
6 | unobserveOnVisible: boolean;
7 | }
8 |
9 | @Injectable()
10 | export class ResponsiveService {
11 | private _observer: IntersectionObserver;
12 |
13 | private _observableStubList: ObservableStub[] = [];
14 |
15 | constructor() {
16 | this._observer = new IntersectionObserver(this.intersectionCallback.bind(this));
17 | }
18 |
19 | intersectionCallback(entries: IntersectionObserverEntry[]) {
20 | entries.filter(entry => {
21 | return entry['isIntersecting']; // current lib.es6.d.ts not updated.
22 | }).forEach((entry: IntersectionObserverEntry) => {
23 | let stub = this.getStub(entry.target);
24 | if (stub) {
25 | stub.callback(entry.boundingClientRect);
26 | if (stub.unobserveOnVisible) {
27 | this.unobserve(stub);
28 | }
29 | }
30 | });
31 | }
32 |
33 | observe(stub: ObservableStub) {
34 | if (this.getStub(stub.target)) {
35 | throw new Error('Duplicate ObservableStub on target');
36 | }
37 | this._observableStubList.push(stub);
38 | this._observer.observe(stub.target);
39 | }
40 |
41 | unobserve(stub: ObservableStub) {
42 | let index = this._observableStubList.findIndex(obStub => obStub == stub);
43 | if (index !== -1) {
44 | this._observableStubList.splice(index, 1);
45 | this._observer.unobserve(stub.target);
46 | }
47 | }
48 |
49 | private getStub(target: Element) {
50 | if (!this._observableStubList) {
51 | return null;
52 | }
53 | return this._observableStubList.find(stub => stub.target === target);
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/app/static-content/apps/apps.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 | import { ActivatedRoute } from '@angular/router';
3 | import { Subscription } from 'rxjs';
4 | import { Title } from '@angular/platform-browser';
5 |
6 | @Component({
7 | selector: 'apps-guide',
8 | templateUrl: './apps.html',
9 | styleUrls: ['./apps.less']
10 | })
11 | export class AppsComponent implements OnInit, OnDestroy {
12 | private _subscription = new Subscription();
13 |
14 | siteTitle = SITE_TITLE;
15 | chromeExtensionId = CHROME_EXTENSION_ID;
16 |
17 | showAndroid: boolean;
18 |
19 | expanded1 = false;
20 |
21 | expanded2 = false;
22 |
23 | constructor(private _route: ActivatedRoute,
24 | titleService: Title) {
25 | titleService.setTitle(`Apps - ${SITE_TITLE}`);
26 | }
27 |
28 | ngOnInit(): void {
29 | this._subscription.add(
30 | this._route.params
31 | .subscribe(params => {
32 | console.log(params);
33 | this.showAndroid = !!params['android'];
34 | })
35 | );
36 | }
37 |
38 | ngOnDestroy(): void {
39 | this._subscription.unsubscribe();
40 | }
41 |
42 | expand1(): void {
43 | this.expanded1 = true;
44 | }
45 |
46 | expand2(): void {
47 | this.expanded2 = true;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/app/static-content/developers/developers.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'developers',
5 | templateUrl: './developers.html'
6 | })
7 | export class DevelopersComponent {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/static-content/developers/developers.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/static-content/privacy/privacy.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'privacy',
5 | templateUrl: './privacy.html'
6 | })
7 | export class PrivacyComponent {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/static-content/privacy/privacy.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | -
5 | 当你访问Web版客户端时,我们使用Google Analytics收集必要的信息用于统计用户使用状况,这些信息由Google收集与储存,本站并不直接存储这些信息。
6 | 所有的这些信息都是匿名的,我们不会将帐号信息提供给Google。
7 |
8 | -
9 | 为了更好的提供服务,本站保存了您的用户名,密码,邮箱地址。其中密码经过加盐hash加密存储的,确保任何人(包括我们)不会知道泄露您的原始密码。
10 | 此外本站还会自动保存您的观看记录和进度。
11 |
12 | -
13 | 所有这些信息收集政策都仅限Web客户端,如果您使用的是第三方客户端,您需要检查第三方客户端的信息收集政策。
14 |
15 |
16 |
17 |
18 | -
19 | 我们仅使用Google Analytics收集匿名信息,但我们并不会与Google分享任何您的帐号相关信息。
20 |
21 | -
22 | 当您使用第三方客户端时,请检查第三方客户端的隐私政策来了解第三方客户端是否会与其他机构或个人分享信息。
23 |
24 |
25 |
26 |
27 | -
28 | 我们的隐私政策可能会随时变化,当隐私政策发生变化时,我们会尽可能通知您,但您依然有义务随时检查隐私政策。只要您继续使用我们的服务即意味着您接受我们的隐私政策。
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/app/static-content/static-content.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'static-content',
5 | templateUrl: './static-content.html',
6 | styleUrls: ['./static-content.less']
7 | })
8 | export class StaticContentComponent {
9 | siteTitle: string = SITE_TITLE;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/static-content/static-content.html:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/static-content/static-content.less:
--------------------------------------------------------------------------------
1 | @navbarHeight: 3.5rem;
2 | @footerHeight: 8em;
3 |
4 | :host {
5 | display: block;
6 | height: 100%;
7 | padding-top: @navbarHeight + 1.5rem;
8 | }
9 |
10 | .navbar {
11 | width: 100%;
12 | height: @navbarHeight;
13 | background-color: #fff;
14 | display: flex;
15 | flex-direction: row;
16 | justify-content: flex-start;
17 | border-bottom: 1px solid #ccc;
18 | align-items: center;
19 | position: fixed;
20 | top: 0;
21 | left: 0;
22 | z-index: 200;
23 |
24 | .brand {
25 | display: block;
26 | padding: 0 1rem 0 2.5rem;
27 | color: #101010;
28 | }
29 | }
--------------------------------------------------------------------------------
/src/app/static-content/static-content.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { PrivacyComponent } from './privacy/privacy.component';
3 | import { TosComponent } from './tos/tos.component';
4 | import { StaticContentComponent } from './static-content.component';
5 | import { RouterModule } from '@angular/router';
6 | import { staticContentRoutes } from './static-content.routes';
7 | import { DevelopersComponent } from './developers/developers.component';
8 | import { AppsComponent } from './apps/apps.component';
9 | import { CommonModule } from '@angular/common';
10 |
11 | @NgModule({
12 | declarations: [
13 | PrivacyComponent,
14 | TosComponent,
15 | StaticContentComponent,
16 | DevelopersComponent,
17 | AppsComponent
18 | ],
19 | imports: [
20 | RouterModule.forChild(staticContentRoutes),
21 | CommonModule
22 | ]
23 | })
24 | export class StaticContentModule {
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/static-content/static-content.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { TosComponent } from './tos/tos.component';
3 | import { StaticContentComponent } from './static-content.component';
4 | import { PrivacyComponent } from './privacy/privacy.component';
5 | import { DevelopersComponent } from './developers/developers.component';
6 | import { AppsComponent } from './apps/apps.component';
7 |
8 | export const staticContentRoutes: Routes = [
9 | {
10 | path: 'about',
11 | component: StaticContentComponent,
12 | children: [
13 | {
14 | path: 'tos',
15 | component: TosComponent
16 | },
17 | {
18 | path: 'privacy',
19 | component: PrivacyComponent
20 | },
21 | {
22 | path: 'developers',
23 | component: DevelopersComponent
24 | },
25 | {
26 | path: 'apps',
27 | component: AppsComponent
28 | }
29 | ]
30 | }
31 | ];
32 |
--------------------------------------------------------------------------------
/src/app/static-content/tos/tos.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'tos',
5 | templateUrl: './tos.html'
6 | })
7 | export class TosComponent {
8 | siteName = SITE_TITLE;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/user-service/index.ts:
--------------------------------------------------------------------------------
1 | import { HttpClientModule } from '@angular/common/http';
2 | import { NgModule } from '@angular/core';
3 | import { UserService } from './user.service';
4 | import { RouterModule } from '@angular/router';
5 | import { Authentication } from './authentication.service';
6 | import { PersistStorage } from './persist-storage';
7 |
8 | @NgModule({
9 | providers: [
10 | UserService,
11 | Authentication,
12 | PersistStorage
13 | ],
14 | imports: [
15 | HttpClientModule,
16 | RouterModule
17 | ]
18 | })
19 | export class UserServiceModule {
20 |
21 | }
22 |
23 | export * from './user.service';
24 | export * from './authentication.service';
25 | export * from './persist-storage';
26 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/capture-button/capture-button.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, HostListener, OnDestroy, OnInit } from '@angular/core';
2 | import { VideoCapture } from '../../core/video-capture.service';
3 | import { VideoPlayer } from '../../video-player.component';
4 | import { Subscription } from 'rxjs';
5 | import { PersistStorage } from '../../../user-service/persist-storage';
6 | import { Capture } from '../../core/settings';
7 |
8 | @Component({
9 | selector: 'video-capture-button',
10 | template: '',
11 | styles: [`
12 | :host {
13 | display: inline-block;
14 | box-sizing: border-box;
15 | flex: 0 0 auto;
16 | margin-left: 0.5rem;
17 | margin-right: 0.5rem;
18 | padding: 0.4rem;
19 | cursor: pointer;
20 | line-height: 1;
21 | }
22 | `]
23 | })
24 | export class VideoCaptureButton implements OnInit, OnDestroy{
25 | private _subscription = new Subscription();
26 |
27 | private _currentTime: number = 0;
28 |
29 | constructor(private _videoCapture: VideoCapture,
30 | private _videoPlayer: VideoPlayer) {
31 | }
32 |
33 | @HostListener('click', ['$event'])
34 | onClick(event: Event) {
35 | event.preventDefault();
36 | event.stopPropagation();
37 | let bangumi_name = this._videoPlayer.bangumiName;
38 | let episode_no = this._videoPlayer.episodeNo;
39 | this._videoCapture.capture(bangumi_name, episode_no, this._currentTime);
40 | }
41 |
42 | ngOnInit(): void {
43 | this._subscription.add(
44 | this._videoPlayer.currentTime
45 | .subscribe(time => this._currentTime = time)
46 | );
47 | }
48 |
49 | ngOnDestroy(): void {
50 | this._subscription.unsubscribe();
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/captured-frame-list/captured-frame-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/captured-frame-list/captured-frame-list.less:
--------------------------------------------------------------------------------
1 | :host {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | height: 10rem;
6 | width: 100%;
7 | background-color: rgba(0, 0, 0, 0.4);
8 | }
9 |
10 | .captured-frame-list-wrapper {
11 | width: 100%;
12 | height: 100%;
13 | padding: 0.6rem 0.3rem;
14 | overflow-y: hidden;
15 | overflow-x: auto;
16 | white-space: nowrap;
17 | user-select: none;
18 | cursor: default;
19 | }
--------------------------------------------------------------------------------
/src/app/video-player/controls/captured-frame-list/operation-dialog/operation-dialog.html:
--------------------------------------------------------------------------------
1 |
6 |
17 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/captured-frame-list/operation-dialog/operation-dialog.less:
--------------------------------------------------------------------------------
1 | :host {
2 | position: absolute;
3 | top: 0;
4 | left: 0;
5 | right: 0;
6 | bottom: 0;
7 | margin: auto;
8 | display: block;
9 | box-sizing: border-box;
10 | text-align: left;
11 | background-color: #000;
12 | overflow: hidden;
13 | padding-bottom: 5rem;
14 |
15 | width: 400px;
16 | height: 320px;
17 |
18 |
19 | @media screen and (max-width: 656px) {
20 | width: 90%;
21 | height: 200px;
22 | }
23 |
24 | @media screen and (min-width: 1320px) and (min-height:870px) {
25 | width: 640px;
26 | height: 460px;
27 | }
28 | }
29 | .image-container {
30 | width: 100%;
31 | height: 0;
32 | padding-bottom: 56.25%;
33 | position: relative;
34 | > .image-wrapper {
35 | position: absolute;
36 | width: 100%;
37 | height: 100%;
38 | margin: 0 -0.3rem;
39 | }
40 | }
41 |
42 | .operation-wrapper {
43 | margin: 0.5rem;
44 | .operation-buttons {
45 | margin: 0;
46 | padding: 0;
47 | &:before,
48 | &:after {
49 | display: table;
50 | content: ' ';
51 | }
52 | > .operation-button {
53 | padding: 0.4rem;
54 | margin: 0.5rem;
55 | display: inline-block;
56 | font-size: 1.2rem;
57 | color: #fff;
58 | cursor: pointer;
59 | &.right {
60 | float: right;
61 | }
62 | }
63 | }
64 | }
65 |
66 | .default-operation {
67 | margin: 0 1.5rem 0.5rem 1.5rem;
68 | color: #fff;
69 | }
--------------------------------------------------------------------------------
/src/app/video-player/controls/config-button/config-panel/config-panel.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/config-button/config-panel/config-panel.less:
--------------------------------------------------------------------------------
1 | @arrowOffset: 0.30714286em;
2 |
3 | @arrowStroke: 1px;
4 | @arrowColor: #000;
5 |
6 | @arrowBoxShadow: @arrowStroke @arrowStroke 0px 0px @arrowColor;
7 | @leftArrowBoxShadow: @arrowStroke -@arrowStroke 0px 0px @arrowColor;
8 | @rightArrowBoxShadow: -@arrowStroke @arrowStroke 0px 0px @arrowColor;
9 | @bottomArrowBoxShadow: -@arrowStroke -@arrowStroke 0px 0px @arrowColor;
10 |
11 |
12 | :host {
13 | display: block;
14 | -webkit-box-sizing: border-box;
15 | -moz-box-sizing: border-box;
16 | box-sizing: border-box;
17 | z-index: 300;
18 | padding: 1rem;
19 | }
20 |
21 | .config-panel-container {
22 | position: relative;
23 | border: 1px solid #000;
24 | border-radius: 4px;
25 | background-color: #000;
26 | color: #fff;
27 | margin: 0.5rem;
28 | -webkit-box-shadow: 0 2px 4px 0 rgba(15,15,15,.12), 0 2px 10px 0 rgba(15, 15, 15, 0.15);
29 | -moz-box-shadow: 0 2px 4px 0 rgba(15,15,15,.12), 0 2px 10px 0 rgba(15,15,15,.15);
30 | box-shadow: 0 2px 4px 0 rgba(15,15,15,.12), 0 2px 10px 0 rgba(15,15,15,.15);
31 | &.ui.form {
32 | padding: 1rem;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/controls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/controls.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 |
9 | &.hide-cursor {
10 | cursor: none;
11 | }
12 | }
13 |
14 | .reflect-state-indicator {
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | right: 0;
19 | bottom: 0;
20 | margin: auto;
21 | width: 2rem;
22 | height: 2rem;
23 | font-size: 2rem;
24 | color: #fff;
25 | line-height: 1;
26 | z-index: 10;
27 | }
28 |
29 | .control-wrapper {
30 | width: 100%;
31 | height: 100%;
32 | display: flex;
33 | flex-direction: column;
34 | justify-content: flex-end;
35 | -webkit-user-select: none;
36 | -moz-user-select: none;
37 | -ms-user-select: none;
38 | user-select: none;
39 | overflow: hidden;
40 | position: relative;
41 | z-index: 100;
42 |
43 | > .control-bar {
44 | display: flex;
45 | flex: 0 0 auto;
46 | width: 100%;
47 | height: 3rem;
48 | flex-direction: column;
49 | justify-content: flex-start;
50 | background-color: rgba(0, 0, 0, 0.4);
51 | > .control-buttons {
52 | width: 100%;
53 | flex: 1 1 auto;
54 | display: flex;
55 | flex-direction: row;
56 | justify-content: flex-start;
57 | align-items: center;
58 | color: #f8f8f8;
59 | .right-aligned-controls {
60 | flex: 1 1 auto;
61 | display: flex;
62 | flex-direction: row;
63 | justify-content: flex-end;
64 | align-items: center;
65 | }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/app/video-player/controls/fullscreen-button/fullscreen-button.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, HostListener, Input} from '@angular/core';
2 | import {VideoPlayer} from '../../video-player.component';
3 | @Component({
4 | selector: 'video-fullscreen-button',
5 | template: ``,
6 | styles: [`
7 | :host {
8 | display: inline-block;
9 | box-sizing: border-box;
10 | flex: 0 0 auto;
11 | margin-left: 0.5rem;
12 | margin-right: 0.5rem;
13 | padding: 0.4rem;
14 | cursor: pointer;
15 | line-height: 1;
16 | }
17 | `]
18 | })
19 | export class VideoFullscreenButton {
20 |
21 | @Input()
22 | controlVisibleState: boolean;
23 |
24 | get isFullscreen() {
25 | if (this._videoPlayer) {
26 | return this._videoPlayer.isFullscreen;
27 | }
28 | return false;
29 | }
30 |
31 | constructor(private _videoPlayer: VideoPlayer) {
32 | }
33 |
34 | @HostListener('click', ['$event'])
35 | onClick(event: Event) {
36 | event.preventDefault();
37 | event.stopPropagation();
38 | if (!this.controlVisibleState) {
39 | return;
40 | }
41 | this._videoPlayer.toggleFullscreen();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/help-button/help-button.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, HostListener } from '@angular/core';
2 | import { VideoPlayer } from '../../video-player.component';
3 |
4 | @Component({
5 | selector: 'video-player-help-button',
6 | template: '',
7 | styles: [`
8 | :host {
9 | display: inline-block;
10 | box-sizing: border-box;
11 | flex: 0 0 auto;
12 | margin-left: 0.5rem;
13 | margin-right: 0.5rem;
14 | padding: 0.4rem;
15 | cursor: pointer;
16 | line-height: 1;
17 | }
18 | `]
19 | })
20 | export class VideoPlayerHelpButton {
21 | constructor(private _videoPlayer: VideoPlayer) {
22 | }
23 |
24 | @HostListener('click', ['$event'])
25 | onClick(event: Event) {
26 | event.preventDefault();
27 | event.stopPropagation();
28 | this._videoPlayer.openHelpDialog();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/scrub-bar/scrub-bar.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | {{pointPosition}}
7 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/time-indicator/time-indicator.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy, OnInit } from '@angular/core';
2 | import { VideoPlayer } from '../../video-player.component';
3 | import { Subscription } from 'rxjs';
4 | import { VideoPlayerHelpers } from '../../core/helpers';
5 |
6 | @Component({
7 | selector: 'video-time-indicator',
8 | template: `
9 | {{currentTimeClock}}
10 | /
11 | {{durationClock}}
12 | `,
13 | styles: [`
14 | :host {
15 | display: inline-block;
16 | box-sizing: border-box;
17 | flex: 0 0 auto;
18 | margin-left: 0.5rem;
19 | margin-right: 0.5rem;
20 | padding: 0.4rem;
21 | line-height: 1;
22 | cursor: default;
23 | font-family: "Segoe UI", sans-serif;
24 | }
25 | `]
26 | })
27 | export class VideoTimeIndicator implements OnInit, OnDestroy {
28 | private _subscription = new Subscription();
29 |
30 | currentTime = Number.NaN;
31 | duration = Number.NaN;
32 |
33 | get durationClock(): string {
34 | if (Number.isNaN(this.duration)) {
35 | return '-';
36 | }
37 | return VideoPlayerHelpers.convertTime(this.duration);
38 | }
39 |
40 | get currentTimeClock() : string {
41 | if (Number.isNaN(this.duration)) {
42 | return '-';
43 | }
44 | return VideoPlayerHelpers.convertTime(this.currentTime);
45 | }
46 |
47 | constructor(private _videoPlayer: VideoPlayer) {
48 | }
49 |
50 | ngOnInit(): void {
51 | this._subscription.add(
52 | this._videoPlayer.currentTime.subscribe(time => this.currentTime = time)
53 | );
54 | this._subscription.add(
55 | this._videoPlayer.duration.subscribe(duration => this.duration = duration)
56 | )
57 | }
58 |
59 | ngOnDestroy(): void {
60 | this._subscription.unsubscribe();
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/volume-control/volume-control.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/app/video-player/controls/volume-control/volume-control.less:
--------------------------------------------------------------------------------
1 | @sliderHandlerRadius: 5px;
2 |
3 | :host {
4 | display: flex;
5 | flex-direction: row;
6 | justify-content: space-between;
7 | align-items: center;
8 | box-sizing: border-box;
9 | flex: 0 0 auto;
10 | margin-left: 0.5rem;
11 | margin-right: 0.5rem;
12 | }
13 |
14 | .mute-button {
15 | display: inline-block;
16 | flex: 0 0 auto;
17 | padding: 0.4rem;
18 | cursor: pointer;
19 | line-height: 1;
20 | > .icon.volume {
21 | position: relative;
22 | }
23 | > .icon.volume.down {
24 | left: -2px;
25 | }
26 | > .icon.volume.off {
27 | left: -3px;
28 | }
29 | }
30 |
31 | .volume-slider-wrapper {
32 | display: inline-block;
33 | flex: 0 0 auto;
34 | position: relative;
35 | width: 4rem;
36 | height: 1rem;
37 | cursor: pointer;
38 | .volume-slider-bg {
39 | width: 100%;
40 | height: 0.3rem;
41 | position: relative;
42 | top: 0.35rem;
43 | background-color: rgba(255, 255, 255, 0.2);
44 | }
45 | .volume-slider-current {
46 | position: absolute;
47 | height: 0.3rem;
48 | top: 0.35rem;
49 | background-color: #f8f8f8;
50 | }
51 | .slider-handler-slot {
52 | display: block;
53 | position: absolute;
54 | top: 0;
55 | left: @sliderHandlerRadius;
56 | bottom: 0;
57 | right: @sliderHandlerRadius;
58 | > .slider-handler {
59 | position: relative;
60 | margin-left: -@sliderHandlerRadius;
61 | margin-top: 0.15rem;
62 | border-radius: @sliderHandlerRadius;
63 | width: 2 * @sliderHandlerRadius;
64 | height: 2 * @sliderHandlerRadius;
65 | background-color: #f8f8f8;
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/video-player/core/settings.ts:
--------------------------------------------------------------------------------
1 | export const PREFIX = 'VideoPlayer';
2 |
3 | export class Capture {
4 | static className = 'Capture';
5 | static prefix = `${PREFIX}:${Capture.className}`;
6 | static AUTO_REMOVE = `${Capture.prefix}:AutoRemove`;
7 | static DIRECT_DOWNLOAD = `${Capture.prefix}:DirectDownload`;
8 | }
9 |
10 | export class PlayList {
11 | static className = 'PlayList';
12 | static prefix = `${PREFIX}:${PlayList.className}`;
13 | static AUTO_PLAY_NEXT = `${PlayList.prefix}:AutoPlayNext`;
14 | }
15 |
16 | export class FloatPlayer {
17 | static className = 'FloatPlayer';
18 | static prefix = `${PREFIX}:${FloatPlayer.className}`;
19 | static AUTO_FLOAT_WHEN_SCROLL = `${FloatPlayer.prefix}:AutoFloatWhenScroll`;
20 | static AUTO_FLOAT_WHEN_LEAVE = `${FloatPlayer.prefix}:AutoFloatWhenLeave`;
21 | }
22 |
--------------------------------------------------------------------------------
/src/app/video-player/core/state.ts:
--------------------------------------------------------------------------------
1 | export class PlayState {
2 | static INITIAL = -1;
3 | static INVALID = 0;
4 | static PLAYING = 1;
5 | static PLAY_END = 2;
6 | static PAUSED = 3;
7 | }
8 |
9 | export class ReadyState {
10 | static HAVE_NOTHING = 0;
11 | static HAVE_METADATA = 1;
12 | static HAVE_CURRENT_DATA = 2;
13 | static HAVE_FUTURE_DATA = 3;
14 | static HAVE_ENOUGH_DATA = 4;
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/video-player/float-controls/float-controls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{videoTitle}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
{{currentTimeClock}} / {{durationClock}}
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/app/video-player/float-controls/non-interactive-progress-bar/non-interactive-progress-bar.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, ElementRef, OnDestroy, OnInit, Self } from '@angular/core';
2 | import { Subscription } from 'rxjs/index';
3 | import { VideoPlayer } from '../../video-player.component';
4 |
5 | @Component({
6 | selector: 'non-interactive-progress-bar',
7 | templateUrl: 'non-interactive-progress-bar.html',
8 | styleUrls: ['./non-interactive-progress-bar.less']
9 | })
10 | export class NonInteractiveProgressBarComponent implements OnInit, OnDestroy {
11 | private _subscription = new Subscription();
12 |
13 | duration = Number.NaN;
14 | currentTime = 0;
15 | buffered = 0;
16 |
17 | get playProgressPercentage(): number {
18 | if (Number.isNaN(this.duration)) {
19 | return 0;
20 | }
21 | return Math.round(this.currentTime / this.duration * 1000) / 10;
22 | }
23 |
24 | get bufferedPercentage(): number {
25 | if (Number.isNaN(this.duration)) {
26 | return 0;
27 | }
28 | return Math.round(this.buffered / this.duration * 1000) / 10;
29 | }
30 |
31 | constructor(@Self() private _hostRef: ElementRef, private _videoPlayer: VideoPlayer) {
32 | }
33 |
34 | ngOnInit(): void {
35 | this._subscription.add(
36 | this._videoPlayer.currentTime.subscribe(time => this.currentTime = time)
37 | );
38 | this._subscription.add(
39 | this._videoPlayer.duration.subscribe(duration => {
40 | this.duration = duration;
41 | })
42 | );
43 | this._subscription.add(
44 | this._videoPlayer.buffered.subscribe(buffered => this.buffered = buffered)
45 | );
46 | }
47 |
48 | ngOnDestroy(): void {
49 | this._subscription.unsubscribe();
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/video-player/float-controls/non-interactive-progress-bar/non-interactive-progress-bar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/video-player/float-controls/non-interactive-progress-bar/non-interactive-progress-bar.less:
--------------------------------------------------------------------------------
1 | @import '../../controls/scrub-bar/scrub-bar.less';
2 |
3 | :host {
4 | height: 0.36rem;
5 | .progress-bar-wrapper {
6 | height: 0.36rem;
7 | .scrub-bar-wrapper();
8 | }
9 | }
--------------------------------------------------------------------------------
/src/app/video-player/help-dialog/help-dialog.component.ts:
--------------------------------------------------------------------------------
1 |
2 | import {fromEvent as observableFromEvent, Subscription , Observable } from 'rxjs';
3 |
4 | import {filter} from 'rxjs/operators';
5 | import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, Self, ViewChild } from '@angular/core';
6 | import { UIDialogRef } from 'deneb-ui';
7 |
8 | export const KEY_ESC = 27;
9 |
10 | @Component({
11 | selector: 'video-player-help-dialog',
12 | templateUrl: './help-dialog.html',
13 | styleUrls: ['./help-dialog.less'],
14 | host: {
15 | 'tabIndex': '-1'
16 | }
17 | })
18 | export class VideoPlayerHelpDialog implements OnInit, AfterViewInit, OnDestroy {
19 | private _subscription = new Subscription();
20 |
21 | @ViewChild('closeButton', {static: false}) closeButton: ElementRef;
22 |
23 | constructor(private _dialogRef: UIDialogRef) {
24 | }
25 |
26 | closeDialog(event: Event) {
27 | event.preventDefault();
28 | event.stopPropagation();
29 | this._dialogRef.close(null);
30 | }
31 |
32 | ngOnInit(): void {
33 | this._subscription.add(
34 | observableFromEvent(document, 'keyup').pipe(
35 | filter((event: KeyboardEvent) => {
36 | return event.which === KEY_ESC;
37 | }))
38 | .subscribe(() => {
39 | this._dialogRef.close('esc');
40 | })
41 | );
42 | }
43 |
44 | ngAfterViewInit(): void {
45 | let closeButtonElement = this.closeButton.nativeElement as HTMLElement;
46 | closeButtonElement.focus();
47 | }
48 |
49 | ngOnDestroy(): void {
50 | this._subscription.unsubscribe();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/video-player/help-dialog/help-dialog.html:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 | space |
11 | 播放/暂停 |
12 |
13 |
14 | ←<, |
15 | 快退 |
16 |
17 |
18 | →>. |
19 | 快进 |
20 |
21 |
22 | ↵ |
23 | 全屏/退出全屏 |
24 |
25 | -_ |
26 | 音量减小10% |
27 |
28 | =+ |
29 | 音量增加10% |
30 |
31 |
32 | m |
33 | 静音/取消静音 |
34 |
35 |
36 | c |
37 | 截图 |
38 |
39 |
40 | /? |
41 | 帮助 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/app/video-player/help-dialog/help-dialog.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | text-align: left;
5 | @media(max-width: 375px) {
6 | width: 90%;
7 | }
8 | width: 300px;
9 | height: 410px;
10 | margin: auto;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | position: absolute;
16 | border-radius: 4px;
17 | background-color: #fff;
18 | overflow: hidden;
19 | }
20 |
21 | .dialog-title {
22 | padding: 0.5rem;
23 | position: relative;
24 | border-bottom: 1px solid #ccc;
25 | > h3 {
26 | margin: 0;
27 | text-align: center;
28 | font-weight: 600;
29 | color: #414141;
30 | }
31 | > .close-button {
32 | position: absolute;
33 | top: 0.5rem;
34 | right: 0.5rem;
35 | color: #636363;
36 | font-size: 1.5rem;
37 | cursor: pointer;
38 | outline: none;
39 | }
40 | }
41 | .dialog-content {
42 | padding: 1rem;
43 | > table.no-border {
44 | border: none;
45 | td {
46 | padding: 0.4rem 0.6rem;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/app/video-player/next-episode-overlay/next-episode-overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
![]()
3 |
4 |
5 |
6 |
下一话
7 |
{{nextEpisodeNameCN}}
8 |
{{nextEpisodeName}}
9 |
10 |
11 |
16 |
17 |
18 |
19 | 取消
20 |
21 |
--------------------------------------------------------------------------------
/src/app/video-player/touch-controls/touch-controls.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
{{currentTimeClock}}
10 |
11 |
12 |
13 |
{{durationClock}}
14 |
15 |
--------------------------------------------------------------------------------
/src/app/video-player/touch-controls/touch-controls.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | position: absolute;
4 | top: 0;
5 | left: 0;
6 | right: 0;
7 | bottom: 0;
8 | }
9 |
10 | .touch-control-wrapper {
11 | width: 100%;
12 | height: 100%;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | -webkit-user-select: none;
17 | -moz-user-select: none;
18 | -ms-user-select: none;
19 | user-select: none;
20 | overflow: hidden;
21 | position: relative;
22 | z-index: 100;
23 | .top-section {
24 | position: absolute;
25 | left: 0;
26 | top: 0;
27 | width: 100%;
28 | display: flex;
29 | flex-direction: row;
30 | justify-content: flex-end;
31 | align-items: flex-start;
32 | padding: 0.5rem 0 1.5rem 0;
33 | color: #fff;
34 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
35 | }
36 | .center-section {
37 | position: relative;
38 | display: flex;
39 | flex-direction: row;
40 | justify-content: center;
41 | font-size: 2.5rem;
42 | color: #fff;
43 | }
44 | .bottom-section {
45 | position: absolute;
46 | left: 0;
47 | bottom: -0.5rem;
48 | width: 100%;
49 | display: flex;
50 | flex-direction: row;
51 | justify-content: space-between;
52 | align-items: flex-end;
53 | padding: 0 0.5rem;
54 | /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#000000+0,000000+100&0+0,0.65+100 */
55 | background: linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
56 | .time-indicator {
57 | flex: 0 1 auto;
58 | font-family: "Segoe UI", sans-serif;
59 | font-size: 1rem;
60 | color: #fff;
61 | line-height: 1;
62 | margin-bottom: 1rem;
63 | }
64 | .scrub-wrapper {
65 | flex: 1 1 auto;
66 | margin-left: 1rem;
67 | margin-right: 1rem;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/src/app/video-player/video-player.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/src/app/video-player/video-player.less:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | box-sizing: border-box;
4 | flex: 0 1 auto;
5 | position: relative;
6 | background-color: #000;
7 | outline: none;
8 | width: 854px;
9 | height: 480px;
10 |
11 | @media screen and (max-width: 656px) {
12 | width: 426px;
13 | height: 240px;
14 | }
15 |
16 | @media screen and (min-width: 1320px) and (min-height: 870px) {
17 | width: 1280px;
18 | height: 720px;
19 | }
20 |
21 | &.is-portrait:not(.fullscreen):not(.float-play) {
22 | position: fixed;
23 | top: 50px; // navbar height
24 | left: 0;
25 | z-index: 204;
26 | }
27 |
28 | &.fullscreen {
29 | position: fixed;
30 | top: 0;
31 | left: 0;
32 | right: 0;
33 | bottom: 0;
34 | width: 100%;
35 | height: 100%;
36 | }
37 |
38 | &.float-play {
39 | position: fixed;
40 | right: 1rem;
41 | bottom: 1rem;
42 | z-index: 204;
43 | //&.is-portrait {
44 | // bottom: inherit;
45 | // top: 3.5rem;
46 | //}
47 | }
48 | }
49 |
50 | video {
51 | display: block;
52 | width: 100%;
53 | height: 100%;
54 | }
55 |
56 | .overlay {
57 | display: none;
58 | position: absolute;
59 | top: 0;
60 | left: 0;
61 | width: 100%;
62 | height: 100%;
63 | &.show {
64 | display: block;
65 | }
66 | }
--------------------------------------------------------------------------------
/src/assets/css/.gitkeep:
--------------------------------------------------------------------------------
1 | @AngularClass
2 |
--------------------------------------------------------------------------------
/src/assets/css/main.css:
--------------------------------------------------------------------------------
1 | #body-container {
2 | margin: 0;
3 | height: 100%;
4 | }
5 | .bgm-link-icon {
6 | display: inline-block;
7 | vertical-align: middle;
8 | }
9 | .bgm-rate-emo {
10 | background: transparent url('/assets/img/rate_emo.gif') no-repeat;
11 | background-position: -77px 0;
12 | height: 37px;
13 | width: 30px;
14 | }
15 |
16 | .bgm-emo-47 {
17 | background: transparent url('/assets/img/24.gif') no-repeat;
18 | height: 21px;
19 | width: 21px;
20 | display: inline-block;
21 | vertical-align: baseline;
22 | }
--------------------------------------------------------------------------------
/src/assets/data.json:
--------------------------------------------------------------------------------
1 | {
2 | "value": "AngularClass"
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/google-fonts/1KWMyx7m-L0fkQGwYhWwuuvvDin1pK8aKteLpeZ5c0A.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/1KWMyx7m-L0fkQGwYhWwuuvvDin1pK8aKteLpeZ5c0A.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/8qcEw_nrk_5HEcCpYdJu8BTbgVql8nDJpwnrE27mub0.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/8qcEw_nrk_5HEcCpYdJu8BTbgVql8nDJpwnrE27mub0.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/AcvTq8Q0lyKKNxRlL28Rn4X0hVgzZQUfRDuZrPvH3D8.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/AcvTq8Q0lyKKNxRlL28Rn4X0hVgzZQUfRDuZrPvH3D8.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/HkF_qI1x_noxlxhrhMQYEJBw1xU1rKptJj_0jans920.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/HkF_qI1x_noxlxhrhMQYEJBw1xU1rKptJj_0jans920.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/MDadn8DQ_3oT6kvnUq_2r_esZW2xOQ-xsNqO47m55DA.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/MDadn8DQ_3oT6kvnUq_2r_esZW2xOQ-xsNqO47m55DA.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/MgNNr5y1C_tIEuLEmicLmwLUuEpTyoUstqEm5AMlJo4.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/MgNNr5y1C_tIEuLEmicLmwLUuEpTyoUstqEm5AMlJo4.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/cT2GN3KRBUX69GVJ2b2hxn-_kf6ByYO6CLYdB4HQE-Y.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/cT2GN3KRBUX69GVJ2b2hxn-_kf6ByYO6CLYdB4HQE-Y.woff2
--------------------------------------------------------------------------------
/src/assets/google-fonts/rZPI2gHXi8zxUjnybc2ZQFKPGs1ZzpMvnHX-7fPOuAc.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/google-fonts/rZPI2gHXi8zxUjnybc2ZQFKPGs1ZzpMvnHX-7fPOuAc.woff2
--------------------------------------------------------------------------------
/src/assets/humans.txt:
--------------------------------------------------------------------------------
1 | # humanstxt.org/
2 | # The humans responsible & technology colophon
3 |
4 | # TEAM
5 |
6 | -- --
7 |
8 | # THANKS
9 |
10 |
11 | PatrickJS -- @gdi2290
12 | AngularClass -- @AngularClass
13 |
14 | # TECHNOLOGY COLOPHON
15 |
16 | HTML5, CSS3
17 | Angular2, TypeScript, Webpack
18 |
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-192x192.png
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-36x36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-36x36.png
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-48x48.png
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/icon/android-icon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/android-icon-96x96.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-114x114.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-120x120.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-152x152.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-180x180.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-57x57.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-60x60.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-72x72.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-76x76.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon-precomposed.png
--------------------------------------------------------------------------------
/src/assets/icon/apple-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/apple-icon.png
--------------------------------------------------------------------------------
/src/assets/icon/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 | #ffffff
--------------------------------------------------------------------------------
/src/assets/icon/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/favicon-16x16.png
--------------------------------------------------------------------------------
/src/assets/icon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/favicon-32x32.png
--------------------------------------------------------------------------------
/src/assets/icon/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/favicon-96x96.png
--------------------------------------------------------------------------------
/src/assets/icon/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/favicon.ico
--------------------------------------------------------------------------------
/src/assets/icon/ms-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/ms-icon-144x144.png
--------------------------------------------------------------------------------
/src/assets/icon/ms-icon-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/ms-icon-150x150.png
--------------------------------------------------------------------------------
/src/assets/icon/ms-icon-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/ms-icon-310x310.png
--------------------------------------------------------------------------------
/src/assets/icon/ms-icon-70x70.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/icon/ms-icon-70x70.png
--------------------------------------------------------------------------------
/src/assets/img/24.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/24.gif
--------------------------------------------------------------------------------
/src/assets/img/angular-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/angular-logo.png
--------------------------------------------------------------------------------
/src/assets/img/angularclass-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/angularclass-avatar.png
--------------------------------------------------------------------------------
/src/assets/img/angularclass-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/angularclass-logo.png
--------------------------------------------------------------------------------
/src/assets/img/mana-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/mana-logo.webp
--------------------------------------------------------------------------------
/src/assets/img/mana-preview-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/mana-preview-1.webp
--------------------------------------------------------------------------------
/src/assets/img/mana-preview-2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/mana-preview-2.webp
--------------------------------------------------------------------------------
/src/assets/img/megumin-logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/megumin-logo.webp
--------------------------------------------------------------------------------
/src/assets/img/megumin-preview-1.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/megumin-preview-1.webp
--------------------------------------------------------------------------------
/src/assets/img/megumin-preview-2.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/megumin-preview-2.webp
--------------------------------------------------------------------------------
/src/assets/img/newyear2018.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/newyear2018.png
--------------------------------------------------------------------------------
/src/assets/img/play_badge_new.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/play_badge_new.png
--------------------------------------------------------------------------------
/src/assets/img/rate_emo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lordfriend/Deneb/44560541483739727bfad30b0bb6eddbf57b6a15/src/assets/img/rate_emo.gif
--------------------------------------------------------------------------------
/src/assets/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "App",
3 | "icons": [
4 | {
5 | "src": "/assets/icon/android-icon-36x36.png",
6 | "sizes": "36x36",
7 | "type": "image/png",
8 | "density": 0.75
9 | },
10 | {
11 | "src": "/assets/icon/android-icon-48x48.png",
12 | "sizes": "48x48",
13 | "type": "image/png",
14 | "density": 1.0
15 | },
16 | {
17 | "src": "/assets/icon/android-icon-72x72.png",
18 | "sizes": "72x72",
19 | "type": "image/png",
20 | "density": 1.5
21 | },
22 | {
23 | "src": "/assets/icon/android-icon-96x96.png",
24 | "sizes": "96x96",
25 | "type": "image/png",
26 | "density": 2.0
27 | },
28 | {
29 | "src": "/assets/icon/android-icon-144x144.png",
30 | "sizes": "144x144",
31 | "type": "image/png",
32 | "density": 3.0
33 | },
34 | {
35 | "src": "/assets/icon/android-icon-192x192.png",
36 | "sizes": "192x192",
37 | "type": "image/png",
38 | "density": 4.0
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/mock-data/mock-data.json:
--------------------------------------------------------------------------------
1 | [
2 | {"res": "data"}
3 | ]
4 |
--------------------------------------------------------------------------------
/src/assets/robots.txt:
--------------------------------------------------------------------------------
1 | # robotstxt.org
2 |
3 | User-agent: *
4 |
--------------------------------------------------------------------------------
/src/assets/semantic-ui.ts:
--------------------------------------------------------------------------------
1 | // semantic ui lib
2 | require('semantic-ui-less/semantic.less');
3 | require('../app/app.less');
4 |
--------------------------------------------------------------------------------
/src/assets/service-worker.js:
--------------------------------------------------------------------------------
1 | // This file is intentionally without code.
2 |
--------------------------------------------------------------------------------
/src/assets/site/elements/flag.variables:
--------------------------------------------------------------------------------
1 | @spritePath: "../../themes/default/assets/images/flags.png";
2 |
--------------------------------------------------------------------------------
/src/assets/site/globals/site.variables:
--------------------------------------------------------------------------------
1 | @imagePath : '../assets/images';
2 | @fontPath : '../assets/fonts';
3 |
--------------------------------------------------------------------------------
/src/helpers/base.service.ts:
--------------------------------------------------------------------------------
1 |
2 | import {throwError as observableThrowError, Observable} from 'rxjs';
3 | import {AuthError} from './error';
4 | import {ServerError} from './error/ServerError';
5 | import {ClientError} from './error/ClientError';
6 |
7 | export abstract class BaseService {
8 |
9 | handleError(resp: any) {
10 | var error: Error;
11 | if (resp.status === 400) {
12 | if (resp.json().message === AuthError.LOGIN_FAIL) {
13 | error = new AuthError(resp.message, resp.status);
14 | } else {
15 | error = new ClientError(resp.message, resp.status);
16 | }
17 | } else if (resp.status === 401) {
18 | error = new AuthError(resp.message, resp.status);
19 | } else if (resp.status == 403) {
20 | error = new AuthError(resp.message, resp.status);
21 | } else if (resp.status === 500) {
22 | error = new ServerError(resp.message, resp.status);
23 | } else if (resp.status === 502) {
24 | error = new ServerError('Server offline', resp.status);
25 | } else {
26 | error = new ServerError('Network Error', 0);
27 | }
28 | return observableThrowError(error);
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/helpers/browser-detect.ts:
--------------------------------------------------------------------------------
1 | export const isChrome = !!window && !!window.chrome && (!!window.chrome.webstore || !!window.chrome.runtime);
2 | export const isFirefox = typeof InstallTrigger !== 'undefined';
3 | export const isIE = /*@cc_on!@*/false || !!document.documentMode;
4 | export const isEdge = !isIE && !!window && !!window.StyleMedia;
5 |
--------------------------------------------------------------------------------
/src/helpers/dom.ts:
--------------------------------------------------------------------------------
1 | // select closet parent element
2 | export function closest(el, selector) {
3 | const matchesSelector = el.matches || el.webkitMatchesSelector || el.mozMatchesSelector || el.msMatchesSelector;
4 |
5 | while (el) {
6 | if (matchesSelector.call(el, selector)) {
7 | return el;
8 | } else {
9 | el = el.parentElement;
10 | }
11 | }
12 | return null;
13 | }
14 |
15 | export function getRemPixel(remValue: number): number {
16 | return remValue * parseFloat(window.getComputedStyle(document.body).getPropertyValue('font-size').match(/(\d+(?:\.\d+)?)px/)[1]);
17 | }
18 |
19 | /**
20 | * get the vw in pixel
21 | * @param value
22 | */
23 | export function getVwInPixel(value: number): number {
24 | let w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
25 | return value / 100 * w;
26 | }
27 |
28 | export function getVhInPixel(value: number): number {
29 | let h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
30 | return value / 100 * h;
31 | }
32 |
--------------------------------------------------------------------------------
/src/helpers/error/AuthError.ts:
--------------------------------------------------------------------------------
1 | import {BaseError} from './BaseError';
2 |
3 | export class AuthError extends BaseError {
4 |
5 | // login error
6 | static LOGIN_FAIL = 'invalid name or password';
7 |
8 |
9 | // register error
10 | static INVALID_INVITE_CODE = 'invalid invite code';
11 | static DUPLICATE_NAME = 'duplicate name';
12 | static PASSWORD_MISMATCH = 'password not match';
13 | static INVALID_EMAIL = 'invalid email';
14 |
15 | // update pass error
16 | static PASSWORD_INCORRECT = 'password incorrect';
17 |
18 | static PERMISSION_DENIED = 'permission denied';
19 |
20 | constructor(
21 | public message: string,
22 | public status: number) {
23 | super('AuthError', message, status);
24 | }
25 |
26 | public isPermission(): boolean {
27 | return this.status === 403;
28 | }
29 |
30 | public isUnauthorized(): boolean {
31 | return this.status === 401;
32 | }
33 |
34 | public isLoginFailed(): boolean {
35 | return this.status === 400 && this.message === AuthError.LOGIN_FAIL;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/helpers/error/BaseError.ts:
--------------------------------------------------------------------------------
1 | export class BaseError implements Error {
2 | name: string;
3 | status: number;
4 | message: string;
5 | constructor(name: string, value: string, status?: number) {
6 | this.name = name;
7 | this.message = value;
8 | this.status = status;
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/helpers/error/ClientError.ts:
--------------------------------------------------------------------------------
1 | import {BaseError} from "./BaseError";
2 |
3 |
4 | export class ClientError extends BaseError {
5 |
6 | // common error
7 | static INVALID_REQUEST = 'invalid parameter';
8 |
9 | static DUPLICATE_EMAIL = 'duplicate email';
10 |
11 | static MAIL_NOT_EXISTS = 'email not exists';
12 |
13 | constructor(
14 | public message: string,
15 | public status: number) {
16 | super('ClientError', message, status);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/helpers/error/ServerError.ts:
--------------------------------------------------------------------------------
1 | import {BaseError} from './BaseError';
2 |
3 |
4 | export class ServerError extends BaseError {
5 | constructor(
6 | public message: string,
7 | public status: number) {
8 | super('ServerError', message, status);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/helpers/error/index.ts:
--------------------------------------------------------------------------------
1 | export {AuthError} from './AuthError';
2 | export {ServerError} from './ServerError';
3 | export {BaseError} from './BaseError';
4 |
--------------------------------------------------------------------------------
/src/helpers/localstorage.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * provide a in-memory polyfill for localstorage
3 | */
4 | export class LocalStorage implements Storage {
5 | valuesMap = new Map();
6 |
7 | getItem(key: string): string | null {
8 | const stringKey = String(key);
9 | if (this.valuesMap.has(key)) {
10 | return String(this.valuesMap.get(stringKey));
11 | }
12 | return null;
13 | }
14 |
15 | setItem(key: string, val: any): void {
16 | this.valuesMap.set(String(key), String(val));
17 | }
18 |
19 | removeItem(key: string): void {
20 | this.valuesMap.delete(key);
21 | }
22 |
23 | clear(): void {
24 | this.valuesMap.clear();
25 | }
26 |
27 | key(index: number): string | null {
28 | if (arguments.length === 0) {
29 | throw new TypeError("Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present.") // this is a TypeError implemented on Chrome, Firefox throws Not enough arguments to Storage.key.
30 | }
31 | let arr = Array.from(this.valuesMap.keys());
32 | return arr[index];
33 | }
34 |
35 | get length(): number {
36 | return this.valuesMap.size;
37 | }
38 |
39 | [key: string]: any;
40 | [index: number]: string;
41 | }
42 | export let localstorageSupport: boolean;
43 | export let storageAPI: Storage;
44 | try {
45 | // Test webstorage existence.
46 | if (!window.localStorage || !window.sessionStorage) throw "exception";
47 | // Test webstorage accessibility - Needed for Safari private browsing.
48 | localStorage.setItem('storage_test', '1');
49 | localStorage.removeItem('storage_test');
50 | localstorageSupport = true;
51 | storageAPI = window.localStorage;
52 | } catch (e) {
53 | console.log('localstorage disabled, use in-memory object as polyfill');
54 | localstorageSupport = false;
55 | storageAPI = new LocalStorage();
56 | }
57 |
--------------------------------------------------------------------------------
/src/helpers/url.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * some code get from query-string
3 | */
4 |
5 | export function queryString(obj: any): string {
6 | let formatter = function (key, value) {
7 | return value === null ? encodeURIComponent(key) : [
8 | encodeURIComponent(key),
9 | '=',
10 | encodeURIComponent(value)
11 | ].join('');
12 | };
13 |
14 | return obj ? Object.keys(obj).sort().map(function (key) {
15 | let val = obj[key];
16 |
17 | if (val === undefined) {
18 | return '';
19 | }
20 |
21 | if (val === null) {
22 | return encodeURIComponent(key);
23 | }
24 |
25 | if (Array.isArray(val)) {
26 | let result = [];
27 |
28 | val.slice().forEach(function (val2) {
29 | if (val2 === undefined) {
30 | return;
31 | }
32 |
33 | result.push(formatter(key, val2));
34 | });
35 |
36 | return result.join('&');
37 | }
38 |
39 | return encodeURIComponent(key) + '=' + encodeURIComponent(val);
40 | }).filter(function (x) {
41 | return x.length > 0;
42 | }).join('&') : '';
43 | }
44 |
--------------------------------------------------------------------------------
/src/main.browser.ts:
--------------------------------------------------------------------------------
1 | import './assets/semantic-ui';
2 | /*
3 | * Providers provided by Angular
4 | */
5 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
6 | import {decorateModuleRef} from './app/environment';
7 |
8 | /*
9 | * App Component
10 | * our top level component that holds all of our components
11 | */
12 | import {AppModule} from './app';
13 |
14 | /*
15 | * Bootstrap our Angular app with a top level NgModule
16 | */
17 | platformBrowserDynamic()
18 | .bootstrapModule(AppModule)
19 | .then(decorateModuleRef)
20 | .catch((err) => console.error(err));
21 |
--------------------------------------------------------------------------------
/src/polyfills.browser.ts:
--------------------------------------------------------------------------------
1 | // Polyfills
2 | // (these modules are what are in 'angular2/bundles/angular2-polyfills' so don't use that here)
3 |
4 | // import 'ie-shim'; // Internet Explorer
5 |
6 | // Added parts of es6 which are necessary for your project or your browser support requirements.
7 | // import 'core-js/es6/symbol';
8 | // import 'core-js/es6/object';
9 | // import 'core-js/es6/function';
10 | // import 'core-js/es6/parse-int';
11 | // import 'core-js/es6/parse-float';
12 | // import 'core-js/es6/number';
13 | // import 'core-js/es6/math';
14 | // import 'core-js/es6/string';
15 | // import 'core-js/es6/date';
16 | // import 'core-js/es6/array';
17 | // import 'core-js/es6/regexp';
18 | // import 'core-js/es6/map';
19 | // import 'core-js/es6/set';
20 | import 'core-js/es6/weak-map';
21 | import 'core-js/es6/weak-set';
22 | import 'core-js/es6/typed';
23 | import 'core-js/es6/reflect';
24 | // see issue https://github.com/AngularClass/angular2-webpack-starter/issues/709
25 | // import 'core-js/es6/promise';
26 | import 'core-js/es7/reflect';
27 | import 'zone.js/dist/zone';
28 |
29 | import 'intersection-observer';
30 | import 'web-animations-js';
31 | import 'url-polyfill';
32 |
33 | if ('production' === ENV) {
34 | // Production
35 |
36 |
37 | } else {
38 | // Development
39 |
40 | Error.stackTraceLimit = Infinity;
41 |
42 | require('zone.js/dist/long-stack-trace-zone');
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
3 |
--------------------------------------------------------------------------------
/src/service-worker/register.ts:
--------------------------------------------------------------------------------
1 | if ('serviceWorker' in navigator) {
2 | window.addEventListener('load', () => {
3 | navigator.serviceWorker.register('/sw.js');
4 | });
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "sourceMap": true,
10 | "noEmitHelpers": true,
11 | "importHelpers": true,
12 | "strictNullChecks": false,
13 | "typeRoots": ["node_modules/@types"],
14 | "lib": [
15 | "dom",
16 | "es2018"
17 | ]
18 | },
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | "src/service-worker"
23 | ],
24 | "angularCompilerOptions": {
25 | "skipMetadataEmit": true
26 | },
27 | "compileOnSave": false,
28 | "buildOnSave": false,
29 | "atom": { "rewriteTsconfig": false }
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.webpack.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2015",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "sourceMap": true,
10 | "noEmitHelpers": true,
11 | "importHelpers": true,
12 | "strictNullChecks": false,
13 | "lib": [
14 | "es2018",
15 | "dom"
16 | ],
17 | "typeRoots": ["node_modules/@types"],
18 | },
19 | "exclude": [
20 | "node_modules",
21 | "dist",
22 | "src/**/*.spec.ts",
23 | "src/**/*.e2e.ts",
24 | "src/service-worker"
25 | ],
26 | "angularCompilerOptions": {
27 | "skipMetadataEmit": true
28 | },
29 | "compileOnSave": false,
30 | "buildOnSave": false,
31 | "atom": {
32 | "rewriteTsconfig": false
33 | }
34 | }
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "component-selector-name": [true, "kebab-case"],
7 | "component-selector-type": [true, "element"],
8 | "host-parameter-decorator": true,
9 | "input-parameter-decorator": true,
10 | "output-parameter-decorator": true,
11 | "attribute-parameter-decorator": false,
12 | "input-property-directive": true,
13 | "output-property-directive": true,
14 |
15 | "class-name": true,
16 | "curly": false,
17 | "eofline": true,
18 | "indent": [
19 | true,
20 | "spaces"
21 | ],
22 | "max-line-length": [
23 | true,
24 | 200
25 | ],
26 | "member-ordering": [
27 | true,
28 | "static-before-instance",
29 | "variables-before-functions"
30 | ],
31 | "no-arg": true,
32 | "no-construct": true,
33 | "no-duplicate-key": true,
34 | "no-duplicate-variable": true,
35 | "no-empty": false,
36 | "no-eval": true,
37 | "trailing-comma": true,
38 | "no-trailing-whitespace": false,
39 | "no-unused-expression": true,
40 | "no-unused-variable": false,
41 | "no-unreachable": true,
42 | "no-use-before-declare": true,
43 | "one-line": [
44 | true,
45 | "check-open-brace",
46 | "check-catch",
47 | "check-else",
48 | "check-whitespace"
49 | ],
50 | "quotemark": [
51 | true,
52 | "single"
53 | ],
54 | "semicolon": true,
55 | "triple-equals": [
56 | true,
57 | "allow-null-check"
58 | ],
59 | "variable-name": false,
60 | "whitespace": [
61 | true,
62 | "check-branch",
63 | "check-decl",
64 | "check-operator",
65 | "check-separator",
66 | "check-type"
67 | ]
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @author: @AngularClass
3 | */
4 | const fs = require('fs');
5 | const helpers = require('./config/helpers');
6 |
7 | /**
8 | * check custom login style exists
9 | */
10 |
11 | let loginStyleExsits;
12 | try {
13 | loginStyleExsits = fs.statSync(helpers.root('src/assets/css/login.css')).isFile();
14 | console.log('login style file existence: ' + loginStyleExsits);
15 | } catch (e) {
16 | console.error('no login style file found, use default');
17 | loginStyleExsits = false;
18 | }
19 |
20 | const ENV = process.env.ENV = process.env.NODE_ENV;
21 | /**
22 | * Webpack Constants
23 | */
24 | const METADATA = {
25 | host: '0.0.0.0',
26 | port: 3000,
27 | title: process.env.SITE_TITLE || 'Deneb',
28 | baseUrl: '/',
29 | GA: process.env.GA || '',
30 | customLoginStyle: loginStyleExsits,
31 | chrome_extension_id: process.env.CHROME_EXTENSION_ID || '',
32 | firefox_extension_id: process.env.FIREFOX_EXTENSION_ID || '',
33 | edge_extension_id: process.env.EDGE_EXTENSION_ID || '',
34 | firefox_extension_url: process.env.FIREFOX_EXTENSION_URL || '',
35 | };
36 |
37 | console.log('CHROME_EXTENSION_ID: ', METADATA.chrome_extension_id);
38 |
39 |
40 | // Look in ./config folder for webpack.dev.js
41 | switch (ENV) {
42 | case 'prod':
43 | case 'production':
44 | METADATA.port = process.env.PORT || 8080;
45 | METADATA.ENV = ENV || 'production';
46 | METADATA.HMR = false;
47 | module.exports = require('./config/webpack.prod')(METADATA);
48 | break;
49 | // case 'test':
50 | // case 'testing':
51 | // METADATA.ENV = ENV || 'test';
52 | // module.exports = require('./config/webpack.test')(METADATA);
53 | // break;
54 | case 'dev':
55 | case 'development':
56 | default:
57 | METADATA.ENV = ENV || 'development';
58 | METADATA.HMR = true;
59 | module.exports = require('./config/webpack.dev')(METADATA);
60 | }
61 |
--------------------------------------------------------------------------------