├── .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 |
2 |
3 | 4 |
番組详情
5 | 6 | 确定保存 7 | 8 |
9 |
10 |
11 |
12 | image 13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 |
41 |
42 |
-------------------------------------------------------------------------------- /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 |
2 |
3 | 13 |
14 | 18 |
-------------------------------------------------------------------------------- /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 |
3 | {{webHook.name}} 4 | ID:{{webHook.id}} 5 | ALIVE 6 | HAS ERROR 7 | DEAD 8 | INITIAL 9 | 10 | 11 | 连续错误次数 12 | {{webHook.consecutive_failure_count}} 13 | 14 |
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 |
8 | 9 | 12 | 13 |
14 |
15 |
还没有添加Web Hook
16 |
17 |
18 |
Loading...
19 |
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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
2 |
3 |

重置密码

4 |
5 |
6 | 7 |
8 | 12 |
13 |
14 |
-------------------------------------------------------------------------------- /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 |
10 |
11 | {{character.name}} 12 |
13 |
14 | {{character.role_name}} 15 | {{character.name_cn}} 16 |
17 |
18 | CV: 19 | 20 | {{actor.name}} 21 | / 22 | 23 |
24 |
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 |
3 |
4 | {{job}}: 5 | 6 | {{staff.name}} 7 | / 8 | 9 |
10 |
-------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |
2 | 我在看的番組 3 |
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 |
5 |
6 | 7 |
8 |
9 | 10 | 14 |
15 |
-------------------------------------------------------------------------------- /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 |
5 |
6 | 7 |
8 |
9 | 10 | 14 |
15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /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 |
2 |

反馈问题

3 |
4 |
5 | 9 |
10 |
11 |
12 | 13 |
14 | 18 |
-------------------------------------------------------------------------------- /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 |

14 | {{currentPV.name_cn}} 15 |

16 |

17 | {{currentPV.name}} 18 |

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 | 11 |
12 |
13 |
14 |
15 |
16 | {{comment}} 17 |
18 |
19 |
20 |
暂时还没有评论
21 | 24 |
25 |
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 | 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 |
17 | 权限 18 |
{{perm}}
19 |
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 |
5 |

6 | {{siteTitle}} 7 |

8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
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 |
2 |
3 |
4 |
5 |

6 | 注册 7 |

8 |
9 |
10 |
11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 33 |
34 |

{{errorMessage}}

35 |
36 |
37 |
38 |
39 |
-------------------------------------------------------------------------------- /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 |
2 |

开放API

3 |

本站允许开发者根据需要来使用API访问网站内容,请开发者自行阅读API文档,注意除了登陆和注册相关API,其他API均需要登录访问。

4 |

HTTP API文档(采用cookie来认证)

5 |

源代码

6 |

我们坚持开放源代码与社区共同维护我们的服务。您可以访问到服务端和客户端的源代码

7 |
    8 |
  1. 服务端源代码:https://github.com/lordfriend/Albireo
  2. 9 |
  3. Web客户端源代码:https://github.com/lordfriend/Deneb
  4. 10 |
11 |
-------------------------------------------------------------------------------- /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 |
  1. 5 | 当你访问Web版客户端时,我们使用Google Analytics收集必要的信息用于统计用户使用状况,这些信息由Google收集与储存,本站并不直接存储这些信息。 6 | 所有的这些信息都是匿名的,我们不会将帐号信息提供给Google。 7 |
  2. 8 |
  3. 9 | 为了更好的提供服务,本站保存了您的用户名,密码,邮箱地址。其中密码经过加盐hash加密存储的,确保任何人(包括我们)不会知道泄露您的原始密码。 10 | 此外本站还会自动保存您的观看记录和进度。 11 |
  4. 12 |
  5. 13 | 所有这些信息收集政策都仅限Web客户端,如果您使用的是第三方客户端,您需要检查第三方客户端的信息收集政策。 14 |
  6. 15 |
16 |

我们分享的信息

17 |
    18 |
  1. 19 | 我们仅使用Google Analytics收集匿名信息,但我们并不会与Google分享任何您的帐号相关信息。 20 |
  2. 21 |
  3. 22 | 当您使用第三方客户端时,请检查第三方客户端的隐私政策来了解第三方客户端是否会与其他机构或个人分享信息。 23 |
  4. 24 |
25 |

政策的变化

26 |
    27 |
  1. 28 | 我们的隐私政策可能会随时变化,当隐私政策发生变化时,我们会尽可能通知您,但您依然有义务随时检查隐私政策。只要您继续使用我们的服务即意味着您接受我们的隐私政策。 29 |
  2. 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 |
2 |
3 | 4 |
5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 |
21 |
-------------------------------------------------------------------------------- /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 |
2 |
3 | 4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 | 13 |
14 |
-------------------------------------------------------------------------------- /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 |
5 | 6 | 9 |
10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /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 |
2 |
3 |
4 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
-------------------------------------------------------------------------------- /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 |
2 |
3 |
4 |
-------------------------------------------------------------------------------- /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 |
3 |

快捷键指南

4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
space播放/暂停
<,快退
>.快进
全屏/退出全屏
-_音量减小10%
=+音量增加10%
m静音/取消静音
c截图
/?帮助
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 | 12 | 13 | 14 | 15 | 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 |
9 |
10 |
-------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------