From aaf258761da8bb49b2afa61be7519b486c09020a Mon Sep 17 00:00:00 2001 From: tsukumi Date: Wed, 16 Nov 2022 09:08:42 +0900 Subject: [PATCH] Release: version 0.6.1 --- .github/workflows/build_installer.yaml | 2 ++ .github/workflows/build_thirdparty.yaml | 2 ++ .github/workflows/publish_docker_image.yaml | 7 +++++++ .github/workflows/publish_release.yaml | 14 +++++++++++--- Dockerfile | 2 +- Readme.md | 4 ++-- client/dist/assets/css/app.379ffadb.css | 1 + client/dist/assets/css/app.918ddfe6.css | 1 - client/dist/assets/js/app.4d3619bb.js | 2 -- client/dist/assets/js/app.4d3619bb.js.map | 1 - client/dist/assets/js/app.66d563f0.js | 2 ++ client/dist/assets/js/app.66d563f0.js.map | 1 + client/dist/index.html | 2 +- client/dist/service-worker.js | 2 +- client/dist/service-worker.js.map | 2 +- client/package.json | 2 +- client/src/components/Panel/Channel.vue | 4 ++-- client/src/views/Settings/Index.vue | 2 ++ installer/KonomiTV-Installer.py | 2 +- server/app/constants.py | 2 +- server/app/tasks/LiveEncodingTask.py | 4 ++++ 21 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 client/dist/assets/css/app.379ffadb.css delete mode 100644 client/dist/assets/css/app.918ddfe6.css delete mode 100644 client/dist/assets/js/app.4d3619bb.js delete mode 100644 client/dist/assets/js/app.4d3619bb.js.map create mode 100644 client/dist/assets/js/app.66d563f0.js create mode 100644 client/dist/assets/js/app.66d563f0.js.map diff --git a/.github/workflows/build_installer.yaml b/.github/workflows/build_installer.yaml index 88c10a38..948127b4 100644 --- a/.github/workflows/build_installer.yaml +++ b/.github/workflows/build_installer.yaml @@ -5,6 +5,8 @@ name: Build Installer # .github/workflows/build_installer.yaml (このファイル) に変更があったとき or 手動実行 on: push: + branches: + - master paths: - 'installer/**' - '.github/workflows/build_installer.yaml' diff --git a/.github/workflows/build_thirdparty.yaml b/.github/workflows/build_thirdparty.yaml index 1402a49d..9118d87a 100644 --- a/.github/workflows/build_thirdparty.yaml +++ b/.github/workflows/build_thirdparty.yaml @@ -4,6 +4,8 @@ name: Build Thirdparty Libraries # .github/workflows/build_thirdparty.yaml (このファイル) に変更があったとき or 手動実行 on: push: + branches: + - master paths: - '.github/workflows/build_thirdparty.yaml' workflow_call: diff --git a/.github/workflows/publish_docker_image.yaml b/.github/workflows/publish_docker_image.yaml index 38ce3b99..f263774f 100644 --- a/.github/workflows/publish_docker_image.yaml +++ b/.github/workflows/publish_docker_image.yaml @@ -18,16 +18,23 @@ env: jobs: build-and-push-image: runs-on: ubuntu-latest + if: (startsWith(github.ref, 'refs/tags/v')) || (contains(github.event.head_commit.message, 'Release:') == false) permissions: contents: read packages: write steps: + - name: Checkout Repository uses: actions/checkout@v3 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v2 + - name: Wait for Release to be Created + if: startsWith(github.ref, 'refs/tags/v') + run: | + sleep 180s + # GitHub Container Registry (ghcr.io) にログイン - name: Login to the Container Registry uses: docker/login-action@v2 diff --git a/.github/workflows/publish_release.yaml b/.github/workflows/publish_release.yaml index 31ed5391..e6371f0d 100644 --- a/.github/workflows/publish_release.yaml +++ b/.github/workflows/publish_release.yaml @@ -30,12 +30,21 @@ jobs: runs-on: ubuntu-22.04 steps: + # KonomiTV のソースコードをチェックアウト + - name: Checkout Repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + path: KonomiTV + token: ${{ secrets.GIT_PUSH_TOKEN }} + # release ブランチを現在の master ブランチの最新コミットに更新し、push する ## release ブランチはリリースしたときのみ更新される - name: Update Release Branch + working-directory: KonomiTV run: | - git clone https://github.com/tsukumijima/KonomiTV.git - cd KonomiTV + git config user.name github-actions + git config user.email github-actions@github.com git switch release git merge master git push @@ -64,7 +73,6 @@ jobs: uses: ncipollo/release-action@v1 with: artifacts: "KonomiTV-Installer.exe,KonomiTV-Installer.elf,thirdparty-windows.7z,thirdparty-linux.tar.xz" - draft: true generateReleaseNotes: true name: KonomiTV (β) ${{ github.event.inputs.version }} tag: v${{ github.event.inputs.version }} diff --git a/Dockerfile b/Dockerfile index 3f2bb275..9f133261 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends aria2 ca-certif # サードパーティーライブラリをダウンロード ## サードパーティーライブラリは変更が少ないので、先にダウンロード処理を実行してビルドキャッシュを効かせる WORKDIR / -RUN aria2c -x10 https://github.com/tsukumijima/KonomiTV/releases/download/v0.6.0/thirdparty-linux.tar.xz +RUN aria2c -x10 https://github.com/tsukumijima/KonomiTV/releases/download/v0.6.1/thirdparty-linux.tar.xz RUN tar xvf thirdparty-linux.tar.xz # -------------------------------------------------------------------------------------------------------------- diff --git a/Readme.md b/Readme.md index 420e08be..bee3d5f9 100644 --- a/Readme.md +++ b/Readme.md @@ -310,7 +310,7 @@ Docker Compose は V1 と V2 の両方に対応していますが、できれば
```bash -curl -LO https://github.com/tsukumijima/KonomiTV/releases/download/v0.6.0/KonomiTV-Installer.elf +curl -LO https://github.com/tsukumijima/KonomiTV/releases/download/v0.6.1/KonomiTV-Installer.elf chmod a+x KonomiTV-Installer.elf ./KonomiTV-Installer.elf ``` @@ -389,7 +389,7 @@ KonomiTV の環境設定は、config.yaml に保存されています。 > 設定ファイルは YAML 形式ですが、JSON のようなスタイルで書いています。括弧がないとわかりにくいと思うので… (JSON は YAML のサブセットなので、実は JSON は YAML として解釈可能です) -config.yaml は、インストーラーでインストールした際に自動的に生成されます。将来的には GUI からの環境設定の変更をサポート予定ですが、0.6.0 時点では config.yaml を手動で編集する必要があります。 +config.yaml は、インストーラーでインストールした際に自動的に生成されます。将来的には GUI からの環境設定の変更をサポート予定ですが、現時点では config.yaml を手動で編集する必要があります。 #### バックエンドの設定 diff --git a/client/dist/assets/css/app.379ffadb.css b/client/dist/assets/css/app.379ffadb.css new file mode 100644 index 00000000..22f59aef --- /dev/null +++ b/client/dist/assets/css/app.379ffadb.css @@ -0,0 +1 @@ +@font-face{font-family:Noto Sans JP Caption;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/noto-sans-japanese@1.0.0/fonts/NotoSansJP-Medium.woff2) format("woff2")}html{overflow-y:auto!important}body .v-application{min-height:100vh;min-height:100dvh;font-family:YakuHanJPs,Open Sans,Hiragino Sans,Noto Sans JP,sans-serif;font-weight:500;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@supports(-webkit-touch-callout:none){body .v-application{min-height:-webkit-fill-available}}body .v-application .v-application--wrap{min-height:100%!important}body main{display:flex;width:100%;min-height:100%}body header+main{padding-top:65px!important}@media(max-width:1000px)and (max-height:450px){body header+main{padding-top:0!important}}body .route-container{height:100%;background:var(--v-background-base)}.v-btn{letter-spacing:0!important}.v-snack{left:110px!important}@media(max-width:850px)and (min-height:850.01px){.v-snack{left:0!important}}@media(max-width:1000px)and (max-height:450px){.v-snack{left:0!important}}@media(max-width:600px)and (min-height:450.01px){.v-snack{left:0!important}}.v-snack .v-btn__content{color:var(--v-primary-lighten1);letter-spacing:.3}.v-snack .error .v-btn__content,.v-snack .info .v-btn__content,.v-snack .success .v-btn__content,.v-snack .warning .v-btn__content{color:var(--v-text-base);letter-spacing:.3}.v-popper--theme-tooltip .v-popper__inner{display:inline-block;padding:4px 10px;border-radius:4px;background:var(--v-background-lighten1);color:var(--v-text-base);font-size:12px;font-family:YakuHanJPs,Open Sans,Hiragino Sans,Noto Sans JP,sans-serif;font-weight:500;opacity:.9;line-height:22px}.v-popper--theme-tooltip .v-popper__arrow-container{display:none}@media(hover:none){:hover:before{background-color:transparent!important}}::-moz-selection{background-color:#e64f9780}::selection{background-color:#e64f9780}.decorate-symbol{display:inline-flex;justify-content:center;align-items:center;position:relative;padding:0 3px;margin-left:2.5px;margin-right:2.5px;border-radius:4px;color:var(--v-text-base);background:var(--v-primary-base);font-size:.94em}*{scrollbar-color:var(--v-gray-base) var(--v-background-base);scrollbar-width:thin}::-webkit-scrollbar{width:7px;height:7px}::-webkit-scrollbar-track{background:var(--v-background-base)}::-webkit-scrollbar-thumb{background:var(--v-background-lighten2)}::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.v-menu__content::-webkit-scrollbar{width:12px;height:12px}.v-menu__content::-webkit-scrollbar-thumb{border:solid 3.5px var(--v-background-base)}.v-enter-active,.v-leave-active{transition:opacity .3s}.v-enter,.v-leave-to{opacity:0}.v-enter-active.route-container{position:fixed;top:0;left:0;right:0}.cursor-pointer{cursor:pointer}.w-25{width:25%}.w-50{width:50%}.w-75{width:75%}.w-100{width:100%}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.header[data-v-506af489]{position:fixed;display:flex;align-items:center;width:100%;height:65px;padding:4px 16px;background:var(--v-background-base);box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);z-index:10}@media(max-width:1000px)and (max-height:450px){.header[data-v-506af489]{width:210px;height:48px;justify-content:center}}@media(max-width:680px)and (max-height:450px){.header[data-v-506af489]{width:190px}}@media(max-width:1000px)and (max-height:450px){.header .spacer[data-v-506af489]{display:none}}.header .konomitv-logo[data-v-506af489]{display:block;padding:12px 8px;border-radius:8px}@media(max-width:1000px)and (max-height:450px){.header .konomitv-logo[data-v-506af489]{margin:0!important}}.header .konomitv-logo__image[data-v-506af489]{display:block}@media(max-width:1000px)and (max-height:450px){.header .konomitv-logo__image[data-v-506af489]{height:19.5px}}.navigation-container[data-v-3c027344]{flex-shrink:0;width:220px;background:var(--v-background-lighten1)}@media(max-width:1000px)and (max-height:450px){.navigation-container[data-v-3c027344]{width:210px}}@media(max-width:680px)and (max-height:450px){.navigation-container[data-v-3c027344]{width:190px}}.navigation-container .navigation[data-v-3c027344]{position:fixed;width:220px;top:65px;left:0;bottom:-100px;padding-bottom:100px;background:var(--v-background-lighten1);z-index:1}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation[data-v-3c027344]{top:48px;width:210px}}@media(max-width:680px)and (max-height:450px){.navigation-container .navigation[data-v-3c027344]{width:190px}}.navigation-container .navigation .navigation-scroll[data-v-3c027344]{display:flex;flex-direction:column;height:100%;padding:22px 12px;overflow-y:auto}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll[data-v-3c027344]{padding:10px 12px}}@media(max-width:680px)and (max-height:450px){.navigation-container .navigation .navigation-scroll[data-v-3c027344]{padding:10px 8px}}.navigation-container .navigation .navigation-scroll[data-v-3c027344]::-webkit-scrollbar-track{background:var(--v-background-lighten1)}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]{display:flex;align-items:center;flex-shrink:0;height:52px;padding-left:16px;margin-top:4px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]{height:40px;padding-left:12px;border-radius:9px;font-size:15px}}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]:hover{background:var(--v-background-lighten2)}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]:first-of-type{margin-top:0}.navigation-container .navigation .navigation-scroll .navigation__link--active[data-v-3c027344]{color:var(--v-primary-base);background:#5b2d3c}.navigation-container .navigation .navigation-scroll .navigation__link--active[data-v-3c027344]:hover{background:#5b2d3c}.navigation-container .navigation .navigation-scroll .navigation__link--version[data-v-3c027344]{font-size:15px}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link--version[data-v-3c027344]{font-size:14.5px}}.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon[data-v-3c027344]{margin-right:14px}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon[data-v-3c027344]{margin-right:10px}}.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon--highlight[data-v-3c027344]{color:var(--v-secondary-base)}.channels-container.channels-container--home .v-tabs-bar{height:54px;background:linear-gradient(to bottom,var(--v-background-base) calc(100% - 3px),var(--v-background-lighten1) 3px)}@media(max-width:1000px)and (max-height:450px){.channels-container.channels-container--home .v-tabs-bar{height:46px}}.channels-container.channels-container--home .v-tabs-slider-wrapper{height:3px!important;transition:left .3s cubic-bezier(.25,.8,.5,1)}.channels-container.channels-container--home .v-window__container{min-height:calc(100vh - 180px);min-height:calc(100dvh - 180px)}@media(hover:none){.channels-container.channels-container--home .v-window__container{min-height:auto}}:root .channels-container.channels-container--home .v-window__container,_::-webkit-full-page-media,_:future{height:inherit!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--home .v-window__container--is-active,_::-webkit-full-page-media,_:future{display:none!important}:root .channels-container.channels-container--home .v-window__container .v-window-item,_::-webkit-full-page-media,_:future{display:none!important;position:static!important;transform:none!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--home .v-window__container .v-window-item--active,_::-webkit-full-page-media,_:future{display:block!important}.channels-container[data-v-0185e37f]{display:flex;flex-direction:column;width:100%;margin-left:21px;margin-right:21px;opacity:1;transition:opacity .4s}.channels-container--loading[data-v-0185e37f]{opacity:0}.channels-container .channels-tab[data-v-0185e37f]{position:sticky;flex:none;top:65px;padding-top:10px;padding-bottom:20px;background:var(--v-background-base);z-index:1}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab[data-v-0185e37f]{top:0;padding-top:0;padding-bottom:8px}}.channels-container .channels-tab .channels-tab__item[data-v-0185e37f]{width:98px;padding:0;color:var(--v-text-base)!important;font-size:16px;text-transform:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab .channels-tab__item[data-v-0185e37f]{font-size:15px}}.channels-container .channels-list[data-v-0185e37f]{padding-bottom:32px;background:transparent!important;overflow:inherit}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list[data-v-0185e37f]{padding-bottom:12px}}.channels-container .channels-list .channels[data-v-0185e37f]{display:grid;grid-template-columns:repeat(auto-fit,minmax(365px,1fr));grid-row-gap:16px;grid-column-gap:16px;justify-content:center;background:var(--v-background-base);will-change:transform}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels[data-v-0185e37f]{grid-row-gap:8px}}@media(hover:none){.channels-container .channels-list .channels[data-v-0185e37f]{content-visibility:auto;contain-intrinsic-height:2000px}}@media(min-width:1630px){.channels-container .channels-list .channels[data-v-0185e37f]{grid-template-columns:repeat(auto-fit,445px)}}.channels-container .channels-list .channels.channels--length-0.channels--tab-ピン留め[data-v-0185e37f]{display:flex;min-height:calc(100vh - 180px);min-height:calc(100dvh - 180px)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels.channels--length-0.channels--tab-ピン留め[data-v-0185e37f]{min-height:calc(100vh - 66px);min-height:calc(100dvh - 66px)}}@media(min-width:1008px){.channels-container .channels-list .channels.channels--length-1[data-v-0185e37f]{margin-right:calc(50% + 8px)}}@media(min-width:1389px){.channels-container .channels-list .channels.channels--length-1[data-v-0185e37f]{margin-right:calc(66.66667% + 10.66667px)}}@media(min-width:1630px){.channels-container .channels-list .channels.channels--length-1[data-v-0185e37f]{margin-right:922px}}@media(min-width:2090px){.channels-container .channels-list .channels.channels--length-1[data-v-0185e37f]{margin-right:1383px}}@media(min-width:1389px){.channels-container .channels-list .channels.channels--length-2[data-v-0185e37f]{margin-right:calc(33.33333% + 5.33333px)}}@media(min-width:1630px){.channels-container .channels-list .channels.channels--length-2[data-v-0185e37f]{margin-right:461px}}@media(min-width:2090px){.channels-container .channels-list .channels.channels--length-2[data-v-0185e37f]{margin-right:922px}.channels-container .channels-list .channels.channels--length-3[data-v-0185e37f]{margin-right:461px}}.channels-container .channels-list .channels .channel[data-v-0185e37f]{display:flex;flex-direction:column;position:relative;height:275px;padding:20px 20px;border-radius:16px;color:var(--v-text-base);background:var(--v-background-lighten1);transition:background-color .15s;overflow:hidden;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;content-visibility:auto;contain-intrinsic-height:235px}@media(max-width:1007.9px){.channels-container .channels-list .channels .channel[data-v-0185e37f]{height:auto}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel[data-v-0185e37f]{padding:12px 14px;padding-top:10px;height:auto;border-radius:11px;contain-intrinsic-height:125px}}.channels-container .channels-list .channels .channel[data-v-0185e37f]:hover{background:var(--v-background-lighten2)}@media(hover:none){.channels-container .channels-list .channels .channel[data-v-0185e37f]:hover{background:var(--v-background-lighten1)}}.channels-container .channels-list .channels .channel .channel__broadcaster[data-v-0185e37f]{display:flex;height:44px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster[data-v-0185e37f]{height:29px}}.channels-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-0185e37f]{display:inline-block;flex-shrink:0;width:80px;height:44px;border-radius:5px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-0185e37f]{width:54px;height:29px;border-radius:4px}}.channels-container .channels-list .channels .channel .channel__broadcaster-content[data-v-0185e37f]{display:flex;flex-direction:column;margin-left:16px;width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-content[data-v-0185e37f]{align-items:center;flex-direction:row;margin-left:12px;margin-right:6px}}.channels-container .channels-list .channels .channel .channel__broadcaster-name[data-v-0185e37f]{flex-shrink:0;font-size:18px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-name[data-v-0185e37f]{font-size:15px}}.channels-container .channels-list .channels .channel .channel__broadcaster-status[data-v-0185e37f]{display:flex;flex-shrink:0;align-items:center;margin-top:2px;font-size:12px;color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status[data-v-0185e37f]{margin-top:3px;margin-left:auto;font-size:12px}}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force[data-v-0185e37f],.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers[data-v-0185e37f]{display:flex;align-items:center}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status-force span[data-v-0185e37f]:nth-child(2),.channels-container .channels-list .channels .channel .channel__broadcaster-status-force span[data-v-0185e37f]:nth-child(4),.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers span[data-v-0185e37f]:nth-child(2),.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers span[data-v-0185e37f]:nth-child(4){display:none}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers[data-v-0185e37f]{margin-left:8px!important}}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--festival[data-v-0185e37f]{color:#e7556e}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--so-many[data-v-0185e37f]{color:#e76b55}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--many[data-v-0185e37f]{color:#e7a355}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-0185e37f]{display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;top:-5px;right:-5px;width:34px;height:34px;padding:4px;color:var(--v-text-darken1);border-radius:50%;transition:color .15s ease,background-color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-0185e37f]{top:-1px}}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-0185e37f]:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor;color:inherit;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.6,1);pointer-events:none}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-0185e37f]:hover{color:var(--v-text-base)}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-0185e37f]:hover:before{opacity:.15}.channels-container .channels-list .channels .channel .channel__broadcaster-pin--pinned[data-v-0185e37f]{color:var(--v-primary-base)}.channels-container .channels-list .channels .channel .channel__broadcaster-pin--pinned[data-v-0185e37f]:hover{color:var(--v-primary-lighten1)}.channels-container .channels-list .channels .channel .channel__program-present[data-v-0185e37f]{display:flex;flex-direction:column}.channels-container .channels-list .channels .channel .channel__program-present-title-wrapper[data-v-0185e37f]{display:block;margin-top:14px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-title-wrapper[data-v-0185e37f]{display:flex;align-items:center;margin-top:10px}}.channels-container .channels-list .channels .channel .channel__program-present-title[data-v-0185e37f]{display:-webkit-box;font-size:16px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-title[data-v-0185e37f]{font-size:14.5px;-webkit-line-clamp:1}}.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-0185e37f]{margin-top:4px;color:var(--v-text-darken1);font-size:13.5px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-0185e37f]{flex-shrink:0;margin-top:0;margin-left:auto;padding-left:10px;font-size:12px}}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-0185e37f]{font-size:11px;padding-left:6px}}.channels-container .channels-list .channels .channel .channel__program-present-description[data-v-0185e37f]{display:-webkit-box;margin-top:8px;color:var(--v-text-darken1);font-size:10.5px;line-height:175%;overflow-wrap:break-word;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:3;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-description[data-v-0185e37f]{margin-top:6px;font-size:10px;-webkit-line-clamp:2}}.channels-container .channels-list .channels .channel .channel__program-following[data-v-0185e37f]{display:flex;flex-direction:column;color:var(--v-text-base);font-size:12.5px}@media(max-width:1007.9px){.channels-container .channels-list .channels .channel .channel__program-following[data-v-0185e37f]{margin-top:6px}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following[data-v-0185e37f]{flex-direction:row;margin-top:6px;font-size:12px}}.channels-container .channels-list .channels .channel .channel__program-following-title[data-v-0185e37f]{display:flex;align-items:center;min-width:0}.channels-container .channels-list .channels .channel .channel__program-following-title-decorate[data-v-0185e37f]{flex-shrink:0;font-weight:700}.channels-container .channels-list .channels .channel .channel__program-following-title-icon[data-v-0185e37f]{flex-shrink:0;margin-left:3px}.channels-container .channels-list .channels .channel .channel__program-following-title-text[data-v-0185e37f]{margin-left:2px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-0185e37f]{color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-0185e37f]{flex-shrink:0;margin-left:auto;padding-left:8px;font-size:11.5px}}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-0185e37f]{font-size:11px;padding-left:6px}}.channels-container .channels-list .channels .channel .channel__progressbar[data-v-0185e37f]{position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--v-gray-base)}.channels-container .channels-list .channels .channel .channel__progressbar-progress[data-v-0185e37f]{height:4px;background:var(--v-primary-base);transition:width .3s}@media(max-width:850px)and (min-height:850.01px){.channels-container .pinned-container h2[data-v-0185e37f]{font-size:21px!important}.channels-container .pinned-container div[data-v-0185e37f]{font-size:12.5px!important;text-align:center}}@media(max-width:1000px)and (max-height:450px){.channels-container .pinned-container h2[data-v-0185e37f]{font-size:21px!important}.channels-container .pinned-container div[data-v-0185e37f]{font-size:13px!important;text-align:center}.channels-container .pinned-container div .mt-4[data-v-0185e37f]{margin-top:12px!important}}@media(max-width:680px)and (max-height:450px){.channels-container .pinned-container h2[data-v-0185e37f]{font-size:16px!important}.channels-container .pinned-container div[data-v-0185e37f]{font-size:10.5px!important}.channels-container .pinned-container div .mt-4[data-v-0185e37f]{margin-top:8px!important}}.channels-container.channels-container--watch .v-tabs-bar{height:42px;background:linear-gradient(to bottom,var(--v-background-base) calc(100% - 3px),var(--v-background-lighten1) 3px)}@media(max-width:1000px)and (max-height:450px){.channels-container.channels-container--watch .v-tabs-bar{height:44px}}.channels-container.channels-container--watch .v-tabs-bar .v-slide-group__next,.channels-container.channels-container--watch .v-tabs-bar .v-slide-group__prev{flex:auto!important;min-width:28px!important}.channels-container.channels-container--watch .v-tabs-slider-wrapper{height:3px!important;transition:left .3s cubic-bezier(.25,.8,.5,1)}:root .channels-container.channels-container--watch .v-window__container,_::-webkit-full-page-media,_:future{height:inherit!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--watch .v-window__container--is-active,_::-webkit-full-page-media,_:future{display:none!important}:root .channels-container.channels-container--watch .v-window__container .v-window-item,_::-webkit-full-page-media,_:future{display:none!important;position:static!important;transform:none!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--watch .v-window__container .v-window-item--active,_::-webkit-full-page-media,_:future{display:block!important}.channels-container[data-v-f0f56584]{display:flex;flex-direction:column}.channels-container .channels-tab[data-v-f0f56584]{position:sticky;flex:none;top:0;padding-left:16px;padding-right:16px;padding-bottom:14px;background:var(--v-background-base);z-index:1}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab[data-v-f0f56584]{padding-bottom:8px;margin-top:0}}.channels-container .channels-tab .channels-tab__item[data-v-f0f56584]{min-width:72px!important;padding:0 8px;color:var(--v-text-base)!important;font-size:15px;text-transform:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab .channels-tab__item[data-v-f0f56584]{font-size:14.5px}}.channels-container .channels-list-container[data-v-f0f56584]{overflow-y:auto}.channels-container .channels-list-container .channels-list[data-v-f0f56584]{padding-left:16px;padding-right:10px;padding-bottom:16px;background:transparent!important;overflow:visible!important}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list[data-v-f0f56584]{padding-bottom:12px}}.channels-container .channels-list-container .channels-list .channels[data-v-f0f56584]{display:flex;justify-content:center;flex-direction:column;will-change:transform}@media(hover:none){.channels-container .channels-list-container .channels-list .channels[data-v-f0f56584]{content-visibility:auto;contain-intrinsic-size:319.3px 2000px}}@media(hover:none)and (max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels[data-v-f0f56584]{contain-intrinsic-size:277.3px 2000px}}@media(min-width:1630px){.channels-container .channels-list-container .channels-list .channels[data-v-f0f56584]{grid-template-columns:repeat(auto-fit,445px)}}.channels-container .channels-list-container .channels-list .channels .channel[data-v-f0f56584]{display:flex;flex-direction:column;position:relative;margin-top:12px;padding:10px 12px 14px 12px;border-radius:10px;color:var(--v-text-base);background:var(--v-background-lighten1);transition:background-color .15s;overflow:hidden;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;content-visibility:auto;contain-intrinsic-size:295.3px 137.3px}.channels-container .channels-list-container .channels-list .channels .channel[data-v-f0f56584]:first-of-type{margin-top:0}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel[data-v-f0f56584]{margin-top:8px;padding:8px 12px 12px 12px;contain-intrinsic-size:253.3px 107.2px}}.channels-container .channels-list-container .channels-list .channels .channel[data-v-f0f56584]:hover{background:var(--v-background-lighten2)}@media(hover:none){.channels-container .channels-list-container .channels-list .channels .channel[data-v-f0f56584]:hover{background:var(--v-background-lighten1)}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster[data-v-f0f56584]{display:flex;height:28px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster[data-v-f0f56584]{height:24px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-f0f56584]{display:inline-block;flex-shrink:0;width:48px;height:100%;border-radius:4px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-f0f56584]{width:46px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-content[data-v-f0f56584]{display:flex;align-items:center;margin-left:12px;width:100%;min-width:0}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-name[data-v-f0f56584]{font-size:14.5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force[data-v-f0f56584]{display:flex;align-items:center;flex-shrink:0;margin-top:2px;margin-left:auto;padding-left:6px;font-size:12px;color:var(--v-text-darken1)}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--festival[data-v-f0f56584]{color:#e7556e}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--so-many[data-v-f0f56584]{color:#e76b55}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--many[data-v-f0f56584]{color:#e7a355}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present[data-v-f0f56584]{display:flex;flex-direction:column}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-title[data-v-f0f56584]{display:-webkit-box;margin-top:8px;font-size:13.5px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-title[data-v-f0f56584]{margin-top:6px;font-size:13px;-webkit-line-clamp:1}}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-time[data-v-f0f56584]{margin-top:4px;color:var(--v-text-darken1);font-size:11.5px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-time[data-v-f0f56584]{font-size:11px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following[data-v-f0f56584]{display:flex;flex-direction:column;margin-top:4px;color:var(--v-text-darken1);font-size:11.5px}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title[data-v-f0f56584]{display:flex;align-items:center}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-decorate[data-v-f0f56584]{flex-shrink:0}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-icon[data-v-f0f56584]{flex-shrink:0;margin-left:3px}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-text[data-v-f0f56584]{margin-left:2px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-time[data-v-f0f56584]{margin-top:1px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-time[data-v-f0f56584]{font-size:10px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__progressbar[data-v-f0f56584]{position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--v-gray-base)}.channels-container .channels-list-container .channels-list .channels .channel .channel__progressbar-progress[data-v-f0f56584]{height:4px;background:var(--v-primary-base);transition:width .3s}.settings__item[data-v-7179f41e]{display:flex;position:relative;flex-direction:column;margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings__item[data-v-7179f41e]{margin-top:16px}}.settings__item--switch[data-v-7179f41e]{margin-right:62px}.settings__item-heading[data-v-7179f41e]{display:flex;align-items:center;color:var(--v-text-base);font-size:16.5px}@media(max-width:1000px)and (max-height:450px){.settings__item-heading[data-v-7179f41e]{font-size:15px}}.settings__item-label[data-v-7179f41e]{margin-top:8px;color:var(--v-text-darken1);font-size:13.5px;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.settings__item-label[data-v-7179f41e]{font-size:11px;line-height:1.7}}.settings__item-form[data-v-7179f41e]{margin-top:14px}@media(max-width:1000px)and (max-height:450px){.settings__item-form[data-v-7179f41e]{font-size:13.5px}}.settings__item-switch[data-v-7179f41e]{align-items:center;position:absolute;top:0;right:-74px;bottom:0;margin-top:0}.settings__item p[data-v-7179f41e]{margin-bottom:8px}.settings__item p[data-v-7179f41e]:last-of-type{margin-bottom:0}.muted-comment-items[data-v-7179f41e]{display:flex;flex-direction:column;margin-top:8px}.muted-comment-items .muted-comment-item[data-v-7179f41e]{display:flex;align-items:center;padding:6px 0;border-bottom:1px solid var(--v-background-lighten2);transition:background-color .15s ease}.muted-comment-items .muted-comment-item[data-v-7179f41e]:last-of-type{border-bottom:none}.muted-comment-items .muted-comment-item__input[data-v-7179f41e]{font-size:14px}.muted-comment-items .muted-comment-item__match-type[data-v-7179f41e]{max-width:150px;margin-left:12px;font-size:14px}.muted-comment-items .muted-comment-item__delete-button[data-v-7179f41e]{display:flex;align-items:center;justify-content:center;width:40px;height:40px;margin-left:6px;border-radius:5px;outline:none;cursor:pointer}.comment-container[data-v-23e84f03]{display:flex;flex-direction:column}.comment-container .comment-header[data-v-23e84f03]{display:flex;align-items:center;flex-shrink:0;width:100%;height:26px;padding-left:16px;padding-right:16px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header[data-v-23e84f03]{margin-top:12px}}.comment-container .comment-header__title[data-v-23e84f03]{display:flex;align-items:center;font-size:18.5px;font-weight:700;line-height:145%}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header__title[data-v-23e84f03]{font-size:16.5px}}.comment-container .comment-header__title-icon[data-v-23e84f03]{margin-bottom:-3px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header__title-icon[data-v-23e84f03]{height:17.5px}}.comment-container .comment-header__title-text[data-v-23e84f03]{margin-left:12px}.comment-container .comment-header__button[data-v-23e84f03]{display:flex;align-items:center;height:26px;padding:0 9px;border-radius:4px;background:var(--v-background-lighten3);font-size:11px;line-height:1.8;letter-spacing:0}.comment-container .comment-list-wrapper[data-v-23e84f03]{position:relative;width:100%;height:100%;min-height:0;margin-top:16px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper[data-v-23e84f03]{margin-top:12px}}.comment-container .comment-list-wrapper .comment-list-dropdown[data-v-23e84f03]{display:inline-block;position:absolute;top:var(--comment-list-dropdown-top,0);right:16px;border-radius:4px;overflow-x:hidden;overflow-y:auto;box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);opacity:0;visibility:hidden;transition:opacity .15s ease,visibility .15s ease;z-index:8}.comment-container .comment-list-wrapper .comment-list-dropdown--display[data-v-23e84f03]{opacity:1;visibility:visible}.comment-container .comment-list-wrapper .comment-list-cover[data-v-23e84f03]{display:none;position:absolute;top:0;left:0;width:100%;height:100%;z-index:7}.comment-container .comment-list-wrapper .comment-list-cover--display[data-v-23e84f03]{display:block}.comment-container .comment-list-wrapper .comment-list[data-v-23e84f03]{width:100%;height:100%;padding-left:16px;padding-right:10px;padding-bottom:12px;overflow-y:scroll!important}.comment-container .comment-list-wrapper .comment-list .comment[data-v-23e84f03]{display:flex;position:relative;align-items:center;min-height:28px;padding-top:6px;word-break:break-all}.comment-container .comment-list-wrapper .comment-list .comment--my-post[data-v-23e84f03]{color:var(--v-secondary-lighten2)}.comment-container .comment-list-wrapper .comment-list .comment__text[data-v-23e84f03]{font-size:13px}.comment-container .comment-list-wrapper .comment-list .comment__time[data-v-23e84f03]{flex-shrink:0;margin-left:auto;padding-left:8px;color:var(--v-text-darken1);font-size:13px}.comment-container .comment-list-wrapper .comment-list .comment__icon[data-v-23e84f03]{width:20px;height:20px;margin-left:8px;border-radius:5px;cursor:pointer}.comment-container .comment-list-wrapper .comment-announce[data-v-23e84f03]{display:flex;align-items:center;justify-content:center;flex-direction:column;position:absolute;top:0;left:0;width:100%;height:100%;padding-left:12px;padding-right:12px}.comment-container .comment-list-wrapper .comment-announce__heading[data-v-23e84f03]{font-size:20px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper .comment-announce__heading[data-v-23e84f03]{font-size:16px}}.comment-container .comment-list-wrapper .comment-announce__text[data-v-23e84f03]{margin-top:12px;color:var(--v-text-darken1);font-size:13.5px;text-align:center}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper .comment-announce__text[data-v-23e84f03]{font-size:12px}}.comment-container .comment-scroll-button[data-v-23e84f03]{display:flex;align-items:center;justify-content:center;position:absolute;left:0;right:0;bottom:22px;width:42px;height:42px;margin:0 auto;border-radius:50%;background:var(--v-primary-base);transition:background-color .15s,opacity .3s,visibility .3s;visibility:hidden;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}.comment-container .comment-scroll-button--display[data-v-23e84f03]{opacity:1;visibility:visible}.program-container[data-v-3c7f1e0c]{padding-left:16px;padding-right:16px;overflow-y:auto}.program-container .program-broadcaster[data-v-3c7f1e0c]{display:none;align-items:center;min-width:0}@media(max-width:1000px)and (max-height:450px){.program-container .program-broadcaster[data-v-3c7f1e0c]{display:flex;margin-top:16px}}.program-container .program-broadcaster__icon[data-v-3c7f1e0c]{display:inline-block;flex-shrink:0;width:43px;height:24px;border-radius:3px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.program-container .program-broadcaster__icon[data-v-3c7f1e0c]{width:42px;height:23.5px}}.program-container .program-broadcaster__number[data-v-3c7f1e0c]{flex-shrink:0;margin-left:12px;font-size:16.5px}.program-container .program-broadcaster__name[data-v-3c7f1e0c]{margin-left:5px;font-size:16.5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.program-container .program-info .program-info__title[data-v-3c7f1e0c]{font-size:22px;font-weight:700;line-height:145%;font-feature-settings:"palt" 1;letter-spacing:.05em}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__title[data-v-3c7f1e0c]{margin-top:10px;font-size:18px}}.program-container .program-info .program-info__time[data-v-3c7f1e0c]{margin-top:8px;color:var(--v-text-darken1);font-size:14px}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__time[data-v-3c7f1e0c]{font-size:13px}}.program-container .program-info .program-info__description[data-v-3c7f1e0c]{margin-top:12px;color:var(--v-text-darken1);font-size:12px;line-height:168%;overflow-wrap:break-word;font-feature-settings:"palt" 1;letter-spacing:.08em}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__description[data-v-3c7f1e0c]{margin-top:8px;font-size:11px}}.program-container .program-info .program-info__genre-container[data-v-3c7f1e0c]{display:flex;flex-wrap:wrap;margin-top:10px}.program-container .program-info .program-info__genre-container .program-info__genre[data-v-3c7f1e0c]{display:inline-block;font-size:10.5px;padding:3px;margin-top:4px;margin-right:4px;border-radius:4px;background:var(--v-background-lighten2)}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__genre-container .program-info__genre[data-v-3c7f1e0c]{font-size:9px}}.program-container .program-info .program-info__next[data-v-3c7f1e0c]{display:flex;align-items:center;margin-top:18px;color:var(--v-text-base);font-size:14px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__next[data-v-3c7f1e0c]{margin-top:14px;font-size:13px}}.program-container .program-info .program-info__next-decorate[data-v-3c7f1e0c]{flex-shrink:0}.program-container .program-info .program-info__next-icon[data-v-3c7f1e0c]{flex-shrink:0;margin-left:3px;font-size:15px}.program-container .program-info .program-info__next-title[data-v-3c7f1e0c]{display:-webkit-box;margin-top:2px;color:var(--v-text-base);font-size:14px;font-weight:700;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__next-title[data-v-3c7f1e0c]{font-size:13px}}.program-container .program-info .program-info__next-time[data-v-3c7f1e0c]{margin-top:3px;color:var(--v-text-darken1);font-size:13.5px}.program-container .program-info .program-info__status[data-v-3c7f1e0c]{display:flex;align-items:center;margin-top:16px;font-size:14px;color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__status[data-v-3c7f1e0c]{margin-top:10px;font-size:12px}}.program-container .program-info .program-info__status-force[data-v-3c7f1e0c],.program-container .program-info .program-info__status-viewers[data-v-3c7f1e0c]{display:flex;align-items:center}.program-container .program-info .program-info__status-force--festival[data-v-3c7f1e0c]{color:#e7556e}.program-container .program-info .program-info__status-force--so-many[data-v-3c7f1e0c]{color:#e76b55}.program-container .program-info .program-info__status-force--many[data-v-3c7f1e0c]{color:#e7a355}.program-container .program-detail-container[data-v-3c7f1e0c]{margin-top:24px;margin-bottom:24px}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container[data-v-3c7f1e0c]{margin-top:20px;margin-bottom:16px}}.program-container .program-detail-container .program-detail[data-v-3c7f1e0c]{margin-top:16px}.program-container .program-detail-container .program-detail .program-detail__heading[data-v-3c7f1e0c]{font-size:18px}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container .program-detail .program-detail__heading[data-v-3c7f1e0c]{font-size:16px}}.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c]{margin-top:8px;color:var(--v-text-darken1);font-size:12px;line-height:168%;overflow-wrap:break-word;white-space:pre-wrap;font-feature-settings:"palt" 1;letter-spacing:.08em}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c]{font-size:11px}}.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c] a:link,.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c] a:visited{color:var(--v-primary-lighten1);text-underline-offset:3px}@media(max-width:1000px)and (max-height:450px){.zoom-capture-modal-container.v-dialog{width:auto!important;max-width:auto!important;aspect-ratio:16/9}}.zoom-capture-modal[data-v-4a0651e4]{position:relative}.zoom-capture-modal__image[data-v-4a0651e4]{display:block;width:100%;border-radius:11px}.zoom-capture-modal__download[data-v-4a0651e4]{display:flex;position:absolute;align-items:center;justify-content:center;right:22px;bottom:20px;width:80px;height:80px;border-radius:50%;color:var(--v-text-base);filter:drop-shadow(0 0 4.5px rgba(0,0,0,.9))}.twitter-container[data-v-4a0651e4]{display:flex;flex-direction:column;position:relative;padding-bottom:8px}.twitter-container.watch-panel__content--active .tab-container .tab-content--active[data-v-4a0651e4]{opacity:1;visibility:visible}@media(hover:none){.twitter-container.watch-panel__content--active .tab-container .tab-content--active[data-v-4a0651e4]{content-visibility:auto}}.twitter-container .tab-container[data-v-4a0651e4]{flex-grow:1;min-height:0}.twitter-container .tab-container .tab-content[data-v-4a0651e4]{position:relative;height:100%;transition:opacity .2s,visibility .2s;opacity:0;visibility:hidden;overflow-y:scroll}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content[data-v-4a0651e4]{padding-top:8px}}@media(hover:none){.twitter-container .tab-container .tab-content[data-v-4a0651e4]{transition:none;content-visibility:hidden}}.twitter-container .tab-container .tab-content .captures[data-v-4a0651e4]{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:12px;grid-column-gap:12px;padding-left:12px;padding-right:5px;max-height:100%}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .captures[data-v-4a0651e4]{grid-row-gap:8px;grid-column-gap:8px}}.twitter-container .tab-container .tab-content .captures .capture[data-v-4a0651e4]{position:relative;height:82px;border-radius:11px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer;content-visibility:auto}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .captures .capture[data-v-4a0651e4]{height:74px}}.twitter-container .tab-container .tab-content .captures .capture__image[data-v-4a0651e4]{display:block;width:100%;height:100%}.twitter-container .tab-container .tab-content .captures .capture__zoom[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;position:absolute;top:1px;right:3px;width:38px;height:38px;border-radius:50%;filter:drop-shadow(0 0 2.5px rgba(0,0,0,.9));cursor:pointer}.twitter-container .tab-container .tab-content .captures .capture__disabled-cover[data-v-4a0651e4],.twitter-container .tab-container .tab-content .captures .capture__selected-number[data-v-4a0651e4]{display:none;align-items:center;justify-content:center;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(30,19,16,.5)}.twitter-container .tab-container .tab-content .captures .capture__selected-number[data-v-4a0651e4]{font-size:38px;text-shadow:0 0 2.5px rgba(0,0,0,.9)}.twitter-container .tab-container .tab-content .captures .capture__selected-checkmark[data-v-4a0651e4]{display:none;position:absolute;top:6px;left:7px;width:20px;height:20px;color:var(--v-primary-base)}.twitter-container .tab-container .tab-content .captures .capture__selected-border[data-v-4a0651e4]{display:none;position:absolute;top:0;left:0;right:0;bottom:0;border-radius:11px;border:4px solid var(--v-primary-base)}.twitter-container .tab-container .tab-content .captures .capture__focused-border[data-v-4a0651e4]{display:none;position:absolute;top:0;left:0;right:0;bottom:0;border-radius:11px;border:4px solid var(--v-secondary-base)}.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-border[data-v-4a0651e4],.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-checkmark[data-v-4a0651e4],.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-number[data-v-4a0651e4]{display:flex}.twitter-container .tab-container .tab-content .captures .capture--focused .capture__focused-border[data-v-4a0651e4]{display:block}.twitter-container .tab-container .tab-content .captures .capture--disabled[data-v-4a0651e4]{cursor:auto}.twitter-container .tab-container .tab-content .captures .capture--disabled .capture__disabled-cover[data-v-4a0651e4]{display:block}.twitter-container .tab-container .tab-content .capture-announce[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;flex-direction:column;height:100%;padding-left:12px;padding-right:5px}.twitter-container .tab-container .tab-content .capture-announce__heading[data-v-4a0651e4]{font-size:20px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .capture-announce__heading[data-v-4a0651e4]{font-size:16px}}.twitter-container .tab-container .tab-content .capture-announce__text[data-v-4a0651e4]{margin-top:12px;color:var(--v-text-darken1);font-size:13.5px;text-align:center}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .capture-announce__text[data-v-4a0651e4]{font-size:12px}}.twitter-container .tab-button-container[data-v-4a0651e4]{display:flex;flex-shrink:0;-moz-column-gap:7px;column-gap:7px;height:40px;margin-left:12px;margin-right:12px;padding-top:8px;padding-bottom:6px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-button-container[data-v-4a0651e4]{height:38px;margin-left:8px;margin-right:8px}}.twitter-container .tab-button-container .tab-button[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;flex:1;background:var(--v-background-lighten2);border-radius:7px;font-size:11px;transition:background-color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-button-container .tab-button[data-v-4a0651e4]{font-size:10.5px}}.twitter-container .tab-button-container .tab-button--active[data-v-4a0651e4]{background:var(--v-twitter-base)}.twitter-container .tab-button-container .tab-button__text[data-v-4a0651e4]{margin-left:4px;margin-right:2px;line-height:2}.twitter-container .tweet-form[data-v-4a0651e4]{display:flex;flex-direction:column;flex-shrink:0;height:136px;margin-left:12px;margin-right:12px;border-radius:12px;border-bottom-left-radius:7px;border-bottom-right-radius:7px;background:var(--v-background-lighten1);transition:box-shadow .09s ease}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form[data-v-4a0651e4]{height:96px;margin-left:8px;margin-right:8px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}}.twitter-container .tweet-form--focused[data-v-4a0651e4]{box-shadow:0 0 0 3.5px rgba(79,130,230,.6)}.twitter-container .tweet-form--virtual-keyboard-display[data-v-4a0651e4]{position:relative;bottom:calc(env(keyboard-inset-height, 0px) - 77px)}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form--virtual-keyboard-display[data-v-4a0651e4]{bottom:calc(env(keyboard-inset-height, 0px) - 56px)}}.twitter-container .tweet-form__hashtag[data-v-4a0651e4]{display:flex;align-items:center;height:19px;margin-top:12px;margin-left:12px;margin-right:12px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__hashtag[data-v-4a0651e4]{height:16px;margin-top:8px}}.twitter-container .tweet-form__hashtag-form[data-v-4a0651e4]{display:block;height:100%;flex-grow:1;font-size:12.5px;color:var(--v-twitter-lighten2);outline:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__hashtag-form[data-v-4a0651e4]{font-size:12px}}.twitter-container .tweet-form__hashtag-form[data-v-4a0651e4]::-moz-placeholder{color:rgba(65,165,241,.6)}.twitter-container .tweet-form__hashtag-form[data-v-4a0651e4]::placeholder{color:rgba(65,165,241,.6)}.twitter-container .tweet-form__hashtag-list-button[data-v-4a0651e4]{display:flex;position:relative;align-items:center;justify-content:center;right:-8px;width:34px;height:34px;padding:6px;border-radius:50%;color:var(--v-twitter-lighten2);cursor:pointer}.twitter-container .tweet-form__textarea[data-v-4a0651e4]{display:block;flex-grow:1;margin-top:8px;margin-left:12px;margin-right:12px;font-size:12.5px;color:var(--v-text-base);word-break:break-all;resize:none;outline:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__textarea[data-v-4a0651e4]{margin-top:6px;font-size:12px}}.twitter-container .tweet-form__textarea[data-v-4a0651e4]::-moz-placeholder{color:var(--v-text-darken2)}.twitter-container .tweet-form__textarea[data-v-4a0651e4]::placeholder{color:var(--v-text-darken2)}.twitter-container .tweet-form__control[data-v-4a0651e4]{display:flex;align-items:center;height:32px;margin-top:6px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control[data-v-4a0651e4]{height:26px}}.twitter-container .tweet-form__control .account-button[data-v-4a0651e4]{display:flex;align-items:center;width:183px;height:100%;border-radius:7px;font-size:13px;color:var(--v-text-base);background:var(--v-background-lighten2);-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .account-button[data-v-4a0651e4]{width:156px;border-radius:5px;font-size:11px}}.twitter-container .tweet-form__control .account-button--no-login .account-button__screen-name[data-v-4a0651e4]{font-weight:500}.twitter-container .tweet-form__control .account-button--no-login .account-button__menu[data-v-4a0651e4]{display:none}.twitter-container .tweet-form__control .account-button__icon[data-v-4a0651e4]{display:block;width:32px;height:100%;border-radius:7px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2))}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .account-button__icon[data-v-4a0651e4]{width:26px}}.twitter-container .tweet-form__control .account-button__screen-name[data-v-4a0651e4]{flex-grow:1;line-height:2;text-align:center;font-weight:700}.twitter-container .tweet-form__control .account-button__menu[data-v-4a0651e4]{margin-right:4px}.twitter-container .tweet-form__control .limit-meter[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;flex-direction:column;flex-grow:1;row-gap:.5px;font-size:10px;color:var(--v-text-darken1);-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .limit-meter[data-v-4a0651e4]{font-size:9px}}.twitter-container .tweet-form__control .limit-meter__content[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .limit-meter__content[data-v-4a0651e4]:nth-child(2){margin-top:-2.5px}}.twitter-container .tweet-form__control .limit-meter__content svg[data-v-4a0651e4]{width:14px}.twitter-container .tweet-form__control .limit-meter__content span[data-v-4a0651e4]{width:16px;margin-left:5px;text-align:center;font-weight:700}.twitter-container .tweet-form__control .limit-meter__content--yellow[data-v-4a0651e4]{color:var(--v-warning-base)}.twitter-container .tweet-form__control .limit-meter__content--red[data-v-4a0651e4]{color:var(--v-error-base)}.twitter-container .tweet-form__control .tweet-button[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;width:94px;height:100%;border-radius:7px;font-size:12.5px;line-height:2;color:var(--v-text-base);background:var(--v-twitter-base);-webkit-user-select:none;-moz-user-select:none;user-select:none;outline:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .tweet-button[data-v-4a0651e4]{width:86px;border-radius:5px;font-size:11.8px}}.twitter-container .tweet-form__control .tweet-button[disabled][data-v-4a0651e4]{opacity:.7;cursor:auto}.twitter-container .twitter-account-list[data-v-4a0651e4]{position:absolute;left:12px;right:12px;bottom:48px;max-height:calc(100vh - 137px);max-height:calc(100dvh - 137px);border-radius:7px;-webkit-clip-path:inset(0 0 0 0 round 7px);clip-path:inset(0 0 0 0 round 7px);background:var(--v-background-lighten2);box-shadow:0 3px 4px rgba(0,0,0,.53);transition:opacity .2s ease,visibility .2s ease;opacity:0;visibility:hidden;overflow-y:auto;z-index:3}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list[data-v-4a0651e4]{left:8px;right:8px;bottom:40px;max-height:calc(100vh - 104px);max-height:calc(100dvh - 104px)}}.twitter-container .twitter-account-list--display[data-v-4a0651e4]{opacity:1;visibility:visible}.twitter-container .twitter-account-list[data-v-4a0651e4]::-webkit-scrollbar-track{background:var(--v-background-lighten2)}.twitter-container .twitter-account-list[data-v-4a0651e4]::-webkit-scrollbar-thumb,.twitter-container .twitter-account-list[data-v-4a0651e4]::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.twitter-container .twitter-account-list .twitter-account[data-v-4a0651e4]{display:flex;align-items:center;padding:12px 12px;border-radius:7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account[data-v-4a0651e4]{padding:8px 12px}}.twitter-container .twitter-account-list .twitter-account__icon[data-v-4a0651e4]{display:block;width:50px;height:50px;border-radius:50%}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__icon[data-v-4a0651e4]{width:36px;height:36px}}.twitter-container .twitter-account-list .twitter-account__info[data-v-4a0651e4]{display:flex;flex-direction:column;flex-grow:1;min-width:0;margin-left:12px}.twitter-container .twitter-account-list .twitter-account__name[data-v-4a0651e4]{font-size:17px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__name[data-v-4a0651e4]{font-size:14px;line-height:1.3}}.twitter-container .twitter-account-list .twitter-account__screen-name[data-v-4a0651e4]{color:var(--v-text-darken1);font-size:14px}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__screen-name[data-v-4a0651e4]{font-size:13px}}.twitter-container .twitter-account-list .twitter-account__check[data-v-4a0651e4]{flex-shrink:0;color:var(--v-twitter-lighten1)}.twitter-container .hashtag-list[data-v-4a0651e4]{position:absolute;left:12px;right:12px;bottom:149px;max-height:calc(100vh - 239px);max-height:calc(100dvh - 239px);padding:12px 4px;border-radius:7px;-webkit-clip-path:inset(0 0 0 0 round 7px);clip-path:inset(0 0 0 0 round 7px);background:var(--v-background-lighten2);box-shadow:0 3px 4px rgba(0,0,0,.53);transition:opacity .2s ease,visibility .2s ease;opacity:0;visibility:hidden;overflow-y:auto;z-index:2}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list[data-v-4a0651e4]{left:8px;right:8px;bottom:110px;max-height:calc(100vh - 174px);max-height:calc(100dvh - 174px);padding:8px 4px}}.twitter-container .hashtag-list--display[data-v-4a0651e4]{opacity:1;visibility:visible}.twitter-container .hashtag-list--virtual-keyboard-display[data-v-4a0651e4]{bottom:calc(env(keyboard-inset-height, 0px) - 74px)!important;max-height:calc(100vh - env(keyboard-inset-height, 0px) - 16px)!important;max-height:calc(100dvh - env(keyboard-inset-height, 0px) - 16px)!important}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list--virtual-keyboard-display[data-v-4a0651e4]{bottom:calc(env(keyboard-inset-height, 0px) - 48px)!important}}.twitter-container .hashtag-list[data-v-4a0651e4]::-webkit-scrollbar-track{background:var(--v-background-lighten2)}.twitter-container .hashtag-list[data-v-4a0651e4]::-webkit-scrollbar-thumb,.twitter-container .hashtag-list[data-v-4a0651e4]::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.twitter-container .hashtag-list .hashtag-heading[data-v-4a0651e4]{display:flex;align-items:center;font-weight:700;padding-left:8px;padding-right:4px}.twitter-container .hashtag-list .hashtag-heading__text[data-v-4a0651e4]{display:flex;align-items:center;flex-grow:1;font-size:14px}.twitter-container .hashtag-list .hashtag-heading__add-button[data-v-4a0651e4]{display:flex;align-items:center;font-size:13px;padding:4px 8px;border-radius:5px;outline:none;cursor:pointer}.twitter-container .hashtag-list .hashtag-container[data-v-4a0651e4]{display:flex;flex-direction:column}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-4a0651e4]{display:flex;align-items:center;padding-top:1.5px;padding-bottom:1.5px;padding-left:8px;padding-right:4px;border-radius:7px;transition:background-color .15s ease;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-4a0651e4]{padding-top:0;padding-bottom:0}}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-4a0651e4]:first-of-type{margin-top:6px}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-4a0651e4]:hover{background:hsla(0,0%,100%,.1)}@media(hover:none){.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-4a0651e4]:hover{background:transparent}}.twitter-container .hashtag-list .hashtag-container .hashtag--editing[data-v-4a0651e4]:hover{background:transparent}.twitter-container .hashtag-list .hashtag-container .hashtag--editing .hashtag__input[data-v-4a0651e4]{box-shadow:0 0 0 3.5px rgba(79,130,230,.6);cursor:text}.twitter-container .hashtag-list .hashtag-container .hashtag__input[data-v-4a0651e4]{display:block;flex-grow:1;border-radius:2px;color:var(--v-twitter-lighten2);font-size:12.5px;outline:none;cursor:pointer;transition:box-shadow .09s ease}.twitter-container .hashtag-list .hashtag-container .hashtag__edit-button[data-v-4a0651e4]{margin-left:4px}.twitter-container .hashtag-list .hashtag-container .hashtag__delete-button[data-v-4a0651e4],.twitter-container .hashtag-list .hashtag-container .hashtag__edit-button[data-v-4a0651e4],.twitter-container .hashtag-list .hashtag-container .hashtag__sort-handle[data-v-4a0651e4]{display:flex;align-items:center;justify-content:center;width:19px;height:27px;border-radius:5px;outline:none;cursor:pointer}.twitter-container .hashtag-list .hashtag-container .hashtag__sort-handle[data-v-4a0651e4]{cursor:move}.watch-player__dplayer svg circle,.watch-player__dplayer svg path{fill:var(--v-text-base)!important}.watch-player__dplayer .dplayer-video-wrap{background:transparent!important}.watch-player__dplayer .dplayer-video-wrap .dplayer-video-wrap-aspect{transition:opacity .4s cubic-bezier(.4,.38,.49,.94);opacity:1}.watch-player__dplayer .dplayer-video-wrap .dplayer-danmaku{max-width:100%;max-height:calc(100% - var(--comment-area-vertical-margin, 0px));aspect-ratio:var(--comment-area-aspect-ratio,16/9);transition:max-height .5s cubic-bezier(.42,.19,.53,.87),aspect-ratio .5s cubic-bezier(.42,.19,.53,.87);will-change:aspect-ratio;overflow:hidden}.watch-player__dplayer .dplayer-video-wrap .dplayer-danloading,.watch-player__dplayer .dplayer-video-wrap .dplayer-loading-icon{display:none!important}.watch-player__dplayer .dplayer-controller-mask{height:82px!important;background:linear-gradient(to bottom,transparent,var(--v-background-base))!important;opacity:0!important;visibility:hidden;transition:opacity .3s ease,visibility .3s ease!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-controller-mask{height:66px!important}}.watch-player__dplayer .dplayer-controller{padding-left:86px!important;padding-bottom:6px!important;transition:opacity .3s ease,visibility .3s ease;opacity:0!important;visibility:hidden}.watch-player__dplayer .dplayer-controller .dplayer-live-badge,.watch-player__dplayer .dplayer-controller .dplayer-time{color:var(--v-text-base)!important}.watch-player__dplayer .dplayer-controller .dplayer-volume-bar{background:var(--v-text-base)!important}.watch-player__dplayer .dplayer-controller .dplayer-icons{bottom:auto!important}.watch-player__dplayer .dplayer-controller .dplayer-icons.dplayer-icons-right{right:22px!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-controller .dplayer-icons.dplayer-icons-right{right:11px!important}}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-full-in-icon{display:none!important}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon{transition:background-color .08s ease;border-radius:6px}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon.dplayer-capturing,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon.dplayer-capturing{background:var(--v-secondary-lighten1)}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon.dplayer-capturing .dplayer-icon-content,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon.dplayer-capturing .dplayer-icon-content{opacity:1}.watch-player__dplayer .dplayer-controller .dplayer-comment-box{transition:opacity .3s ease,visibility .3s ease!important}.watch-player__dplayer .dplayer-controller .dplayer-comment-box .dplayer-comment-input{transition:box-shadow .09s ease}.watch-player__dplayer .dplayer-controller .dplayer-comment-box .dplayer-comment-input:focus{box-shadow:0 0 0 3.5px rgba(79,130,230,.6)}.watch-player__dplayer .dplayer-notice{padding:16px 22px!important;margin-right:30px;border-radius:4px!important;font-size:15px!important;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-notice{padding:12px 16px!important;margin-right:16px;font-size:13.5px!important}}.watch-player__dplayer .dplayer-info-panel{transition:top .3s,left .3s}.watch-player__dplayer .dplayer-setting-box .dplayer-setting-audio-panel .dplayer-setting-audio-item.dplayer-setting-audio-item--disabled{pointer-events:none}.watch-player__dplayer .dplayer-setting-box .dplayer-setting-audio-panel .dplayer-setting-audio-item.dplayer-setting-audio-item--disabled .dplayer-label{color:#aaa}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-title{color:var(--v-text-base)}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-type span{border:1px solid --v-text-base}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-type input:checked+span{background:var(--v-text-base)}.watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:98px!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:74px!important}}.watch-player__dplayer.dplayer-mobile.dplayer-hide-controller .dplayer-controller{transform:none!important}.watch-player--loading .dplayer-video-wrap-aspect{opacity:0!important}:root .dplayer-icon:hover .dplayer-icon-content,_::-webkit-full-page-media,_:future{opacity:.8!important}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask{opacity:1!important;visibility:visible!important}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller .dplayer-comment-box,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask .dplayer-comment-box{left:88px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller .dplayer-comment-box,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask .dplayer-comment-box{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:98px;bottom:62px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-info-panel{top:82px;left:98px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-info-panel{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-comment-setting-box{left:88px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-comment-setting-box{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-mobile .dplayer-mobile-icon-wrap{opacity:.7!important;visibility:visible!important}.watch-container:not(.watch-container--control-display) .watch-player__dplayer .dplayer-danmaku{max-height:100%!important;aspect-ratio:16/9!important}.watch-container:not(.watch-container--control-display) .watch-player__dplayer .dplayer-notice{bottom:20px!important}.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-controller{padding-left:20px!important}.watch-container.watch-container--fullscreen .watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:30px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:16px!important}}.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-box,.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-setting-box{left:20px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-box,.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-setting-box{left:16px!important}}.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-info-panel,.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:30px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-info-panel,.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:16px!important}}.watch-player.watch-player--virtual-keyboard-display .watch-player__dplayer .dplayer-controller-mask{position:absolute;bottom:env(keyboard-inset-height,0)!important}.watch-player.watch-player--virtual-keyboard-display .watch-player__dplayer .dplayer-icons.dplayer-comment-box{position:absolute;bottom:calc(env(keyboard-inset-height, 0px) + 4px)!important}.shortcut-key[data-v-49a8f767]{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;min-width:32px;min-height:28px;padding:3px 8px;border-radius:5px;background-color:var(--v-background-lighten2);font-size:14.5px;text-align:center}.shortcut-key-plus[data-v-49a8f767]{display:inline-block;margin:0 5px;flex-shrink:0}.route-container[data-v-49a8f767]{height:100vh!important;height:100dvh!important;background:var(--v-black-base)!important;overflow:hidden}@supports(-webkit-touch-callout:none){.route-container[data-v-49a8f767]{height:-webkit-fill-available!important}}.watch-container[data-v-49a8f767]{display:flex;width:calc(100% + 352px);height:100%;transition:width .4s cubic-bezier(.26,.68,.55,.99)}@media(max-width:1000px)and (max-height:450px){.watch-container[data-v-49a8f767]{width:calc(100% + 310px)}}.watch-container.watch-container--control-display .watch-content[data-v-49a8f767]{cursor:auto!important}.watch-container.watch-container--control-display .watch-header[data-v-49a8f767],.watch-container.watch-container--control-display .watch-navigation[data-v-49a8f767],.watch-container.watch-container--control-display .watch-player__button[data-v-49a8f767]{opacity:1!important;visibility:visible!important}.watch-container.watch-container--panel-display[data-v-49a8f767]{width:100%}.watch-container.watch-container--panel-display .switch-button-panel .switch-button-icon[data-v-49a8f767]{color:var(--v-primary-base)}@media(hover:none){.watch-container.watch-container--panel-display .watch-panel[data-v-49a8f767]{content-visibility:auto}}.watch-container.watch-container--fullscreen .watch-navigation[data-v-49a8f767]{display:none}.watch-container.watch-container--fullscreen .watch-content .watch-header[data-v-49a8f767]{padding-left:30px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-content .watch-header[data-v-49a8f767]{padding-left:16px}}.watch-container .watch-navigation[data-v-49a8f767]{display:flex;flex-direction:column;position:fixed;width:68px;top:0;left:0;bottom:-100px;padding:18px 8px 122px;background:#2f221f80;transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden;z-index:2}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation[data-v-49a8f767]{width:56px;padding:18px 6px 122px}}.watch-container .watch-navigation .watch-navigation__icon[data-v-49a8f767]{display:flex;justify-content:center;align-items:center;height:52px;margin-bottom:17px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__icon[data-v-49a8f767]{height:32px;border-radius:10px}.watch-container .watch-navigation div.spacer[data-v-49a8f767]{display:none}}.watch-container .watch-navigation .watch-navigation__link[data-v-49a8f767]{display:flex;justify-content:center;align-items:center;height:52px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link[data-v-49a8f767]{height:44px;border-radius:10px}.watch-container .watch-navigation .watch-navigation__link[data-v-49a8f767]:last-child,.watch-container .watch-navigation .watch-navigation__link[data-v-49a8f767]:nth-last-child(2){display:none}}.watch-container .watch-navigation .watch-navigation__link[data-v-49a8f767]:hover{background:#433532a0}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link-icon[data-v-49a8f767]{width:26px;height:26px}}.watch-container .watch-navigation .watch-navigation__link--active[data-v-49a8f767]{color:var(--v-primary-base);background:#433532a0}.watch-container .watch-navigation .watch-navigation__link+.watch-navigation__link[data-v-49a8f767]{margin-top:4px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link+.watch-navigation__link[data-v-49a8f767]{margin-top:auto}}.watch-container .watch-content[data-v-49a8f767]{display:flex;position:relative;width:100%;cursor:none}.watch-container .watch-content .watch-header[data-v-49a8f767]{display:flex;align-items:center;position:absolute;top:0;left:0;width:100%;height:82px;padding-left:98px;padding-right:30px;background:linear-gradient(to bottom,var(--v-background-base),transparent);transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden;z-index:1}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header[data-v-49a8f767]{padding-left:84px;padding-right:16px;height:66px;padding-left:72px}}.watch-container .watch-content .watch-header .watch-header__broadcaster[data-v-49a8f767]{display:inline-block;flex-shrink:0;width:64px;height:36px;border-radius:5px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__broadcaster[data-v-49a8f767]{width:48px;height:28px;border-radius:4px}}.watch-container .watch-content .watch-header .watch-header__program-title[data-v-49a8f767]{margin-left:18px;font-size:18px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.05em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__program-title[data-v-49a8f767]{margin-left:12px;font-size:16px}}.watch-container .watch-content .watch-header .watch-header__program-time[data-v-49a8f767]{flex-shrink:0;margin-left:16px;font-size:15px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__program-time[data-v-49a8f767]{margin-left:8px;font-size:14px}}.watch-container .watch-content .watch-header .watch-header__now[data-v-49a8f767]{flex-shrink:0;margin-left:16px;font-size:13px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__now[data-v-49a8f767]{display:none}}.watch-container .watch-content .watch-player[data-v-49a8f767]{display:flex;position:relative;width:100%;height:100%;background-size:contain;background-position:50%}.watch-container .watch-content .watch-player .watch-player__background[data-v-49a8f767]{position:absolute;top:50%;left:50%;width:100%;max-height:100%;padding-top:min(56.25%,100vh);padding-top:min(56.25%,100dvh);aspect-ratio:16/9;background-blend-mode:overlay;background-color:rgba(14,14,18,.35);background-size:cover;background-image:none;transform:translate(-50%,-50%);opacity:0;visibility:hidden;transition:opacity .4s cubic-bezier(.4,.38,.49,.94),visibility .4s cubic-bezier(.4,.38,.49,.94);will-change:opacity}.watch-container .watch-content .watch-player .watch-player__background--display[data-v-49a8f767]{opacity:1;visibility:visible}.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-49a8f767]{display:inline-block;position:absolute;height:34px;right:56px;bottom:44px;filter:drop-shadow(0 0 5px var(--v-black-base))}@media(max-width:1264px)and (max-height:850px){.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-49a8f767]{height:30px;right:34px;bottom:30px}}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-49a8f767]{height:25px;right:30px;bottom:24px}}.watch-container .watch-content .watch-player .watch-player__buffering[data-v-49a8f767]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--v-background-lighten3);filter:drop-shadow(0 0 3px rgba(0,0,0,.3));opacity:0;visibility:hidden;transition:opacity .2s cubic-bezier(.4,.38,.49,.94),visibility .2s cubic-bezier(.4,.38,.49,.94);will-change:opacity;z-index:3}.watch-container .watch-content .watch-player .watch-player__buffering--display[data-v-49a8f767]{opacity:1;visibility:visible}.watch-container .watch-content .watch-player .watch-player__dplayer[data-v-49a8f767]{width:100%}.watch-container .watch-content .watch-player .watch-player__button[data-v-49a8f767]{display:flex;justify-content:space-around;flex-direction:column;position:absolute;top:50%;right:28px;height:190px;transform:translateY(-50%);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button[data-v-49a8f767]{right:15px;height:155px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-49a8f767]{display:flex;justify-content:center;align-items:center;width:48px;height:48px;color:var(--v-text-base);background:#2f221fc0;border-radius:7px;transition:background-color .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-49a8f767]{width:38px;height:38px;border-radius:5px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-49a8f767]:hover{background:#2f221ff0}@media(hover:none){.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-49a8f767]:hover{background:#2f221fc0}}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button .switch-button svg[data-v-49a8f767]{height:27px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button .switch-button-icon[data-v-49a8f767]{position:relative}.watch-container .watch-content .watch-player .watch-player__button .switch-button-up>.switch-button-icon[data-v-49a8f767]{top:6px}.watch-container .watch-content .watch-player .watch-player__button .switch-button-panel>.switch-button-icon[data-v-49a8f767]{top:1.5px;transition:color .4s cubic-bezier(.26,.68,.55,.99)}.watch-container .watch-content .watch-player .watch-player__button .switch-button-down>.switch-button-icon[data-v-49a8f767]{bottom:4px}.watch-container .watch-panel[data-v-49a8f767]{display:flex;flex-direction:column;flex-shrink:0;width:352px;height:100%;background:var(--v-background-base)}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel[data-v-49a8f767]{width:310px}}@media(hover:none){.watch-container .watch-panel[data-v-49a8f767]{content-visibility:hidden}}.watch-container .watch-panel .watch-panel__header[data-v-49a8f767]{display:flex;align-items:center;flex-shrink:0;width:100%;height:70px;padding-left:16px;padding-right:16px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header[data-v-49a8f767]{display:none}}.watch-container .watch-panel .watch-panel__header .panel-close-button[data-v-49a8f767]{display:flex;position:relative;align-items:center;flex-shrink:0;left:-4px;height:35px;padding:0 4px;border-radius:5px;font-size:16px;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-close-button[data-v-49a8f767]{font-size:14px}}.watch-container .watch-panel .watch-panel__header .panel-close-button__icon[data-v-49a8f767]{position:relative;left:-4px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-close-button__icon[data-v-49a8f767]{height:22px}}.watch-container .watch-panel .watch-panel__header .panel-close-button__text[data-v-49a8f767]{font-weight:700}.watch-container .watch-panel .watch-panel__header .panel-broadcaster[data-v-49a8f767]{display:flex;align-items:center;min-width:0;margin-left:16px}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__icon[data-v-49a8f767]{display:inline-block;flex-shrink:0;width:43px;height:24px;border-radius:3px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__icon[data-v-49a8f767]{width:38px;height:22px}}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__number[data-v-49a8f767]{flex-shrink:0;margin-left:8px;font-size:16px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__number[data-v-49a8f767]{font-size:14px}}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__name[data-v-49a8f767]{margin-left:5px;font-size:16px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__name[data-v-49a8f767]{font-size:14px}}.watch-container .watch-panel .watch-panel__content-container[data-v-49a8f767]{position:relative;height:100%}.watch-container .watch-panel .watch-panel__content-container .watch-panel__content[data-v-49a8f767]{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--v-background-base);transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden}@media(hover:none){.watch-container .watch-panel .watch-panel__content-container .watch-panel__content[data-v-49a8f767]{transition:none;content-visibility:hidden}}.watch-container .watch-panel .watch-panel__content-container .watch-panel__content--active[data-v-49a8f767]{opacity:1;visibility:visible;content-visibility:auto}.watch-container .watch-panel .watch-panel__navigation[data-v-49a8f767]{display:flex;align-items:center;justify-content:space-evenly;flex-shrink:0;height:77px;background:var(--v-background-lighten1)}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation[data-v-49a8f767]{height:56px}}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button[data-v-49a8f767]{display:flex;justify-content:center;align-items:center;flex-direction:column;width:77px;height:56px;padding:6px 0;border-radius:5px;color:var(--v-text-base);box-sizing:content-box;transition:color .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button[data-v-49a8f767]{height:42px;padding:6px 0 4px}}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button--active[data-v-49a8f767]{color:var(--v-primary-base)}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__icon[data-v-49a8f767]{height:30px}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__text[data-v-49a8f767]{margin-top:5px;font-size:13px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__text[data-v-49a8f767]{margin-top:2px;font-size:12px}}.settings-container[data-v-a67629d4]{width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.settings-container[data-v-a67629d4]{padding:16px 20px!important}}@media(max-width:680px)and (max-height:450px){.settings-container[data-v-a67629d4]{padding:16px 16px!important}}.settings-container .settings-navigation[data-v-a67629d4]{transform:none!important;visibility:visible!important}@media(max-width:1000px)and (max-height:450px){.settings-container .settings-navigation h1[data-v-a67629d4]{font-size:22px!important}.settings-container .settings-navigation .v-navigation-drawer__content .v-list[data-v-a67629d4]{margin-top:4px!important;padding:0!important}}@media(max-width:680px)and (max-height:450px){.settings-container .settings-navigation .v-navigation-drawer__content .v-list[data-v-a67629d4]{margin-top:0!important}}.settings-container .settings-navigation .v-list-item--link[data-v-a67629d4],.settings-container .settings-navigation .v-list-item--link[data-v-a67629d4]:before{border-radius:11px!important;margin-bottom:0!important}.settings-container[data-v-03345d7e]{width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.settings-container[data-v-03345d7e]{padding:16px 20px!important}}@media(max-width:680px)and (max-height:450px){.settings-container[data-v-03345d7e]{padding:16px 16px!important}}.settings-container .settings-navigation[data-v-03345d7e]{position:sticky;top:85px!important}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}.settings-container .settings-navigation .v-list-item--link[data-v-03345d7e],.settings-container .settings-navigation .v-list-item--link[data-v-03345d7e]:before{border-radius:11px!important}.settings-container .settings[data-v-03345d7e]{width:100%;min-width:0;border-radius:11px!important}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e]{margin-left:0!important}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e]{padding:20px!important;margin-left:0!important}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e]{margin-left:0!important}}.settings-container .settings[data-v-03345d7e] .settings__heading{display:flex;align-items:center;font-size:22px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading{font-size:20px}}.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:none;position:relative;left:-6px;padding:6px;border-radius:50%;color:var(--v-text-base)}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}.settings-container .settings[data-v-03345d7e] .settings__content{margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content{margin-top:16px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item{display:flex;position:relative;flex-direction:column;margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item{margin-top:16px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--sync-disabled .settings__item-heading:after{content:"デバイス間同期無効";display:flex;flex-shrink:0;position:relative;align-items:center;padding:2px 4px;margin-left:auto;border-radius:4px;background:var(--v-background-lighten2);font-size:11px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--switch{margin-right:62px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--switch .settings__item-heading{width:calc(100% + 62px)}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--disabled{opacity:.5}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-heading{display:flex;align-items:center;color:var(--v-text-base);font-size:16.5px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-heading{font-size:15px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-label{margin-top:8px;color:var(--v-text-darken1);font-size:13.5px;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-label{font-size:11px;line-height:1.7}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-form{margin-top:14px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-form{font-size:13.5px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-switch{align-items:center;position:absolute;top:0;right:-74px;bottom:0;margin-top:0}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item p{margin-bottom:8px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item p:last-of-type{margin-bottom:0}.settings-container .settings[data-v-03345d7e] .settings__content .settings__save-button{height:45px;background:var(--v-background-lighten2);font-size:15.5px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__save-button{height:40px;padding:0 12px;font-size:14px}}.settings__content[data-v-12036e32]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-12036e32]{opacity:0}.account[data-v-12036e32]{display:flex;align-items:center;height:130px;padding:18px 20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1264px)and (max-height:850px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:850px)and (min-height:850.01px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:1000px)and (max-height:450px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}.account-wrapper[data-v-12036e32]{display:flex;align-items:center;min-width:0;height:94px}@media(max-width:1000px)and (max-height:450px){.account-wrapper[data-v-12036e32]{height:80px}}.account__icon[data-v-12036e32]{flex-shrink:0;min-width:94px;height:100%;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}@media(max-width:1000px)and (max-height:450px){.account__icon[data-v-12036e32]{min-width:80px}}.account__info[data-v-12036e32]{display:flex;flex-direction:column;min-width:0;margin-left:20px;margin-right:12px}.account__info-name[data-v-12036e32]{display:inline-flex;align-items:center;height:33px}.account__info-name-text[data-v-12036e32]{display:inline-block;font-size:23px;color:var(--v-text-base);font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.account__info-name-text[data-v-12036e32]{font-size:21px}}.account__info-admin[data-v-12036e32]{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:52px;height:28px;margin-left:10px;border-radius:5px;background:var(--v-secondary-base);font-size:14px;font-weight:500;line-height:2}@media(max-width:1000px)and (max-height:450px){.account__info-admin[data-v-12036e32]{width:45px;height:24px;border-radius:4px;font-size:11.5px}}.account__info-id[data-v-12036e32]{display:inline-block;margin-top:2px;color:var(--v-text-darken1);font-size:16px}@media(max-width:1000px)and (max-height:450px){.account__info-id[data-v-12036e32]{font-size:14.5px}}.account__login[data-v-12036e32]{border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:1264px)and (max-height:850px){.account__login[data-v-12036e32]{height:50px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:850px)and (min-height:850.01px){.account__login[data-v-12036e32]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.account__login[data-v-12036e32]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}.account-register[data-v-12036e32]{display:flex;flex-direction:column;margin-top:28px}.account-register__heading[data-v-12036e32]{font-size:21px;font-weight:700;text-align:center;font-feature-settings:"palt" 1;letter-spacing:.04em}@media(max-width:1000px)and (max-height:450px){.account-register__heading[data-v-12036e32]{font-size:19px}}.account-register__feature[data-v-12036e32]{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:18px;grid-column-gap:16px;margin-top:28px}@media(max-width:1264px)and (max-height:850px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}@media(max-width:850px)and (min-height:850.01px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}@media(max-width:1000px)and (max-height:450px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}.account-register__feature .account-feature[data-v-12036e32]{display:flex;align-items:center}.account-register__feature .account-feature__icon[data-v-12036e32]{width:46px;height:46px;flex-shrink:0;margin-right:16px;color:var(--v-secondary-lighten1)}.account-register__feature .account-feature__info[data-v-12036e32]{display:flex;flex-direction:column}.account-register__feature .account-feature__info-heading[data-v-12036e32]{font-size:15px}.account-register__feature .account-feature__info-text[data-v-12036e32]{margin-top:3px;color:var(--v-text-darken1);font-size:12.5px;line-height:1.65}.account-register__description[data-v-12036e32]{margin-top:32px;font-size:15px;line-height:1.7;text-align:center}@media(max-width:1264px)and (max-height:850px){.account-register__description[data-v-12036e32]{font-size:12.5px}}@media(max-width:850px)and (min-height:850.01px){.account-register__description[data-v-12036e32]{font-size:10.5px}}@media(max-width:1000px)and (max-height:450px){.account-register__description[data-v-12036e32]{font-size:12.5px}}@media(max-width:680px)and (max-height:450px){.account-register__description[data-v-12036e32]{font-size:10.5px}}.account-register__button[data-v-12036e32]{margin-top:24px;margin-left:auto;margin-right:auto;border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:850px)and (min-height:850.01px){.account-register__button[data-v-12036e32]{height:42px!important;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.account-register__button[data-v-12036e32]{height:42px!important;font-size:14.5px}}.settings__content[data-v-786083d5]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-786083d5]{opacity:0}.niconico-account[data-v-786083d5]{display:flex;align-items:center;height:120px;padding:20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1264px)and (max-height:850px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:850px)and (min-height:850.01px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:16px!important;margin-right:0!important}.niconico-account .niconico-account-wrapper .niconico-account__info-name-text[data-v-786083d5]{font-size:18.5px}.niconico-account .niconico-account-wrapper .niconico-account__info-description[data-v-786083d5]{font-size:13.5px}}@media(max-width:1000px)and (max-height:450px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-right:0!important}}@media(max-width:680px)and (max-height:450px){.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:16px!important}.niconico-account .niconico-account-wrapper .niconico-account__info-name-text[data-v-786083d5]{font-size:18px}.niconico-account .niconico-account-wrapper .niconico-account__info-description[data-v-786083d5]{font-size:13px}}@media(max-width:850px)and (min-height:850.01px){.niconico-account--anonymous .niconico-account__login[data-v-786083d5]{margin-top:12px}}@media(max-width:1000px)and (max-height:450px){.niconico-account--anonymous .niconico-account__login[data-v-786083d5]{margin-top:12px}}@media(max-width:680px)and (max-height:450px){.niconico-account--anonymous .niconico-account-wrapper svg[data-v-786083d5]{display:none}.niconico-account--anonymous .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:0!important}}.niconico-account-wrapper[data-v-786083d5]{display:flex;align-items:center;min-width:0;height:80px}.niconico-account__icon[data-v-786083d5]{flex-shrink:0;min-width:80px;height:100%;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}.niconico-account__info[data-v-786083d5]{display:flex;flex-direction:column;min-width:0;margin-left:20px;margin-right:16px}.niconico-account__info-name[data-v-786083d5]{display:inline-flex;align-items:center;height:33px}.niconico-account__info-name-text[data-v-786083d5]{display:inline-block;font-size:20px;color:var(--v-text-base);font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.niconico-account__info-description[data-v-786083d5]{display:inline-block;margin-top:4px;color:var(--v-text-darken1);font-size:14px}.niconico-account__login[data-v-786083d5]{border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:1264px)and (max-height:850px){.niconico-account__login[data-v-786083d5]{height:50px!important;margin-top:8px;margin-right:auto}}@media(max-width:850px)and (min-height:850.01px){.niconico-account__login[data-v-786083d5]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.niconico-account__login[data-v-786083d5]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}.settings__content[data-v-1970b264]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-1970b264]{opacity:0}.twitter-accounts[data-v-1970b264]{display:flex;flex-direction:column;padding:20px 20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1000px)and (max-height:450px){.twitter-accounts[data-v-1970b264]{padding:16px 20px}}.twitter-accounts__heading[data-v-1970b264]{display:flex;align-items:center;font-size:18px;font-weight:700}.twitter-accounts__guide[data-v-1970b264]{display:flex;align-items:center}@media(max-width:850px)and (min-height:850.01px){.twitter-accounts__guide .text-h6[data-v-1970b264]{font-size:19px!important}}@media(max-width:680px)and (max-height:450px){.twitter-accounts__guide svg[data-v-1970b264]{display:none}.twitter-accounts__guide svg+div[data-v-1970b264]{margin-left:0!important}}.twitter-accounts .twitter-account[data-v-1970b264]{display:flex;align-items:center;margin-top:20px}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account[data-v-1970b264]{margin-top:16px}}.twitter-accounts .twitter-account__icon[data-v-1970b264]{flex-shrink:0;width:70px;height:70px;margin-right:16px;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__icon[data-v-1970b264]{width:52px;height:52px}}.twitter-accounts .twitter-account__info[data-v-1970b264]{display:flex;flex-direction:column;min-width:0;margin-right:16px}.twitter-accounts .twitter-account__info-name[data-v-1970b264]{display:inline-flex;align-items:center}.twitter-accounts .twitter-account__info-name-text[data-v-1970b264]{display:inline-block;color:var(--v-text-base);font-size:20px;font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__info-name-text[data-v-1970b264]{font-size:18px}}.twitter-accounts .twitter-account__info-screen-name[data-v-1970b264]{display:inline-block;color:var(--v-text-darken1);font-size:16px}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__info-screen-name[data-v-1970b264]{font-size:14px}}.twitter-accounts .twitter-account__login[data-v-1970b264]{margin-top:20px;margin-left:auto;margin-right:auto;border-radius:7px;font-size:15px;letter-spacing:0}@media(max-width:850px)and (min-height:850.01px){.twitter-accounts .twitter-account__login[data-v-1970b264]{height:42px!important;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__login[data-v-1970b264]{height:42px!important;font-size:14.5px}}.twitter-accounts .twitter-account__logout[data-v-1970b264]{background:var(--v-gray-base);border-radius:7px;font-size:15px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__logout[data-v-1970b264]{width:116px!important}.login-container-wrapper[data-v-0c2bb32a]{padding:20px!important;margin-bottom:0!important}}.login-container-wrapper .login-container[data-v-0c2bb32a]{border-radius:11px}@media(max-width:1000px)and (max-height:450px){.login-container-wrapper .login-container[data-v-0c2bb32a]{padding:24px!important}.login-container-wrapper .login-container .login__logo[data-v-0c2bb32a]{padding-top:4px!important;padding-bottom:20px!important}.login-container-wrapper .login-container .login__logo .v-image[data-v-0c2bb32a]{max-width:200px!important}.login-container-wrapper .login-container .v-input[data-v-0c2bb32a]{margin-top:24px!important;font-size:14px!important}}.login-container-wrapper .login-container .login-button[data-v-0c2bb32a]{border-radius:7px;margin-top:48px!important;font-size:18px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.login-container-wrapper .login-container .login-button[data-v-0c2bb32a]{height:44px!important;margin-top:24px!important;font-size:16px}.register-container-wrapper[data-v-d0eaf0ae]{padding:20px!important;margin-bottom:0!important}}.register-container-wrapper .register-container[data-v-d0eaf0ae]{border-radius:11px}@media(max-width:1000px)and (max-height:450px){.register-container-wrapper .register-container[data-v-d0eaf0ae]{padding:24px!important}.register-container-wrapper .register-container .register__logo[data-v-d0eaf0ae]{padding-top:4px!important;padding-bottom:8px!important}.register-container-wrapper .register-container .register__logo .v-image[data-v-d0eaf0ae]{max-width:200px!important}.register-container-wrapper .register-container .register__logo h4[data-v-d0eaf0ae]{margin-top:16px!important;font-size:19px!important}.register-container-wrapper .register-container .v-input[data-v-d0eaf0ae]{margin-top:0!important;font-size:14px!important}.register-container-wrapper .register-container .v-input[data-v-d0eaf0ae]:first-child{margin-top:24px!important}}.register-container-wrapper .register-container .register-button[data-v-d0eaf0ae]{border-radius:7px;margin-top:48px!important;font-size:18px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.register-container-wrapper .register-container .register-button[data-v-d0eaf0ae]{height:44px!important;margin-top:0!important;font-size:16px}}@media(max-width:850px)and (min-height:850.01px){h1[data-v-daa4530a]{font-size:24px!important}}@media(max-width:1000px)and (max-height:450px){h1[data-v-daa4530a]{font-size:24px!important}span[data-v-daa4530a]{font-size:15px!important}} \ No newline at end of file diff --git a/client/dist/assets/css/app.918ddfe6.css b/client/dist/assets/css/app.918ddfe6.css deleted file mode 100644 index a1a2edea..00000000 --- a/client/dist/assets/css/app.918ddfe6.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:Noto Sans JP Caption;font-weight:400;src:url(https://cdn.jsdelivr.net/npm/noto-sans-japanese@1.0.0/fonts/NotoSansJP-Medium.woff2) format("woff2")}html{overflow-y:auto!important}body .v-application{min-height:100vh;min-height:100dvh;font-family:YakuHanJPs,Open Sans,Hiragino Sans,Noto Sans JP,sans-serif;font-weight:500;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@supports(-webkit-touch-callout:none){body .v-application{min-height:-webkit-fill-available}}body .v-application .v-application--wrap{min-height:100%!important}body main{display:flex;width:100%;min-height:100%}body header+main{padding-top:65px!important}@media(max-width:1000px)and (max-height:450px){body header+main{padding-top:0!important}}body .route-container{height:100%;background:var(--v-background-base)}.v-btn{letter-spacing:0!important}.v-snack{left:110px!important}@media(max-width:850px)and (min-height:850.01px){.v-snack{left:0!important}}@media(max-width:1000px)and (max-height:450px){.v-snack{left:0!important}}@media(max-width:600px)and (min-height:450.01px){.v-snack{left:0!important}}.v-snack .v-btn__content{color:var(--v-primary-lighten1);letter-spacing:.3}.v-snack .error .v-btn__content,.v-snack .info .v-btn__content,.v-snack .success .v-btn__content,.v-snack .warning .v-btn__content{color:var(--v-text-base);letter-spacing:.3}.v-popper--theme-tooltip .v-popper__inner{display:inline-block;padding:4px 10px;border-radius:4px;background:var(--v-background-lighten1);color:var(--v-text-base);font-size:12px;font-family:YakuHanJPs,Open Sans,Hiragino Sans,Noto Sans JP,sans-serif;font-weight:500;opacity:.9;line-height:22px}.v-popper--theme-tooltip .v-popper__arrow-container{display:none}@media(hover:none){:hover:before{background-color:transparent!important}}::-moz-selection{background-color:#e64f9780}::selection{background-color:#e64f9780}.decorate-symbol{display:inline-flex;justify-content:center;align-items:center;position:relative;padding:0 3px;margin-left:2.5px;margin-right:2.5px;border-radius:4px;color:var(--v-text-base);background:var(--v-primary-base);font-size:.94em}*{scrollbar-color:var(--v-gray-base) var(--v-background-base);scrollbar-width:thin}::-webkit-scrollbar{width:7px;height:7px}::-webkit-scrollbar-track{background:var(--v-background-base)}::-webkit-scrollbar-thumb{background:var(--v-background-lighten2)}::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.v-menu__content::-webkit-scrollbar{width:12px;height:12px}.v-menu__content::-webkit-scrollbar-thumb{border:solid 3.5px var(--v-background-base)}.v-enter-active,.v-leave-active{transition:opacity .3s}.v-enter,.v-leave-to{opacity:0}.v-enter-active.route-container{position:fixed;top:0;left:0;right:0}.cursor-pointer{cursor:pointer}.w-25{width:25%}.w-50{width:50%}.w-75{width:75%}.w-100{width:100%}.h-25{height:25%}.h-50{height:50%}.h-75{height:75%}.h-100{height:100%}.header[data-v-506af489]{position:fixed;display:flex;align-items:center;width:100%;height:65px;padding:4px 16px;background:var(--v-background-base);box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);z-index:10}@media(max-width:1000px)and (max-height:450px){.header[data-v-506af489]{width:210px;height:48px;justify-content:center}}@media(max-width:680px)and (max-height:450px){.header[data-v-506af489]{width:190px}}@media(max-width:1000px)and (max-height:450px){.header .spacer[data-v-506af489]{display:none}}.header .konomitv-logo[data-v-506af489]{display:block;padding:12px 8px;border-radius:8px}@media(max-width:1000px)and (max-height:450px){.header .konomitv-logo[data-v-506af489]{margin:0!important}}.header .konomitv-logo__image[data-v-506af489]{display:block}@media(max-width:1000px)and (max-height:450px){.header .konomitv-logo__image[data-v-506af489]{height:19.5px}}.navigation-container[data-v-3c027344]{flex-shrink:0;width:220px;background:var(--v-background-lighten1)}@media(max-width:1000px)and (max-height:450px){.navigation-container[data-v-3c027344]{width:210px}}@media(max-width:680px)and (max-height:450px){.navigation-container[data-v-3c027344]{width:190px}}.navigation-container .navigation[data-v-3c027344]{position:fixed;width:220px;top:65px;left:0;bottom:-100px;padding-bottom:100px;background:var(--v-background-lighten1);z-index:1}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation[data-v-3c027344]{top:48px;width:210px}}@media(max-width:680px)and (max-height:450px){.navigation-container .navigation[data-v-3c027344]{width:190px}}.navigation-container .navigation .navigation-scroll[data-v-3c027344]{display:flex;flex-direction:column;height:100%;padding:22px 12px;overflow-y:auto}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll[data-v-3c027344]{padding:10px 12px}}@media(max-width:680px)and (max-height:450px){.navigation-container .navigation .navigation-scroll[data-v-3c027344]{padding:10px 8px}}.navigation-container .navigation .navigation-scroll[data-v-3c027344]::-webkit-scrollbar-track{background:var(--v-background-lighten1)}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]{display:flex;align-items:center;flex-shrink:0;height:52px;padding-left:16px;margin-top:4px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]{height:40px;padding-left:12px;border-radius:9px;font-size:15px}}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]:hover{background:var(--v-background-lighten2)}.navigation-container .navigation .navigation-scroll .navigation__link[data-v-3c027344]:first-of-type{margin-top:0}.navigation-container .navigation .navigation-scroll .navigation__link--active[data-v-3c027344]{color:var(--v-primary-base);background:#5b2d3c}.navigation-container .navigation .navigation-scroll .navigation__link--active[data-v-3c027344]:hover{background:#5b2d3c}.navigation-container .navigation .navigation-scroll .navigation__link--version[data-v-3c027344]{font-size:15px}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link--version[data-v-3c027344]{font-size:14.5px}}.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon[data-v-3c027344]{margin-right:14px}@media(max-width:1000px)and (max-height:450px){.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon[data-v-3c027344]{margin-right:10px}}.navigation-container .navigation .navigation-scroll .navigation__link .navigation__link-icon--highlight[data-v-3c027344]{color:var(--v-secondary-base)}.channels-container.channels-container--home .v-tabs-bar{height:54px;background:linear-gradient(to bottom,var(--v-background-base) calc(100% - 3px),var(--v-background-lighten1) 3px)}@media(max-width:1000px)and (max-height:450px){.channels-container.channels-container--home .v-tabs-bar{height:46px}}.channels-container.channels-container--home .v-tabs-slider-wrapper{height:3px!important;transition:left .3s cubic-bezier(.25,.8,.5,1)}.channels-container.channels-container--home .v-window__container{min-height:calc(100vh - 180px);min-height:calc(100dvh - 180px)}@media(hover:none){.channels-container.channels-container--home .v-window__container{min-height:auto}}:root .channels-container.channels-container--home .v-window__container,_::-webkit-full-page-media,_:future{height:inherit!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--home .v-window__container--is-active,_::-webkit-full-page-media,_:future{display:none!important}:root .channels-container.channels-container--home .v-window__container .v-window-item,_::-webkit-full-page-media,_:future{display:none!important;position:static!important;transform:none!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--home .v-window__container .v-window-item--active,_::-webkit-full-page-media,_:future{display:block!important}.channels-container[data-v-189c71d3]{display:flex;flex-direction:column;width:100%;margin-left:21px;margin-right:21px;opacity:1;transition:opacity .4s}.channels-container--loading[data-v-189c71d3]{opacity:0}.channels-container .channels-tab[data-v-189c71d3]{position:sticky;flex:none;top:65px;padding-top:10px;padding-bottom:20px;background:var(--v-background-base);z-index:1}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab[data-v-189c71d3]{top:0;padding-top:0;padding-bottom:8px}}.channels-container .channels-tab .channels-tab__item[data-v-189c71d3]{width:98px;padding:0;color:var(--v-text-base)!important;font-size:16px;text-transform:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab .channels-tab__item[data-v-189c71d3]{font-size:15px}}.channels-container .channels-list[data-v-189c71d3]{padding-bottom:32px;background:transparent!important;overflow:inherit}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list[data-v-189c71d3]{padding-bottom:12px}}.channels-container .channels-list .channels[data-v-189c71d3]{display:grid;grid-template-columns:repeat(auto-fit,minmax(365px,1fr));grid-row-gap:16px;grid-column-gap:16px;justify-content:center;background:var(--v-background-base);will-change:transform}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels[data-v-189c71d3]{grid-row-gap:8px}}@media(min-width:1630px){.channels-container .channels-list .channels[data-v-189c71d3]{grid-template-columns:repeat(auto-fit,445px)}}.channels-container .channels-list .channels.channels--length-0.channels--tab-ピン留め[data-v-189c71d3]{display:flex;min-height:calc(100vh - 180px);min-height:calc(100dvh - 180px)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels.channels--length-0.channels--tab-ピン留め[data-v-189c71d3]{min-height:calc(100vh - 66px);min-height:calc(100dvh - 66px)}}@media(min-width:1008px){.channels-container .channels-list .channels.channels--length-1[data-v-189c71d3]{margin-right:calc(50% + 8px)}}@media(min-width:1389px){.channels-container .channels-list .channels.channels--length-1[data-v-189c71d3]{margin-right:calc(66.66667% + 10.66667px)}}@media(min-width:1630px){.channels-container .channels-list .channels.channels--length-1[data-v-189c71d3]{margin-right:922px}}@media(min-width:2090px){.channels-container .channels-list .channels.channels--length-1[data-v-189c71d3]{margin-right:1383px}}@media(min-width:1389px){.channels-container .channels-list .channels.channels--length-2[data-v-189c71d3]{margin-right:calc(33.33333% + 5.33333px)}}@media(min-width:1630px){.channels-container .channels-list .channels.channels--length-2[data-v-189c71d3]{margin-right:461px}}@media(min-width:2090px){.channels-container .channels-list .channels.channels--length-2[data-v-189c71d3]{margin-right:922px}.channels-container .channels-list .channels.channels--length-3[data-v-189c71d3]{margin-right:461px}}.channels-container .channels-list .channels .channel[data-v-189c71d3]{display:flex;flex-direction:column;position:relative;height:275px;padding:20px 20px;border-radius:16px;color:var(--v-text-base);background:var(--v-background-lighten1);transition:background-color .15s;overflow:hidden;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1007.9px){.channels-container .channels-list .channels .channel[data-v-189c71d3]{height:auto}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel[data-v-189c71d3]{padding:12px 14px;padding-top:10px;height:auto;border-radius:11px}}.channels-container .channels-list .channels .channel[data-v-189c71d3]:hover{background:var(--v-background-lighten2)}@media(hover:none){.channels-container .channels-list .channels .channel[data-v-189c71d3]:hover{background:var(--v-background-lighten1)}}.channels-container .channels-list .channels .channel .channel__broadcaster[data-v-189c71d3]{display:flex;height:44px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster[data-v-189c71d3]{height:29px}}.channels-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-189c71d3]{display:inline-block;flex-shrink:0;width:80px;height:44px;border-radius:5px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-189c71d3]{width:54px;height:29px;border-radius:4px}}.channels-container .channels-list .channels .channel .channel__broadcaster-content[data-v-189c71d3]{display:flex;flex-direction:column;margin-left:16px;width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-content[data-v-189c71d3]{align-items:center;flex-direction:row;margin-left:12px;margin-right:6px}}.channels-container .channels-list .channels .channel .channel__broadcaster-name[data-v-189c71d3]{flex-shrink:0;font-size:18px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-name[data-v-189c71d3]{font-size:15px}}.channels-container .channels-list .channels .channel .channel__broadcaster-status[data-v-189c71d3]{display:flex;flex-shrink:0;align-items:center;margin-top:2px;font-size:12px;color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status[data-v-189c71d3]{margin-top:3px;margin-left:auto;font-size:12px}}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force[data-v-189c71d3],.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers[data-v-189c71d3]{display:flex;align-items:center}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status-force span[data-v-189c71d3]:nth-child(2),.channels-container .channels-list .channels .channel .channel__broadcaster-status-force span[data-v-189c71d3]:nth-child(4),.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers span[data-v-189c71d3]:nth-child(2),.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers span[data-v-189c71d3]:nth-child(4){display:none}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-status-viewers[data-v-189c71d3]{margin-left:8px!important}}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--festival[data-v-189c71d3]{color:#e7556e}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--so-many[data-v-189c71d3]{color:#e76b55}.channels-container .channels-list .channels .channel .channel__broadcaster-status-force--many[data-v-189c71d3]{color:#e7a355}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-189c71d3]{display:flex;align-items:center;justify-content:center;flex-shrink:0;position:relative;top:-5px;right:-5px;width:34px;height:34px;padding:4px;color:var(--v-text-darken1);border-radius:50%;transition:color .15s ease,background-color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-189c71d3]{top:-1px}}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-189c71d3]:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;border-radius:inherit;background-color:currentColor;color:inherit;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.6,1);pointer-events:none}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-189c71d3]:hover{color:var(--v-text-base)}.channels-container .channels-list .channels .channel .channel__broadcaster-pin[data-v-189c71d3]:hover:before{opacity:.15}.channels-container .channels-list .channels .channel .channel__broadcaster-pin--pinned[data-v-189c71d3]{color:var(--v-primary-base)}.channels-container .channels-list .channels .channel .channel__broadcaster-pin--pinned[data-v-189c71d3]:hover{color:var(--v-primary-lighten1)}.channels-container .channels-list .channels .channel .channel__program-present[data-v-189c71d3]{display:flex;flex-direction:column}.channels-container .channels-list .channels .channel .channel__program-present-title-wrapper[data-v-189c71d3]{display:block;margin-top:14px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-title-wrapper[data-v-189c71d3]{display:flex;align-items:center;margin-top:10px}}.channels-container .channels-list .channels .channel .channel__program-present-title[data-v-189c71d3]{display:-webkit-box;font-size:16px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-title[data-v-189c71d3]{font-size:14.5px;-webkit-line-clamp:1}}.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-189c71d3]{margin-top:4px;color:var(--v-text-darken1);font-size:13.5px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-189c71d3]{flex-shrink:0;margin-top:0;margin-left:auto;padding-left:10px;font-size:12px}}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-time[data-v-189c71d3]{font-size:11px;padding-left:6px}}.channels-container .channels-list .channels .channel .channel__program-present-description[data-v-189c71d3]{display:-webkit-box;margin-top:8px;color:var(--v-text-darken1);font-size:10.5px;line-height:175%;overflow-wrap:break-word;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:3;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-present-description[data-v-189c71d3]{margin-top:6px;font-size:10px;-webkit-line-clamp:2}}.channels-container .channels-list .channels .channel .channel__program-following[data-v-189c71d3]{display:flex;flex-direction:column;color:var(--v-text-base);font-size:12.5px}@media(max-width:1007.9px){.channels-container .channels-list .channels .channel .channel__program-following[data-v-189c71d3]{margin-top:6px}}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following[data-v-189c71d3]{flex-direction:row;margin-top:6px;font-size:12px}}.channels-container .channels-list .channels .channel .channel__program-following-title[data-v-189c71d3]{display:flex;align-items:center;min-width:0}.channels-container .channels-list .channels .channel .channel__program-following-title-decorate[data-v-189c71d3]{flex-shrink:0;font-weight:700}.channels-container .channels-list .channels .channel .channel__program-following-title-icon[data-v-189c71d3]{flex-shrink:0;margin-left:3px}.channels-container .channels-list .channels .channel .channel__program-following-title-text[data-v-189c71d3]{margin-left:2px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-189c71d3]{color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-189c71d3]{flex-shrink:0;margin-left:auto;padding-left:8px;font-size:11.5px}}@media(max-width:680px)and (max-height:450px){.channels-container .channels-list .channels .channel .channel__program-following-time[data-v-189c71d3]{font-size:11px;padding-left:6px}}.channels-container .channels-list .channels .channel .channel__progressbar[data-v-189c71d3]{position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--v-gray-base)}.channels-container .channels-list .channels .channel .channel__progressbar-progress[data-v-189c71d3]{height:4px;background:var(--v-primary-base);transition:width .3s}@media(max-width:850px)and (min-height:850.01px){.channels-container .pinned-container h2[data-v-189c71d3]{font-size:21px!important}.channels-container .pinned-container div[data-v-189c71d3]{font-size:12.5px!important;text-align:center}}@media(max-width:1000px)and (max-height:450px){.channels-container .pinned-container h2[data-v-189c71d3]{font-size:21px!important}.channels-container .pinned-container div[data-v-189c71d3]{font-size:13px!important;text-align:center}.channels-container .pinned-container div .mt-4[data-v-189c71d3]{margin-top:12px!important}}@media(max-width:680px)and (max-height:450px){.channels-container .pinned-container h2[data-v-189c71d3]{font-size:16px!important}.channels-container .pinned-container div[data-v-189c71d3]{font-size:10.5px!important}.channels-container .pinned-container div .mt-4[data-v-189c71d3]{margin-top:8px!important}}.channels-container.channels-container--watch .v-tabs-bar{position:relative;top:-9px;height:48px;background:linear-gradient(to bottom,var(--v-background-base) calc(100% - 3px),var(--v-background-lighten1) 3px)}@media(max-width:1000px)and (max-height:450px){.channels-container.channels-container--watch .v-tabs-bar{height:40px}}.channels-container.channels-container--watch .v-tabs-bar .v-slide-group__next,.channels-container.channels-container--watch .v-tabs-bar .v-slide-group__prev{flex:auto!important;min-width:28px!important}.channels-container.channels-container--watch .v-tabs-slider-wrapper{height:3px!important;transition:left .3s cubic-bezier(.25,.8,.5,1)}:root .channels-container.channels-container--watch .v-window__container,_::-webkit-full-page-media,_:future{height:inherit!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--watch .v-window__container--is-active,_::-webkit-full-page-media,_:future{display:none!important}:root .channels-container.channels-container--watch .v-window__container .v-window-item,_::-webkit-full-page-media,_:future{display:none!important;position:static!important;transform:none!important;-webkit-transition:none!important;transition:none!important}:root .channels-container.channels-container--watch .v-window__container .v-window-item--active,_::-webkit-full-page-media,_:future{display:block!important}.channels-container[data-v-4380062e]{display:flex;flex-direction:column}.channels-container .channels-tab[data-v-4380062e]{position:sticky;flex:none;top:0;padding-left:16px;padding-right:16px;padding-bottom:9px;background:var(--v-background-base);z-index:1}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab[data-v-4380062e]{padding-bottom:0;margin-top:12px}}.channels-container .channels-tab .channels-tab__item[data-v-4380062e]{min-width:72px!important;padding:0 8px;color:var(--v-text-base)!important;font-size:15px;text-transform:none}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-tab .channels-tab__item[data-v-4380062e]{font-size:14.5px}}.channels-container .channels-list-container[data-v-4380062e]{overflow-y:auto}.channels-container .channels-list-container .channels-list[data-v-4380062e]{padding-left:16px;padding-right:10px;padding-bottom:16px;background:transparent!important;overflow:visible!important}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list[data-v-4380062e]{padding-bottom:12px}}.channels-container .channels-list-container .channels-list .channels[data-v-4380062e]{display:flex;justify-content:center;flex-direction:column;will-change:transform}@media(min-width:1630px){.channels-container .channels-list-container .channels-list .channels[data-v-4380062e]{grid-template-columns:repeat(auto-fit,445px)}}.channels-container .channels-list-container .channels-list .channels .channel[data-v-4380062e]{display:flex;flex-direction:column;position:relative;margin-top:12px;padding:10px 12px 14px 12px;border-radius:10px;color:var(--v-text-base);background:var(--v-background-lighten1);transition:background-color .15s;overflow:hidden;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}.channels-container .channels-list-container .channels-list .channels .channel[data-v-4380062e]:first-of-type{margin-top:0}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel[data-v-4380062e]{margin-top:8px;padding:8px 12px 12px 12px}}.channels-container .channels-list-container .channels-list .channels .channel[data-v-4380062e]:hover{background:var(--v-background-lighten2)}@media(hover:none){.channels-container .channels-list-container .channels-list .channels .channel[data-v-4380062e]:hover{background:var(--v-background-lighten1)}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster[data-v-4380062e]{display:flex;height:28px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster[data-v-4380062e]{height:24px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-4380062e]{display:inline-block;flex-shrink:0;width:48px;height:100%;border-radius:4px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-icon[data-v-4380062e]{width:46px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-content[data-v-4380062e]{display:flex;align-items:center;margin-left:12px;width:100%;min-width:0}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-name[data-v-4380062e]{font-size:14.5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force[data-v-4380062e]{display:flex;align-items:center;flex-shrink:0;margin-top:2px;margin-left:auto;padding-left:6px;font-size:12px;color:var(--v-text-darken1)}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--festival[data-v-4380062e]{color:#e7556e}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--so-many[data-v-4380062e]{color:#e76b55}.channels-container .channels-list-container .channels-list .channels .channel .channel__broadcaster-force--many[data-v-4380062e]{color:#e7a355}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present[data-v-4380062e]{display:flex;flex-direction:column}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-title[data-v-4380062e]{display:-webkit-box;margin-top:8px;font-size:13.5px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.07em;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-title[data-v-4380062e]{margin-top:6px;font-size:13px;-webkit-line-clamp:1}}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-time[data-v-4380062e]{margin-top:4px;color:var(--v-text-darken1);font-size:11.5px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-present-time[data-v-4380062e]{font-size:11px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following[data-v-4380062e]{display:flex;flex-direction:column;margin-top:4px;color:var(--v-text-darken1);font-size:11.5px}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title[data-v-4380062e]{display:flex;align-items:center}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-decorate[data-v-4380062e]{flex-shrink:0}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-icon[data-v-4380062e]{flex-shrink:0;margin-left:3px}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-title-text[data-v-4380062e]{margin-left:2px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-time[data-v-4380062e]{margin-top:1px}@media(max-width:1000px)and (max-height:450px){.channels-container .channels-list-container .channels-list .channels .channel .channel__program-following-time[data-v-4380062e]{font-size:10px}}.channels-container .channels-list-container .channels-list .channels .channel .channel__progressbar[data-v-4380062e]{position:absolute;left:0;right:0;bottom:0;height:4px;background:var(--v-gray-base)}.channels-container .channels-list-container .channels-list .channels .channel .channel__progressbar-progress[data-v-4380062e]{height:4px;background:var(--v-primary-base);transition:width .3s}.settings__item[data-v-5d831536]{display:flex;position:relative;flex-direction:column;margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings__item[data-v-5d831536]{margin-top:16px}}.settings__item--switch[data-v-5d831536]{margin-right:62px}.settings__item-heading[data-v-5d831536]{display:flex;align-items:center;color:var(--v-text-base);font-size:16.5px}@media(max-width:1000px)and (max-height:450px){.settings__item-heading[data-v-5d831536]{font-size:15px}}.settings__item-label[data-v-5d831536]{margin-top:8px;color:var(--v-text-darken1);font-size:13.5px;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.settings__item-label[data-v-5d831536]{font-size:11px;line-height:1.7}}.settings__item-form[data-v-5d831536]{margin-top:14px}@media(max-width:1000px)and (max-height:450px){.settings__item-form[data-v-5d831536]{font-size:13.5px}}.settings__item-switch[data-v-5d831536]{align-items:center;position:absolute;top:0;right:-74px;bottom:0;margin-top:0}.settings__item p[data-v-5d831536]{margin-bottom:8px}.settings__item p[data-v-5d831536]:last-of-type{margin-bottom:0}.muted-comment-items[data-v-5d831536]{display:flex;flex-direction:column;margin-top:8px}.muted-comment-items .muted-comment-item[data-v-5d831536]{display:flex;align-items:center;padding:6px 0;border-bottom:1px solid var(--v-background-lighten2);transition:background-color .15s ease}.muted-comment-items .muted-comment-item[data-v-5d831536]:last-of-type{border-bottom:none}.muted-comment-items .muted-comment-item__input[data-v-5d831536]{font-size:14px}.muted-comment-items .muted-comment-item__match-type[data-v-5d831536]{max-width:150px;margin-left:12px;font-size:14px}.muted-comment-items .muted-comment-item__delete-button[data-v-5d831536]{display:flex;align-items:center;justify-content:center;width:40px;height:40px;margin-left:6px;border-radius:5px;outline:none;cursor:pointer}.comment-container[data-v-3eadf094]{display:flex;flex-direction:column}.comment-container .comment-header[data-v-3eadf094]{display:flex;align-items:center;flex-shrink:0;width:100%;height:26px;padding-left:16px;padding-right:16px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header[data-v-3eadf094]{margin-top:12px}}.comment-container .comment-header__title[data-v-3eadf094]{display:flex;align-items:center;font-size:18.5px;font-weight:700;line-height:145%}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header__title[data-v-3eadf094]{font-size:16.5px}}.comment-container .comment-header__title-icon[data-v-3eadf094]{margin-bottom:-3px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-header__title-icon[data-v-3eadf094]{height:17.5px}}.comment-container .comment-header__title-text[data-v-3eadf094]{margin-left:12px}.comment-container .comment-header__button[data-v-3eadf094]{display:flex;align-items:center;height:26px;padding:0 9px;border-radius:4px;background:var(--v-background-lighten3);font-size:11px;line-height:1.8;letter-spacing:0}.comment-container .comment-list-wrapper[data-v-3eadf094]{position:relative;width:100%;height:100%;min-height:0;margin-top:16px}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper[data-v-3eadf094]{margin-top:12px}}.comment-container .comment-list-wrapper .comment-list-dropdown[data-v-3eadf094]{display:inline-block;position:absolute;top:var(--comment-list-dropdown-top,0);right:16px;border-radius:4px;overflow-x:hidden;overflow-y:auto;box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);opacity:0;visibility:hidden;transition:opacity .15s ease,visibility .15s ease;z-index:8}.comment-container .comment-list-wrapper .comment-list-dropdown--display[data-v-3eadf094]{opacity:1;visibility:visible}.comment-container .comment-list-wrapper .comment-list-cover[data-v-3eadf094]{display:none;position:absolute;top:0;left:0;width:100%;height:100%;z-index:7}.comment-container .comment-list-wrapper .comment-list-cover--display[data-v-3eadf094]{display:block}.comment-container .comment-list-wrapper .comment-list[data-v-3eadf094]{width:100%;height:100%;padding-left:16px;padding-right:10px;padding-bottom:12px;overflow-y:scroll!important}.comment-container .comment-list-wrapper .comment-list .comment[data-v-3eadf094]{display:flex;position:relative;align-items:center;min-height:28px;padding-top:6px;word-break:break-all}.comment-container .comment-list-wrapper .comment-list .comment--my-post[data-v-3eadf094]{color:var(--v-secondary-lighten2)}.comment-container .comment-list-wrapper .comment-list .comment__text[data-v-3eadf094]{font-size:13px}.comment-container .comment-list-wrapper .comment-list .comment__time[data-v-3eadf094]{flex-shrink:0;margin-left:auto;padding-left:8px;color:var(--v-text-darken1);font-size:13px}.comment-container .comment-list-wrapper .comment-list .comment__icon[data-v-3eadf094]{width:20px;height:20px;margin-left:8px}.comment-container .comment-list-wrapper .comment-announce[data-v-3eadf094]{display:flex;align-items:center;justify-content:center;flex-direction:column;position:absolute;top:0;left:0;width:100%;height:100%;padding-left:12px;padding-right:12px}.comment-container .comment-list-wrapper .comment-announce__heading[data-v-3eadf094]{font-size:20px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper .comment-announce__heading[data-v-3eadf094]{font-size:16px}}.comment-container .comment-list-wrapper .comment-announce__text[data-v-3eadf094]{margin-top:12px;color:var(--v-text-darken1);font-size:13.5px;text-align:center}@media(max-width:1000px)and (max-height:450px){.comment-container .comment-list-wrapper .comment-announce__text[data-v-3eadf094]{font-size:12px}}.comment-container .comment-scroll-button[data-v-3eadf094]{display:flex;align-items:center;justify-content:center;position:absolute;left:0;right:0;bottom:22px;width:42px;height:42px;margin:0 auto;border-radius:50%;background:var(--v-primary-base);transition:background-color .15s,opacity .3s,visibility .3s;visibility:hidden;opacity:0;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}.comment-container .comment-scroll-button--display[data-v-3eadf094]{opacity:1;visibility:visible}.program-container[data-v-3c7f1e0c]{padding-left:16px;padding-right:16px;overflow-y:auto}.program-container .program-broadcaster[data-v-3c7f1e0c]{display:none;align-items:center;min-width:0}@media(max-width:1000px)and (max-height:450px){.program-container .program-broadcaster[data-v-3c7f1e0c]{display:flex;margin-top:16px}}.program-container .program-broadcaster__icon[data-v-3c7f1e0c]{display:inline-block;flex-shrink:0;width:43px;height:24px;border-radius:3px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.program-container .program-broadcaster__icon[data-v-3c7f1e0c]{width:42px;height:23.5px}}.program-container .program-broadcaster__number[data-v-3c7f1e0c]{flex-shrink:0;margin-left:12px;font-size:16.5px}.program-container .program-broadcaster__name[data-v-3c7f1e0c]{margin-left:5px;font-size:16.5px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.program-container .program-info .program-info__title[data-v-3c7f1e0c]{font-size:22px;font-weight:700;line-height:145%;font-feature-settings:"palt" 1;letter-spacing:.05em}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__title[data-v-3c7f1e0c]{margin-top:10px;font-size:18px}}.program-container .program-info .program-info__time[data-v-3c7f1e0c]{margin-top:8px;color:var(--v-text-darken1);font-size:14px}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__time[data-v-3c7f1e0c]{font-size:13px}}.program-container .program-info .program-info__description[data-v-3c7f1e0c]{margin-top:12px;color:var(--v-text-darken1);font-size:12px;line-height:168%;overflow-wrap:break-word;font-feature-settings:"palt" 1;letter-spacing:.08em}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__description[data-v-3c7f1e0c]{margin-top:8px;font-size:11px}}.program-container .program-info .program-info__genre-container[data-v-3c7f1e0c]{display:flex;flex-wrap:wrap;margin-top:10px}.program-container .program-info .program-info__genre-container .program-info__genre[data-v-3c7f1e0c]{display:inline-block;font-size:10.5px;padding:3px;margin-top:4px;margin-right:4px;border-radius:4px;background:var(--v-background-lighten2)}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__genre-container .program-info__genre[data-v-3c7f1e0c]{font-size:9px}}.program-container .program-info .program-info__next[data-v-3c7f1e0c]{display:flex;align-items:center;margin-top:18px;color:var(--v-text-base);font-size:14px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__next[data-v-3c7f1e0c]{margin-top:14px;font-size:13px}}.program-container .program-info .program-info__next-decorate[data-v-3c7f1e0c]{flex-shrink:0}.program-container .program-info .program-info__next-icon[data-v-3c7f1e0c]{flex-shrink:0;margin-left:3px;font-size:15px}.program-container .program-info .program-info__next-title[data-v-3c7f1e0c]{display:-webkit-box;margin-top:2px;color:var(--v-text-base);font-size:14px;font-weight:700;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__next-title[data-v-3c7f1e0c]{font-size:13px}}.program-container .program-info .program-info__next-time[data-v-3c7f1e0c]{margin-top:3px;color:var(--v-text-darken1);font-size:13.5px}.program-container .program-info .program-info__status[data-v-3c7f1e0c]{display:flex;align-items:center;margin-top:16px;font-size:14px;color:var(--v-text-darken1)}@media(max-width:1000px)and (max-height:450px){.program-container .program-info .program-info__status[data-v-3c7f1e0c]{margin-top:10px;font-size:12px}}.program-container .program-info .program-info__status-force[data-v-3c7f1e0c],.program-container .program-info .program-info__status-viewers[data-v-3c7f1e0c]{display:flex;align-items:center}.program-container .program-info .program-info__status-force--festival[data-v-3c7f1e0c]{color:#e7556e}.program-container .program-info .program-info__status-force--so-many[data-v-3c7f1e0c]{color:#e76b55}.program-container .program-info .program-info__status-force--many[data-v-3c7f1e0c]{color:#e7a355}.program-container .program-detail-container[data-v-3c7f1e0c]{margin-top:24px;margin-bottom:24px}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container[data-v-3c7f1e0c]{margin-top:20px;margin-bottom:16px}}.program-container .program-detail-container .program-detail[data-v-3c7f1e0c]{margin-top:16px}.program-container .program-detail-container .program-detail .program-detail__heading[data-v-3c7f1e0c]{font-size:18px}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container .program-detail .program-detail__heading[data-v-3c7f1e0c]{font-size:16px}}.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c]{margin-top:8px;color:var(--v-text-darken1);font-size:12px;line-height:168%;overflow-wrap:break-word;white-space:pre-wrap;font-feature-settings:"palt" 1;letter-spacing:.08em}@media(max-width:1000px)and (max-height:450px){.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c]{font-size:11px}}.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c] a:link,.program-container .program-detail-container .program-detail .program-detail__text[data-v-3c7f1e0c] a:visited{color:var(--v-primary-lighten1);text-underline-offset:3px}@media(max-width:1000px)and (max-height:450px){.zoom-capture-modal-container.v-dialog{width:auto!important;max-width:auto!important;aspect-ratio:16/9}}.zoom-capture-modal[data-v-df5cea26]{position:relative}.zoom-capture-modal__image[data-v-df5cea26]{display:block;width:100%;border-radius:11px}.zoom-capture-modal__download[data-v-df5cea26]{display:flex;position:absolute;align-items:center;justify-content:center;right:22px;bottom:20px;width:80px;height:80px;border-radius:50%;color:var(--v-text-base);filter:drop-shadow(0 0 4.5px rgba(0,0,0,.9))}.twitter-container[data-v-df5cea26]{display:flex;flex-direction:column;position:relative;padding-bottom:8px}.twitter-container.watch-panel__content--active .tab-container .tab-content--active[data-v-df5cea26]{opacity:1;visibility:visible}.twitter-container .tab-container[data-v-df5cea26]{flex-grow:1;min-height:0}.twitter-container .tab-container .tab-content[data-v-df5cea26]{position:relative;height:100%;transition:opacity .2s,visibility .2s;opacity:0;visibility:hidden;overflow-y:scroll}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content[data-v-df5cea26]{padding-top:8px}}.twitter-container .tab-container .tab-content .captures[data-v-df5cea26]{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:12px;grid-column-gap:12px;padding-left:12px;padding-right:5px;max-height:100%}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .captures[data-v-df5cea26]{grid-row-gap:8px;grid-column-gap:8px}}.twitter-container .tab-container .tab-content .captures .capture[data-v-df5cea26]{position:relative;height:82px;border-radius:11px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .captures .capture[data-v-df5cea26]{height:74px}}.twitter-container .tab-container .tab-content .captures .capture__image[data-v-df5cea26]{display:block;width:100%;height:100%}.twitter-container .tab-container .tab-content .captures .capture__zoom[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;position:absolute;top:1px;right:3px;width:38px;height:38px;border-radius:50%;filter:drop-shadow(0 0 2.5px rgba(0,0,0,.9));cursor:pointer}.twitter-container .tab-container .tab-content .captures .capture__disabled-cover[data-v-df5cea26],.twitter-container .tab-container .tab-content .captures .capture__selected-number[data-v-df5cea26]{display:none;align-items:center;justify-content:center;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(30,19,16,.5)}.twitter-container .tab-container .tab-content .captures .capture__selected-number[data-v-df5cea26]{font-size:38px;text-shadow:0 0 2.5px rgba(0,0,0,.9)}.twitter-container .tab-container .tab-content .captures .capture__selected-checkmark[data-v-df5cea26]{display:none;position:absolute;top:6px;left:7px;width:20px;height:20px;color:var(--v-primary-base)}.twitter-container .tab-container .tab-content .captures .capture__selected-border[data-v-df5cea26]{display:none;position:absolute;top:0;left:0;right:0;bottom:0;border-radius:11px;border:4px solid var(--v-primary-base)}.twitter-container .tab-container .tab-content .captures .capture__focused-border[data-v-df5cea26]{display:none;position:absolute;top:0;left:0;right:0;bottom:0;border-radius:11px;border:4px solid var(--v-secondary-base)}.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-border[data-v-df5cea26],.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-checkmark[data-v-df5cea26],.twitter-container .tab-container .tab-content .captures .capture--selected .capture__selected-number[data-v-df5cea26]{display:flex}.twitter-container .tab-container .tab-content .captures .capture--focused .capture__focused-border[data-v-df5cea26]{display:block}.twitter-container .tab-container .tab-content .captures .capture--disabled[data-v-df5cea26]{cursor:auto}.twitter-container .tab-container .tab-content .captures .capture--disabled .capture__disabled-cover[data-v-df5cea26]{display:block}.twitter-container .tab-container .tab-content .capture-announce[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;flex-direction:column;height:100%;padding-left:12px;padding-right:5px}.twitter-container .tab-container .tab-content .capture-announce__heading[data-v-df5cea26]{font-size:20px;font-weight:700}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .capture-announce__heading[data-v-df5cea26]{font-size:16px}}.twitter-container .tab-container .tab-content .capture-announce__text[data-v-df5cea26]{margin-top:12px;color:var(--v-text-darken1);font-size:13.5px;text-align:center}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-container .tab-content .capture-announce__text[data-v-df5cea26]{font-size:12px}}.twitter-container .tab-button-container[data-v-df5cea26]{display:flex;flex-shrink:0;-moz-column-gap:7px;column-gap:7px;height:40px;margin-left:12px;margin-right:12px;padding-top:8px;padding-bottom:6px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-button-container[data-v-df5cea26]{height:38px;margin-left:8px;margin-right:8px}}.twitter-container .tab-button-container .tab-button[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;flex:1;background:var(--v-background-lighten2);border-radius:7px;font-size:11px;transition:background-color .15s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tab-button-container .tab-button[data-v-df5cea26]{font-size:10.5px}}.twitter-container .tab-button-container .tab-button--active[data-v-df5cea26]{background:var(--v-twitter-base)}.twitter-container .tab-button-container .tab-button__text[data-v-df5cea26]{margin-left:4px;margin-right:2px;line-height:2}.twitter-container .tweet-form[data-v-df5cea26]{display:flex;flex-direction:column;flex-shrink:0;height:136px;margin-left:12px;margin-right:12px;border-radius:12px;border-bottom-left-radius:7px;border-bottom-right-radius:7px;background:var(--v-background-lighten1);transition:box-shadow .09s ease}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form[data-v-df5cea26]{height:96px;margin-left:8px;margin-right:8px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}}.twitter-container .tweet-form--focused[data-v-df5cea26]{box-shadow:0 0 0 3.5px rgba(79,130,230,.6)}.twitter-container .tweet-form--virtual-keyboard-display[data-v-df5cea26]{position:relative;bottom:calc(env(keyboard-inset-height, 0px) - 77px)}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form--virtual-keyboard-display[data-v-df5cea26]{bottom:calc(env(keyboard-inset-height, 0px) - 56px)}}.twitter-container .tweet-form__hashtag[data-v-df5cea26]{display:flex;align-items:center;height:19px;margin-top:12px;margin-left:12px;margin-right:12px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__hashtag[data-v-df5cea26]{height:16px;margin-top:8px}}.twitter-container .tweet-form__hashtag-form[data-v-df5cea26]{display:block;height:100%;flex-grow:1;font-size:12.5px;color:var(--v-twitter-lighten2);outline:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__hashtag-form[data-v-df5cea26]{font-size:12px}}.twitter-container .tweet-form__hashtag-form[data-v-df5cea26]::-moz-placeholder{color:rgba(65,165,241,.6)}.twitter-container .tweet-form__hashtag-form[data-v-df5cea26]::placeholder{color:rgba(65,165,241,.6)}.twitter-container .tweet-form__hashtag-list-button[data-v-df5cea26]{display:flex;position:relative;align-items:center;justify-content:center;right:-8px;width:34px;height:34px;padding:6px;border-radius:50%;color:var(--v-twitter-lighten2);cursor:pointer}.twitter-container .tweet-form__textarea[data-v-df5cea26]{display:block;flex-grow:1;margin-top:8px;margin-left:12px;margin-right:12px;font-size:12.5px;color:var(--v-text-base);word-break:break-all;resize:none;outline:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__textarea[data-v-df5cea26]{margin-top:6px;font-size:12px}}.twitter-container .tweet-form__textarea[data-v-df5cea26]::-moz-placeholder{color:var(--v-text-darken2)}.twitter-container .tweet-form__textarea[data-v-df5cea26]::placeholder{color:var(--v-text-darken2)}.twitter-container .tweet-form__control[data-v-df5cea26]{display:flex;align-items:center;height:32px;margin-top:6px}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control[data-v-df5cea26]{height:26px}}.twitter-container .tweet-form__control .account-button[data-v-df5cea26]{display:flex;align-items:center;width:183px;height:100%;border-radius:7px;font-size:13px;color:var(--v-text-base);background:var(--v-background-lighten2);-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .account-button[data-v-df5cea26]{width:156px;border-radius:5px;font-size:11px}}.twitter-container .tweet-form__control .account-button--no-login .account-button__screen-name[data-v-df5cea26]{font-weight:500}.twitter-container .tweet-form__control .account-button--no-login .account-button__menu[data-v-df5cea26]{display:none}.twitter-container .tweet-form__control .account-button__icon[data-v-df5cea26]{display:block;width:32px;height:100%;border-radius:7px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2))}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .account-button__icon[data-v-df5cea26]{width:26px}}.twitter-container .tweet-form__control .account-button__screen-name[data-v-df5cea26]{flex-grow:1;line-height:2;text-align:center;font-weight:700}.twitter-container .tweet-form__control .account-button__menu[data-v-df5cea26]{margin-right:4px}.twitter-container .tweet-form__control .limit-meter[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;flex-direction:column;flex-grow:1;row-gap:.5px;font-size:10px;color:var(--v-text-darken1);-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .limit-meter[data-v-df5cea26]{font-size:9px}}.twitter-container .tweet-form__control .limit-meter__content[data-v-df5cea26]{display:flex;align-items:center;justify-content:center}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .limit-meter__content[data-v-df5cea26]:nth-child(2){margin-top:-2.5px}}.twitter-container .tweet-form__control .limit-meter__content svg[data-v-df5cea26]{width:14px}.twitter-container .tweet-form__control .limit-meter__content span[data-v-df5cea26]{width:16px;margin-left:5px;text-align:center;font-weight:700}.twitter-container .tweet-form__control .limit-meter__content--yellow[data-v-df5cea26]{color:var(--v-warning-base)}.twitter-container .tweet-form__control .limit-meter__content--red[data-v-df5cea26]{color:var(--v-error-base)}.twitter-container .tweet-form__control .tweet-button[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;width:94px;height:100%;border-radius:7px;font-size:12.5px;line-height:2;color:var(--v-text-base);background:var(--v-twitter-base);-webkit-user-select:none;-moz-user-select:none;user-select:none;outline:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .tweet-form__control .tweet-button[data-v-df5cea26]{width:86px;border-radius:5px;font-size:11.8px}}.twitter-container .tweet-form__control .tweet-button[disabled][data-v-df5cea26]{opacity:.7;cursor:auto}.twitter-container .twitter-account-list[data-v-df5cea26]{position:absolute;left:12px;right:12px;bottom:48px;max-height:calc(100vh - 137px);max-height:calc(100dvh - 137px);border-radius:7px;-webkit-clip-path:inset(0 0 0 0 round 7px);clip-path:inset(0 0 0 0 round 7px);background:var(--v-background-lighten2);box-shadow:0 3px 4px rgba(0,0,0,.53);transition:opacity .2s ease,visibility .2s ease;opacity:0;visibility:hidden;overflow-y:auto;z-index:3}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list[data-v-df5cea26]{left:8px;right:8px;bottom:40px;max-height:calc(100vh - 104px);max-height:calc(100dvh - 104px)}}.twitter-container .twitter-account-list--display[data-v-df5cea26]{opacity:1;visibility:visible}.twitter-container .twitter-account-list[data-v-df5cea26]::-webkit-scrollbar-track{background:var(--v-background-lighten2)}.twitter-container .twitter-account-list[data-v-df5cea26]::-webkit-scrollbar-thumb,.twitter-container .twitter-account-list[data-v-df5cea26]::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.twitter-container .twitter-account-list .twitter-account[data-v-df5cea26]{display:flex;align-items:center;padding:12px 12px;border-radius:7px;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account[data-v-df5cea26]{padding:8px 12px}}.twitter-container .twitter-account-list .twitter-account__icon[data-v-df5cea26]{display:block;width:50px;height:50px;border-radius:50%}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__icon[data-v-df5cea26]{width:36px;height:36px}}.twitter-container .twitter-account-list .twitter-account__info[data-v-df5cea26]{display:flex;flex-direction:column;flex-grow:1;min-width:0;margin-left:12px}.twitter-container .twitter-account-list .twitter-account__name[data-v-df5cea26]{font-size:17px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__name[data-v-df5cea26]{font-size:14px;line-height:1.3}}.twitter-container .twitter-account-list .twitter-account__screen-name[data-v-df5cea26]{color:var(--v-text-darken1);font-size:14px}@media(max-width:1000px)and (max-height:450px){.twitter-container .twitter-account-list .twitter-account__screen-name[data-v-df5cea26]{font-size:13px}}.twitter-container .twitter-account-list .twitter-account__check[data-v-df5cea26]{flex-shrink:0;color:var(--v-twitter-lighten1)}.twitter-container .hashtag-list[data-v-df5cea26]{position:absolute;left:12px;right:12px;bottom:149px;max-height:calc(100vh - 239px);max-height:calc(100dvh - 239px);padding:12px 4px;border-radius:7px;-webkit-clip-path:inset(0 0 0 0 round 7px);clip-path:inset(0 0 0 0 round 7px);background:var(--v-background-lighten2);box-shadow:0 3px 4px rgba(0,0,0,.53);transition:opacity .2s ease,visibility .2s ease;opacity:0;visibility:hidden;overflow-y:auto;z-index:2}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list[data-v-df5cea26]{left:8px;right:8px;bottom:110px;max-height:calc(100vh - 174px);max-height:calc(100dvh - 174px);padding:8px 4px}}.twitter-container .hashtag-list--display[data-v-df5cea26]{opacity:1;visibility:visible}.twitter-container .hashtag-list--virtual-keyboard-display[data-v-df5cea26]{bottom:calc(env(keyboard-inset-height, 0px) - 74px)!important;max-height:calc(100vh - env(keyboard-inset-height, 0px) - 16px)!important;max-height:calc(100dvh - env(keyboard-inset-height, 0px) - 16px)!important}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list--virtual-keyboard-display[data-v-df5cea26]{bottom:calc(env(keyboard-inset-height, 0px) - 48px)!important}}.twitter-container .hashtag-list[data-v-df5cea26]::-webkit-scrollbar-track{background:var(--v-background-lighten2)}.twitter-container .hashtag-list[data-v-df5cea26]::-webkit-scrollbar-thumb,.twitter-container .hashtag-list[data-v-df5cea26]::-webkit-scrollbar-thumb:hover{background:var(--v-gray-base)}.twitter-container .hashtag-list .hashtag-heading[data-v-df5cea26]{display:flex;align-items:center;font-weight:700;padding-left:8px;padding-right:4px}.twitter-container .hashtag-list .hashtag-heading__text[data-v-df5cea26]{display:flex;align-items:center;flex-grow:1;font-size:14px}.twitter-container .hashtag-list .hashtag-heading__add-button[data-v-df5cea26]{display:flex;align-items:center;font-size:13px;padding:4px 8px;border-radius:5px;outline:none;cursor:pointer}.twitter-container .hashtag-list .hashtag-container[data-v-df5cea26]{display:flex;flex-direction:column}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-df5cea26]{display:flex;align-items:center;padding-top:1.5px;padding-bottom:1.5px;padding-left:8px;padding-right:4px;border-radius:7px;transition:background-color .15s ease;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-df5cea26]{padding-top:0;padding-bottom:0}}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-df5cea26]:first-of-type{margin-top:6px}.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-df5cea26]:hover{background:hsla(0,0%,100%,.1)}@media(hover:none){.twitter-container .hashtag-list .hashtag-container .hashtag[data-v-df5cea26]:hover{background:transparent}}.twitter-container .hashtag-list .hashtag-container .hashtag--editing[data-v-df5cea26]:hover{background:transparent}.twitter-container .hashtag-list .hashtag-container .hashtag--editing .hashtag__input[data-v-df5cea26]{box-shadow:0 0 0 3.5px rgba(79,130,230,.6);cursor:text}.twitter-container .hashtag-list .hashtag-container .hashtag__input[data-v-df5cea26]{display:block;flex-grow:1;border-radius:2px;color:var(--v-twitter-lighten2);font-size:12.5px;outline:none;cursor:pointer;transition:box-shadow .09s ease}.twitter-container .hashtag-list .hashtag-container .hashtag__edit-button[data-v-df5cea26]{margin-left:4px}.twitter-container .hashtag-list .hashtag-container .hashtag__delete-button[data-v-df5cea26],.twitter-container .hashtag-list .hashtag-container .hashtag__edit-button[data-v-df5cea26],.twitter-container .hashtag-list .hashtag-container .hashtag__sort-handle[data-v-df5cea26]{display:flex;align-items:center;justify-content:center;width:19px;height:27px;border-radius:5px;outline:none;cursor:pointer}.twitter-container .hashtag-list .hashtag-container .hashtag__sort-handle[data-v-df5cea26]{cursor:move}.watch-player__dplayer svg circle,.watch-player__dplayer svg path{fill:var(--v-text-base)!important}.watch-player__dplayer .dplayer-video-wrap{background:transparent!important}.watch-player__dplayer .dplayer-video-wrap .dplayer-video-wrap-aspect{transition:opacity .4s cubic-bezier(.4,.38,.49,.94);opacity:1}.watch-player__dplayer .dplayer-video-wrap .dplayer-danmaku{max-width:100%;max-height:calc(100% - var(--comment-area-vertical-margin, 0px));aspect-ratio:var(--comment-area-aspect-ratio,16/9);transition:max-height .5s cubic-bezier(.42,.19,.53,.87),aspect-ratio .5s cubic-bezier(.42,.19,.53,.87);will-change:aspect-ratio;overflow:hidden}.watch-player__dplayer .dplayer-video-wrap .dplayer-danloading,.watch-player__dplayer .dplayer-video-wrap .dplayer-loading-icon{display:none!important}.watch-player__dplayer .dplayer-controller-mask{height:82px!important;background:linear-gradient(to bottom,transparent,var(--v-background-base))!important;opacity:0!important;visibility:hidden;transition:opacity .3s ease,visibility .3s ease!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-controller-mask{height:66px!important}}.watch-player__dplayer .dplayer-controller{padding-left:86px!important;padding-bottom:6px!important;transition:opacity .3s ease,visibility .3s ease;opacity:0!important;visibility:hidden}.watch-player__dplayer .dplayer-controller .dplayer-live-badge,.watch-player__dplayer .dplayer-controller .dplayer-time{color:var(--v-text-base)!important}.watch-player__dplayer .dplayer-controller .dplayer-volume-bar{background:var(--v-text-base)!important}.watch-player__dplayer .dplayer-controller .dplayer-icons{bottom:auto!important}.watch-player__dplayer .dplayer-controller .dplayer-icons.dplayer-icons-right{right:22px!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-controller .dplayer-icons.dplayer-icons-right{right:11px!important}}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-full-in-icon{display:none!important}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon{transition:background-color .08s ease;border-radius:6px}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon.dplayer-capturing,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon.dplayer-capturing{background:var(--v-secondary-lighten1)}.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-capture-icon.dplayer-capturing .dplayer-icon-content,.watch-player__dplayer .dplayer-controller .dplayer-icons .dplayer-icon.dplayer-comment-capture-icon.dplayer-capturing .dplayer-icon-content{opacity:1}.watch-player__dplayer .dplayer-controller .dplayer-comment-box{transition:opacity .3s ease,visibility .3s ease!important}.watch-player__dplayer .dplayer-controller .dplayer-comment-box .dplayer-comment-input{transition:box-shadow .09s ease}.watch-player__dplayer .dplayer-controller .dplayer-comment-box .dplayer-comment-input:focus{box-shadow:0 0 0 3.5px rgba(79,130,230,.6)}.watch-player__dplayer .dplayer-notice{padding:16px 22px!important;margin-right:30px;border-radius:4px!important;font-size:15px!important;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer .dplayer-notice{padding:12px 16px!important;margin-right:16px;font-size:13.5px!important}}.watch-player__dplayer .dplayer-info-panel{transition:top .3s,left .3s}.watch-player__dplayer .dplayer-setting-box .dplayer-setting-audio-panel .dplayer-setting-audio-item.dplayer-setting-audio-item--disabled{pointer-events:none}.watch-player__dplayer .dplayer-setting-box .dplayer-setting-audio-panel .dplayer-setting-audio-item.dplayer-setting-audio-item--disabled .dplayer-label{color:#aaa}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-title{color:var(--v-text-base)}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-type span{border:1px solid --v-text-base}.watch-player__dplayer .dplayer-comment-setting-box .dplayer-comment-setting-type input:checked+span{background:var(--v-text-base)}.watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:98px!important}@media(max-width:1000px)and (max-height:450px){.watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:74px!important}}.watch-player__dplayer.dplayer-mobile.dplayer-hide-controller .dplayer-controller{transform:none!important}.watch-player--loading .dplayer-video-wrap-aspect{opacity:0!important}:root .dplayer-icon:hover .dplayer-icon-content,_::-webkit-full-page-media,_:future{opacity:.8!important}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask{opacity:1!important;visibility:visible!important}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller .dplayer-comment-box,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask .dplayer-comment-box{left:88px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller .dplayer-comment-box,.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-controller-mask .dplayer-comment-box{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:98px;bottom:62px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-info-panel{top:82px;left:98px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-info-panel{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-comment-setting-box{left:88px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-comment-setting-box{left:84px;left:72px}}.watch-container.watch-container--control-display .watch-player__dplayer .dplayer-mobile .dplayer-mobile-icon-wrap{opacity:.7!important;visibility:visible!important}.watch-container:not(.watch-container--control-display) .watch-player__dplayer .dplayer-danmaku{max-height:100%!important;aspect-ratio:16/9!important}.watch-container:not(.watch-container--control-display) .watch-player__dplayer .dplayer-notice{bottom:20px!important}.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-controller{padding-left:20px!important}.watch-container.watch-container--fullscreen .watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:30px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-player__dplayer.dplayer-mobile .dplayer-controller{padding-left:16px!important}}.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-box,.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-setting-box{left:20px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-box,.watch-container.watch-container--fullscreen .watch-player__dplayer .dplayer-comment-setting-box{left:16px!important}}.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-info-panel,.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:30px!important}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-info-panel,.watch-container.watch-container--fullscreen.watch-container--control-display .watch-player__dplayer .dplayer-notice{left:16px!important}}.watch-player.watch-player--virtual-keyboard-display .watch-player__dplayer .dplayer-controller-mask,.watch-player.watch-player--virtual-keyboard-display .watch-player__dplayer .dplayer-icons.dplayer-comment-box{position:absolute;bottom:env(keyboard-inset-height,0)!important}.shortcut-key[data-v-6a0c19bf]{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;min-width:32px;min-height:28px;padding:3px 8px;border-radius:5px;background-color:var(--v-background-lighten2);font-size:14.5px;text-align:center}.shortcut-key-plus[data-v-6a0c19bf]{display:inline-block;margin:0 5px;flex-shrink:0}.route-container[data-v-6a0c19bf]{height:100vh!important;height:100dvh!important;background:var(--v-black-base)!important;overflow:hidden}@supports(-webkit-touch-callout:none){.route-container[data-v-6a0c19bf]{height:-webkit-fill-available!important}}.watch-container[data-v-6a0c19bf]{display:flex;width:calc(100% + 352px);height:100%;transition:width .4s cubic-bezier(.26,.68,.55,.99)}@media(max-width:1000px)and (max-height:450px){.watch-container[data-v-6a0c19bf]{width:calc(100% + 310px)}}.watch-container.watch-container--control-display .watch-content[data-v-6a0c19bf]{cursor:auto!important}.watch-container.watch-container--control-display .watch-header[data-v-6a0c19bf],.watch-container.watch-container--control-display .watch-navigation[data-v-6a0c19bf],.watch-container.watch-container--control-display .watch-player__button[data-v-6a0c19bf]{opacity:1!important;visibility:visible!important}.watch-container.watch-container--panel-display[data-v-6a0c19bf]{width:100%}.watch-container.watch-container--panel-display .switch-button-panel .switch-button-icon[data-v-6a0c19bf]{color:var(--v-primary-base)}.watch-container.watch-container--fullscreen .watch-navigation[data-v-6a0c19bf]{display:none}.watch-container.watch-container--fullscreen .watch-content .watch-header[data-v-6a0c19bf]{padding-left:30px}@media(max-width:1000px)and (max-height:450px){.watch-container.watch-container--fullscreen .watch-content .watch-header[data-v-6a0c19bf]{padding-left:16px}}.watch-container .watch-navigation[data-v-6a0c19bf]{display:flex;flex-direction:column;position:fixed;width:68px;top:0;left:0;bottom:-100px;padding:18px 8px 122px;background:#2f221f80;transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden;z-index:2}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation[data-v-6a0c19bf]{width:56px;padding:18px 6px 122px}}.watch-container .watch-navigation .watch-navigation__icon[data-v-6a0c19bf]{display:flex;justify-content:center;align-items:center;height:52px;margin-bottom:17px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__icon[data-v-6a0c19bf]{height:32px;border-radius:10px}.watch-container .watch-navigation div.spacer[data-v-6a0c19bf]{display:none}}.watch-container .watch-navigation .watch-navigation__link[data-v-6a0c19bf]{display:flex;justify-content:center;align-items:center;height:52px;border-radius:11px;font-size:16px;color:var(--v-text-base);transition:background-color .15s;text-decoration:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link[data-v-6a0c19bf]{height:44px;border-radius:10px}.watch-container .watch-navigation .watch-navigation__link[data-v-6a0c19bf]:last-child,.watch-container .watch-navigation .watch-navigation__link[data-v-6a0c19bf]:nth-last-child(2){display:none}}.watch-container .watch-navigation .watch-navigation__link[data-v-6a0c19bf]:hover{background:#433532a0}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link-icon[data-v-6a0c19bf]{width:26px;height:26px}}.watch-container .watch-navigation .watch-navigation__link--active[data-v-6a0c19bf]{color:var(--v-primary-base);background:#433532a0}.watch-container .watch-navigation .watch-navigation__link+.watch-navigation__link[data-v-6a0c19bf]{margin-top:4px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-navigation .watch-navigation__link+.watch-navigation__link[data-v-6a0c19bf]{margin-top:auto}}.watch-container .watch-content[data-v-6a0c19bf]{display:flex;position:relative;width:100%;cursor:none}.watch-container .watch-content .watch-header[data-v-6a0c19bf]{display:flex;align-items:center;position:absolute;top:0;left:0;width:100%;height:82px;padding-left:98px;padding-right:30px;background:linear-gradient(to bottom,var(--v-background-base),transparent);transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden;z-index:1}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header[data-v-6a0c19bf]{padding-left:84px;padding-right:16px;height:66px;padding-left:72px}}.watch-container .watch-content .watch-header .watch-header__broadcaster[data-v-6a0c19bf]{display:inline-block;flex-shrink:0;width:64px;height:36px;border-radius:5px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__broadcaster[data-v-6a0c19bf]{width:48px;height:28px;border-radius:4px}}.watch-container .watch-content .watch-header .watch-header__program-title[data-v-6a0c19bf]{margin-left:18px;font-size:18px;font-weight:700;font-feature-settings:"palt" 1;letter-spacing:.05em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__program-title[data-v-6a0c19bf]{margin-left:12px;font-size:16px}}.watch-container .watch-content .watch-header .watch-header__program-time[data-v-6a0c19bf]{flex-shrink:0;margin-left:16px;font-size:15px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__program-time[data-v-6a0c19bf]{margin-left:8px;font-size:14px}}.watch-container .watch-content .watch-header .watch-header__now[data-v-6a0c19bf]{flex-shrink:0;margin-left:16px;font-size:13px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-header .watch-header__now[data-v-6a0c19bf]{display:none}}.watch-container .watch-content .watch-player[data-v-6a0c19bf]{display:flex;position:relative;width:100%;height:100%;background-size:contain;background-position:50%}.watch-container .watch-content .watch-player .watch-player__background[data-v-6a0c19bf]{position:absolute;top:50%;left:50%;width:100%;max-height:100%;padding-top:min(56.25%,100vh);padding-top:min(56.25%,100dvh);aspect-ratio:16/9;background-blend-mode:overlay;background-color:rgba(14,14,18,.35);background-size:cover;background-image:none;transform:translate(-50%,-50%);opacity:0;visibility:hidden;transition:opacity .4s cubic-bezier(.4,.38,.49,.94),visibility .4s cubic-bezier(.4,.38,.49,.94);will-change:opacity}.watch-container .watch-content .watch-player .watch-player__background--display[data-v-6a0c19bf]{opacity:1;visibility:visible}.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-6a0c19bf]{display:inline-block;position:absolute;height:34px;right:56px;bottom:44px;filter:drop-shadow(0 0 5px var(--v-black-base))}@media(max-width:1264px)and (max-height:850px){.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-6a0c19bf]{height:30px;right:34px;bottom:30px}}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__background .watch-player__background-logo[data-v-6a0c19bf]{height:25px;right:30px;bottom:24px}}.watch-container .watch-content .watch-player .watch-player__buffering[data-v-6a0c19bf]{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--v-background-lighten3);filter:drop-shadow(0 0 3px rgba(0,0,0,.3));opacity:0;visibility:hidden;transition:opacity .2s cubic-bezier(.4,.38,.49,.94),visibility .2s cubic-bezier(.4,.38,.49,.94);will-change:opacity;z-index:3}.watch-container .watch-content .watch-player .watch-player__buffering--display[data-v-6a0c19bf]{opacity:1;visibility:visible}.watch-container .watch-content .watch-player .watch-player__dplayer[data-v-6a0c19bf]{width:100%}.watch-container .watch-content .watch-player .watch-player__button[data-v-6a0c19bf]{display:flex;justify-content:space-around;flex-direction:column;position:absolute;top:50%;right:28px;height:190px;transform:translateY(-50%);opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button[data-v-6a0c19bf]{right:15px;height:155px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-6a0c19bf]{display:flex;justify-content:center;align-items:center;width:48px;height:48px;color:var(--v-text-base);background:#2f221fc0;border-radius:7px;transition:background-color .15s;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-6a0c19bf]{width:38px;height:38px;border-radius:5px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-6a0c19bf]:hover{background:#2f221ff0}@media(hover:none){.watch-container .watch-content .watch-player .watch-player__button .switch-button[data-v-6a0c19bf]:hover{background:#2f221fc0}}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-content .watch-player .watch-player__button .switch-button svg[data-v-6a0c19bf]{height:27px}}.watch-container .watch-content .watch-player .watch-player__button .switch-button .switch-button-icon[data-v-6a0c19bf]{position:relative}.watch-container .watch-content .watch-player .watch-player__button .switch-button-up>.switch-button-icon[data-v-6a0c19bf]{top:6px}.watch-container .watch-content .watch-player .watch-player__button .switch-button-panel>.switch-button-icon[data-v-6a0c19bf]{top:1.5px;transition:color .4s cubic-bezier(.26,.68,.55,.99)}.watch-container .watch-content .watch-player .watch-player__button .switch-button-down>.switch-button-icon[data-v-6a0c19bf]{bottom:4px}.watch-container .watch-panel[data-v-6a0c19bf]{display:flex;flex-direction:column;flex-shrink:0;width:352px;height:100%;background:var(--v-background-base)}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel[data-v-6a0c19bf]{width:310px}}.watch-container .watch-panel .watch-panel__header[data-v-6a0c19bf]{display:flex;align-items:center;flex-shrink:0;width:100%;height:70px;padding-left:16px;padding-right:16px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header[data-v-6a0c19bf]{display:none}}.watch-container .watch-panel .watch-panel__header .panel-close-button[data-v-6a0c19bf]{display:flex;position:relative;align-items:center;flex-shrink:0;left:-4px;height:35px;padding:0 4px;border-radius:5px;font-size:16px;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-close-button[data-v-6a0c19bf]{font-size:14px}}.watch-container .watch-panel .watch-panel__header .panel-close-button__icon[data-v-6a0c19bf]{position:relative;left:-4px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-close-button__icon[data-v-6a0c19bf]{height:22px}}.watch-container .watch-panel .watch-panel__header .panel-close-button__text[data-v-6a0c19bf]{font-weight:700}.watch-container .watch-panel .watch-panel__header .panel-broadcaster[data-v-6a0c19bf]{display:flex;align-items:center;min-width:0;margin-left:16px}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__icon[data-v-6a0c19bf]{display:inline-block;flex-shrink:0;width:43px;height:24px;border-radius:3px;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));-o-object-fit:cover;object-fit:cover;-webkit-user-select:none;-moz-user-select:none;user-select:none}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__icon[data-v-6a0c19bf]{width:38px;height:22px}}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__number[data-v-6a0c19bf]{flex-shrink:0;margin-left:8px;font-size:16px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__number[data-v-6a0c19bf]{font-size:14px}}.watch-container .watch-panel .watch-panel__header .panel-broadcaster__name[data-v-6a0c19bf]{margin-left:5px;font-size:16px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__header .panel-broadcaster__name[data-v-6a0c19bf]{font-size:14px}}.watch-container .watch-panel .watch-panel__content-container[data-v-6a0c19bf]{position:relative;height:100%}.watch-container .watch-panel .watch-panel__content-container .watch-panel__content[data-v-6a0c19bf]{position:absolute;top:0;left:0;right:0;bottom:0;background:var(--v-background-base);transition:opacity .3s,visibility .3s;opacity:0;visibility:hidden}.watch-container .watch-panel .watch-panel__content-container .watch-panel__content--active[data-v-6a0c19bf]{opacity:1;visibility:visible}.watch-container .watch-panel .watch-panel__navigation[data-v-6a0c19bf]{display:flex;align-items:center;justify-content:space-evenly;flex-shrink:0;height:77px;background:var(--v-background-lighten1)}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation[data-v-6a0c19bf]{height:56px}}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button[data-v-6a0c19bf]{display:flex;justify-content:center;align-items:center;flex-direction:column;width:77px;height:56px;padding:6px 0;border-radius:5px;color:var(--v-text-base);box-sizing:content-box;transition:color .3s;-webkit-user-select:none;-moz-user-select:none;user-select:none;cursor:pointer}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button[data-v-6a0c19bf]{height:42px;padding:6px 0 4px}}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button--active[data-v-6a0c19bf]{color:var(--v-primary-base)}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__icon[data-v-6a0c19bf]{height:30px}.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__text[data-v-6a0c19bf]{margin-top:5px;font-size:13px}@media(max-width:1000px)and (max-height:450px){.watch-container .watch-panel .watch-panel__navigation .panel-navigation-button__text[data-v-6a0c19bf]{margin-top:2px;font-size:12px}}.settings-container[data-v-036b263a]{width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.settings-container[data-v-036b263a]{padding:16px 20px!important}}@media(max-width:680px)and (max-height:450px){.settings-container[data-v-036b263a]{padding:16px 16px!important}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings-navigation h1[data-v-036b263a]{font-size:22px!important}.settings-container .settings-navigation .v-navigation-drawer__content .v-list[data-v-036b263a]{margin-top:4px!important;padding:0!important}}@media(max-width:680px)and (max-height:450px){.settings-container .settings-navigation .v-navigation-drawer__content .v-list[data-v-036b263a]{margin-top:0!important}}.settings-container .settings-navigation .v-list-item--link[data-v-036b263a],.settings-container .settings-navigation .v-list-item--link[data-v-036b263a]:before{border-radius:11px!important;margin-bottom:0!important}.settings-container[data-v-03345d7e]{width:100%;min-width:0}@media(max-width:1000px)and (max-height:450px){.settings-container[data-v-03345d7e]{padding:16px 20px!important}}@media(max-width:680px)and (max-height:450px){.settings-container[data-v-03345d7e]{padding:16px 16px!important}}.settings-container .settings-navigation[data-v-03345d7e]{position:sticky;top:85px!important}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings-navigation[data-v-03345d7e]{display:none}}.settings-container .settings-navigation .v-list-item--link[data-v-03345d7e],.settings-container .settings-navigation .v-list-item--link[data-v-03345d7e]:before{border-radius:11px!important}.settings-container .settings[data-v-03345d7e]{width:100%;min-width:0;border-radius:11px!important}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e]{margin-left:0!important}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e]{padding:20px!important;margin-left:0!important}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e]{margin-left:0!important}}.settings-container .settings[data-v-03345d7e] .settings__heading{display:flex;align-items:center;font-size:22px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading{font-size:20px}}.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:none;position:relative;left:-6px;padding:6px;border-radius:50%;color:var(--v-text-base)}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button{display:flex}}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg{display:none}}@media(max-width:850px)and (min-height:850.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}@media(max-width:600px)and (min-height:450.01px){.settings-container .settings[data-v-03345d7e] .settings__heading .settings__back-button+svg+span{margin-left:0!important}}.settings-container .settings[data-v-03345d7e] .settings__content{margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content{margin-top:16px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item{display:flex;position:relative;flex-direction:column;margin-top:24px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item{margin-top:16px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--sync-disabled .settings__item-heading:after{content:"デバイス間同期無効";display:flex;flex-shrink:0;position:relative;align-items:center;padding:2px 4px;margin-left:auto;border-radius:4px;background:var(--v-background-lighten2);font-size:11px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--switch{margin-right:62px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--switch .settings__item-heading{width:calc(100% + 62px)}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item--disabled{opacity:.5}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-heading{display:flex;align-items:center;color:var(--v-text-base);font-size:16.5px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-heading{font-size:15px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-label{margin-top:8px;color:var(--v-text-darken1);font-size:13.5px;line-height:1.6}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-label{font-size:11px;line-height:1.7}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-form{margin-top:14px}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-form{font-size:13.5px}}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item-switch{align-items:center;position:absolute;top:0;right:-74px;bottom:0;margin-top:0}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item p{margin-bottom:8px}.settings-container .settings[data-v-03345d7e] .settings__content .settings__item p:last-of-type{margin-bottom:0}.settings-container .settings[data-v-03345d7e] .settings__content .settings__save-button{height:45px;background:var(--v-background-lighten2);font-size:15.5px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.settings-container .settings[data-v-03345d7e] .settings__content .settings__save-button{height:40px;padding:0 12px;font-size:14px}}.settings__content[data-v-12036e32]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-12036e32]{opacity:0}.account[data-v-12036e32]{display:flex;align-items:center;height:130px;padding:18px 20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1264px)and (max-height:850px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:850px)and (min-height:850.01px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:1000px)and (max-height:450px){.account[data-v-12036e32]{align-items:normal;flex-direction:column;height:auto;padding:16px}}.account-wrapper[data-v-12036e32]{display:flex;align-items:center;min-width:0;height:94px}@media(max-width:1000px)and (max-height:450px){.account-wrapper[data-v-12036e32]{height:80px}}.account__icon[data-v-12036e32]{flex-shrink:0;min-width:94px;height:100%;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}@media(max-width:1000px)and (max-height:450px){.account__icon[data-v-12036e32]{min-width:80px}}.account__info[data-v-12036e32]{display:flex;flex-direction:column;min-width:0;margin-left:20px;margin-right:12px}.account__info-name[data-v-12036e32]{display:inline-flex;align-items:center;height:33px}.account__info-name-text[data-v-12036e32]{display:inline-block;font-size:23px;color:var(--v-text-base);font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.account__info-name-text[data-v-12036e32]{font-size:21px}}.account__info-admin[data-v-12036e32]{display:flex;align-items:center;justify-content:center;flex-shrink:0;width:52px;height:28px;margin-left:10px;border-radius:5px;background:var(--v-secondary-base);font-size:14px;font-weight:500;line-height:2}@media(max-width:1000px)and (max-height:450px){.account__info-admin[data-v-12036e32]{width:45px;height:24px;border-radius:4px;font-size:11.5px}}.account__info-id[data-v-12036e32]{display:inline-block;margin-top:2px;color:var(--v-text-darken1);font-size:16px}@media(max-width:1000px)and (max-height:450px){.account__info-id[data-v-12036e32]{font-size:14.5px}}.account__login[data-v-12036e32]{border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:1264px)and (max-height:850px){.account__login[data-v-12036e32]{height:50px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:850px)and (min-height:850.01px){.account__login[data-v-12036e32]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.account__login[data-v-12036e32]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}.account-register[data-v-12036e32]{display:flex;flex-direction:column;margin-top:28px}.account-register__heading[data-v-12036e32]{font-size:21px;font-weight:700;text-align:center;font-feature-settings:"palt" 1;letter-spacing:.04em}@media(max-width:1000px)and (max-height:450px){.account-register__heading[data-v-12036e32]{font-size:19px}}.account-register__feature[data-v-12036e32]{display:grid;grid-template-columns:1fr 1fr;grid-row-gap:18px;grid-column-gap:16px;margin-top:28px}@media(max-width:1264px)and (max-height:850px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}@media(max-width:850px)and (min-height:850.01px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}@media(max-width:1000px)and (max-height:450px){.account-register__feature[data-v-12036e32]{grid-template-columns:1fr}}.account-register__feature .account-feature[data-v-12036e32]{display:flex;align-items:center}.account-register__feature .account-feature__icon[data-v-12036e32]{width:46px;height:46px;flex-shrink:0;margin-right:16px;color:var(--v-secondary-lighten1)}.account-register__feature .account-feature__info[data-v-12036e32]{display:flex;flex-direction:column}.account-register__feature .account-feature__info-heading[data-v-12036e32]{font-size:15px}.account-register__feature .account-feature__info-text[data-v-12036e32]{margin-top:3px;color:var(--v-text-darken1);font-size:12.5px;line-height:1.65}.account-register__description[data-v-12036e32]{margin-top:32px;font-size:15px;line-height:1.7;text-align:center}@media(max-width:1264px)and (max-height:850px){.account-register__description[data-v-12036e32]{font-size:12.5px}}@media(max-width:850px)and (min-height:850.01px){.account-register__description[data-v-12036e32]{font-size:10.5px}}@media(max-width:1000px)and (max-height:450px){.account-register__description[data-v-12036e32]{font-size:12.5px}}@media(max-width:680px)and (max-height:450px){.account-register__description[data-v-12036e32]{font-size:10.5px}}.account-register__button[data-v-12036e32]{margin-top:24px;margin-left:auto;margin-right:auto;border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:850px)and (min-height:850.01px){.account-register__button[data-v-12036e32]{height:42px!important;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.account-register__button[data-v-12036e32]{height:42px!important;font-size:14.5px}}.settings__content[data-v-786083d5]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-786083d5]{opacity:0}.niconico-account[data-v-786083d5]{display:flex;align-items:center;height:120px;padding:20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1264px)and (max-height:850px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}}@media(max-width:850px)and (min-height:850.01px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:16px!important;margin-right:0!important}.niconico-account .niconico-account-wrapper .niconico-account__info-name-text[data-v-786083d5]{font-size:18.5px}.niconico-account .niconico-account-wrapper .niconico-account__info-description[data-v-786083d5]{font-size:13.5px}}@media(max-width:1000px)and (max-height:450px){.niconico-account[data-v-786083d5]{align-items:normal;flex-direction:column;height:auto;padding:16px}.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-right:0!important}}@media(max-width:680px)and (max-height:450px){.niconico-account .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:16px!important}.niconico-account .niconico-account-wrapper .niconico-account__info-name-text[data-v-786083d5]{font-size:18px}.niconico-account .niconico-account-wrapper .niconico-account__info-description[data-v-786083d5]{font-size:13px}}@media(max-width:850px)and (min-height:850.01px){.niconico-account--anonymous .niconico-account__login[data-v-786083d5]{margin-top:12px}}@media(max-width:1000px)and (max-height:450px){.niconico-account--anonymous .niconico-account__login[data-v-786083d5]{margin-top:12px}}@media(max-width:680px)and (max-height:450px){.niconico-account--anonymous .niconico-account-wrapper svg[data-v-786083d5]{display:none}.niconico-account--anonymous .niconico-account-wrapper .niconico-account__info[data-v-786083d5]{margin-left:0!important}}.niconico-account-wrapper[data-v-786083d5]{display:flex;align-items:center;min-width:0;height:80px}.niconico-account__icon[data-v-786083d5]{flex-shrink:0;min-width:80px;height:100%;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}.niconico-account__info[data-v-786083d5]{display:flex;flex-direction:column;min-width:0;margin-left:20px;margin-right:16px}.niconico-account__info-name[data-v-786083d5]{display:inline-flex;align-items:center;height:33px}.niconico-account__info-name-text[data-v-786083d5]{display:inline-block;font-size:20px;color:var(--v-text-base);font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.niconico-account__info-description[data-v-786083d5]{display:inline-block;margin-top:4px;color:var(--v-text-darken1);font-size:14px}.niconico-account__login[data-v-786083d5]{border-radius:7px;font-size:16px;letter-spacing:0}@media(max-width:1264px)and (max-height:850px){.niconico-account__login[data-v-786083d5]{height:50px!important;margin-top:8px;margin-right:auto}}@media(max-width:850px)and (min-height:850.01px){.niconico-account__login[data-v-786083d5]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.niconico-account__login[data-v-786083d5]{height:42px!important;margin-top:8px;margin-right:auto;font-size:14.5px}}.settings__content[data-v-1970b264]{opacity:1;transition:opacity .4s}.settings__content--loading[data-v-1970b264]{opacity:0}.twitter-accounts[data-v-1970b264]{display:flex;flex-direction:column;padding:20px 20px;border-radius:15px;background:var(--v-background-lighten2)}@media(max-width:1000px)and (max-height:450px){.twitter-accounts[data-v-1970b264]{padding:16px 20px}}.twitter-accounts__heading[data-v-1970b264]{display:flex;align-items:center;font-size:18px;font-weight:700}.twitter-accounts__guide[data-v-1970b264]{display:flex;align-items:center}@media(max-width:850px)and (min-height:850.01px){.twitter-accounts__guide .text-h6[data-v-1970b264]{font-size:19px!important}}@media(max-width:680px)and (max-height:450px){.twitter-accounts__guide svg[data-v-1970b264]{display:none}.twitter-accounts__guide svg+div[data-v-1970b264]{margin-left:0!important}}.twitter-accounts .twitter-account[data-v-1970b264]{display:flex;align-items:center;margin-top:20px}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account[data-v-1970b264]{margin-top:16px}}.twitter-accounts .twitter-account__icon[data-v-1970b264]{flex-shrink:0;width:70px;height:70px;margin-right:16px;border-radius:50%;-o-object-fit:cover;object-fit:cover;background:linear-gradient(150deg,var(--v-gray-base),var(--v-background-lighten2));image-rendering:-webkit-optimize-contrast}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__icon[data-v-1970b264]{width:52px;height:52px}}.twitter-accounts .twitter-account__info[data-v-1970b264]{display:flex;flex-direction:column;min-width:0;margin-right:16px}.twitter-accounts .twitter-account__info-name[data-v-1970b264]{display:inline-flex;align-items:center}.twitter-accounts .twitter-account__info-name-text[data-v-1970b264]{display:inline-block;color:var(--v-text-base);font-size:20px;font-weight:700;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__info-name-text[data-v-1970b264]{font-size:18px}}.twitter-accounts .twitter-account__info-screen-name[data-v-1970b264]{display:inline-block;color:var(--v-text-darken1);font-size:16px}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__info-screen-name[data-v-1970b264]{font-size:14px}}.twitter-accounts .twitter-account__login[data-v-1970b264]{margin-top:20px;margin-left:auto;margin-right:auto;border-radius:7px;font-size:15px;letter-spacing:0}@media(max-width:850px)and (min-height:850.01px){.twitter-accounts .twitter-account__login[data-v-1970b264]{height:42px!important;font-size:14.5px}}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__login[data-v-1970b264]{height:42px!important;font-size:14.5px}}.twitter-accounts .twitter-account__logout[data-v-1970b264]{background:var(--v-gray-base);border-radius:7px;font-size:15px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.twitter-accounts .twitter-account__logout[data-v-1970b264]{width:116px!important}.login-container-wrapper[data-v-0c2bb32a]{padding:20px!important;margin-bottom:0!important}}.login-container-wrapper .login-container[data-v-0c2bb32a]{border-radius:11px}@media(max-width:1000px)and (max-height:450px){.login-container-wrapper .login-container[data-v-0c2bb32a]{padding:24px!important}.login-container-wrapper .login-container .login__logo[data-v-0c2bb32a]{padding-top:4px!important;padding-bottom:20px!important}.login-container-wrapper .login-container .login__logo .v-image[data-v-0c2bb32a]{max-width:200px!important}.login-container-wrapper .login-container .v-input[data-v-0c2bb32a]{margin-top:24px!important;font-size:14px!important}}.login-container-wrapper .login-container .login-button[data-v-0c2bb32a]{border-radius:7px;margin-top:48px!important;font-size:18px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.login-container-wrapper .login-container .login-button[data-v-0c2bb32a]{height:44px!important;margin-top:24px!important;font-size:16px}.register-container-wrapper[data-v-d0eaf0ae]{padding:20px!important;margin-bottom:0!important}}.register-container-wrapper .register-container[data-v-d0eaf0ae]{border-radius:11px}@media(max-width:1000px)and (max-height:450px){.register-container-wrapper .register-container[data-v-d0eaf0ae]{padding:24px!important}.register-container-wrapper .register-container .register__logo[data-v-d0eaf0ae]{padding-top:4px!important;padding-bottom:8px!important}.register-container-wrapper .register-container .register__logo .v-image[data-v-d0eaf0ae]{max-width:200px!important}.register-container-wrapper .register-container .register__logo h4[data-v-d0eaf0ae]{margin-top:16px!important;font-size:19px!important}.register-container-wrapper .register-container .v-input[data-v-d0eaf0ae]{margin-top:0!important;font-size:14px!important}.register-container-wrapper .register-container .v-input[data-v-d0eaf0ae]:first-child{margin-top:24px!important}}.register-container-wrapper .register-container .register-button[data-v-d0eaf0ae]{border-radius:7px;margin-top:48px!important;font-size:18px;letter-spacing:0}@media(max-width:1000px)and (max-height:450px){.register-container-wrapper .register-container .register-button[data-v-d0eaf0ae]{height:44px!important;margin-top:0!important;font-size:16px}}@media(max-width:850px)and (min-height:850.01px){h1[data-v-daa4530a]{font-size:24px!important}}@media(max-width:1000px)and (max-height:450px){h1[data-v-daa4530a]{font-size:24px!important}span[data-v-daa4530a]{font-size:15px!important}} \ No newline at end of file diff --git a/client/dist/assets/js/app.4d3619bb.js b/client/dist/assets/js/app.4d3619bb.js deleted file mode 100644 index 7518c6ca..00000000 --- a/client/dist/assets/js/app.4d3619bb.js +++ /dev/null @@ -1,2 +0,0 @@ -(function(){"use strict";var t={8435:function(t,e,s){var i=s(2856),a=s(5742),n=s(144),o=s(2346),r=s(4801),l=s(1797),c=s.n(l),_=s(9652),d=s(998),m=function(){var t=this,e=t._self._c;return e(d.Z,{attrs:{id:"app"}},[e("transition",[e("router-view")],1)],1)},u=[],p=s(1001),h={},g=(0,p.Z)(h,m,u,!1,null,null,null),v=g.exports,f=s(2165),w=f.Z.extend({render(t){return t("transition",{props:{name:this.computedTransition},on:{beforeEnter:this.onBeforeTransition,afterEnter:this.onAfterTransition,enterCancelled:this.onTransitionCancelled,beforeLeave:this.onBeforeTransition,afterLeave:this.onAfterTransition,leaveCancelled:this.onTransitionCancelled,enter:this.onEnter}},[this.genWindowItem()])}}),y=s(5352),b=s(8481),C=s(530),k=C.Z.extend({data(){return{items:[]}},methods:{register(t){const e=this.items[this.internalIndex];this.items.push(t),this.items.sort(((t,e)=>{const s=this.$slots.default.findIndex((e=>t.$vnode.key===e.key)),i=this.$slots.default.findIndex((t=>e.$vnode.key===t.key));return s-i})),t.$on("change",(()=>this.onClick(t))),this.mandatory&&!this.selectedValues.length&&this.updateMandatory(),this.updateItem(t,this.items.indexOf(t)),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))},unregister(t){const e=this.items[this.internalIndex];this.constructor.super.options.methods.unregister.call(this,t),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))}}}),x=b.Z.extend({methods:{genBar(t,e){const s={style:{height:(0,y.kb)(this.height)},props:{activeClass:this.activeClass,centerActive:this.centerActive,dark:this.dark,light:this.light,mandatory:!this.optional,mobileBreakpoint:this.mobileBreakpoint,nextIcon:this.nextIcon,prevIcon:this.prevIcon,showArrows:this.showArrows,value:this.internalValue},on:{"call:slider":this.callSlider,change:t=>{this.internalValue=t}},ref:"items"};return this.setTextColor(this.computedColor,s),this.setBackgroundColor(this.backgroundColor,s),this.$createElement(k,s,[this.genSlider(e),t])}}}),O=s(5085),S=O.Z.extend({data(){return{items:[]}},methods:{register(t){const e=this.items[this.internalIndex];this.items.push(t),this.items.sort(((t,e)=>{const s=this.$slots.default.findIndex((e=>t.$vnode.key===e.key)),i=this.$slots.default.findIndex((t=>e.$vnode.key===t.key));return s-i})),t.$on("change",(()=>this.onClick(t))),this.mandatory&&!this.selectedValues.length&&this.updateMandatory(),this.updateItem(t,this.items.indexOf(t)),void 0!==e&&this.items.indexOf(e)!==this.internalValue&&this.updateInternalValue(this.items.indexOf(e))},unregister(t){const e=this.items[this.internalIndex];this.constructor.super.options.methods.unregister.call(this,t),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))},updateReverse(t,e){const s=this.items.length,i=s-1;return s<=2?t":">"};return t.replace(/[&"'<>]/g,(t=>e[t]))}static getWindowFeatures(){const t=650,e=window.screen.height>=800?800:window.screen.height-100,s=(window.screen.height-e)/2,i=(window.screen.width-t)/2;return`toolbar=0,status=0,top=${s},left=${i},width=${t},height=${e},modal=yes,alwaysRaised=yes`}static hasActiveElementClass(t){return null!==document.activeElement&&document.activeElement.classList.contains(t)}static isSmartphoneHorizontal(){return window.matchMedia("(max-width: 1000px) and (max-height: 450px)").matches}static isSmartphoneVertical(){return window.matchMedia("(max-width: 600px) and (min-height: 450.01px)").matches}static isTabletHorizontal(){return window.matchMedia("(max-width: 1264px) and (max-height: 850px)").matches}static isTabletVertical(){return window.matchMedia("(max-width: 850px) and (min-height: 850.01px)").matches}static mathFloor(t,e=0){return Math.floor(t*10**e)/10**e}static async sleep(t){return await new Promise((e=>setTimeout(e,1e3*t)))}static time(){return Date.now()/1e3}static typeof(t){return Object.prototype.toString.call(t).slice(8,-1).toLowerCase()}static URLtoLink(t){t=I.escapeHTML(t);const e=/(https?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;return t.replace(e,'$1')}}I.version="0.6.0",I.api_base_url=(()=>`${window.location.protocol}//${window.location.host}/api`)(),I.default_settings={pinned_channel_ids:[],showed_panel_last_time:!0,selected_twitter_account_id:null,saved_twitter_hashtags:[],tv_streaming_quality:"1080p",tv_data_saver_mode:!1,tv_low_latency_mode:!0,tv_show_superimpose:!0,panel_display_state:"RestorePreviousState",tv_panel_active_tab:"Program",caption_font:"Windows TV MaruGothic",always_border_caption_text:!0,specify_caption_background_color:!1,caption_background_color:"#00000080",capture_copy_to_clipboard:!0,capture_save_mode:"Browser",capture_caption_mode:"Both",sync_settings:!1,comment_speed_rate:1,comment_font_size:34,comment_delay_time:1.75,close_comment_form_after_sending:!0,muted_comment_keywords:[],muted_niconico_user_ids:[],mute_vulgar_comments:!0,mute_abusive_discriminatory_prejudiced_comments:!0,mute_big_size_comments:!0,mute_fixed_comments:!1,mute_colored_comments:!1,mute_consecutive_same_characters_comments:!1,fold_panel_after_sending_tweet:!1,reset_hashtag_when_program_switches:!0,auto_add_watching_channel_hashtag:!0,twitter_active_tab:"Capture",tweet_hashtag_position:"Append",tweet_capture_watermark_position:"None"},I.sync_settings_keys=["pinned_channel_ids","saved_twitter_hashtags","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_save_mode","capture_caption_mode","comment_speed_rate","comment_font_size","close_comment_form_after_sending","muted_comment_keywords","muted_niconico_user_ids","mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments","fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position"],I.uploading_settings=!1;class T{static getChannelType(t,e=!1){const s=t.match("(?[a-z]+)[0-9]+").groups.channel_type.toUpperCase();if(!0!==e)return s;switch(s){case"GR":return"地デジ";case"STARDIGIO":return"StarDigio";default:return s}}static getChannelForceType(t){return null===t?"normal":t>=1e3?"festival":t>=200?"so-many":t>=100?"many":"normal"}static getChannelFromRemoconID(t,e,s){const i=e.replace("GR","地デジ").replace("STARDIGIO","StarDigio"),a=t.get(i);for(let n=0;n\n \n \n \n \n '),this.player_container.querySelector(".dplayer-icons.dplayer-icons-right").insertAdjacentHTML("afterbegin",'\n
\n \n \n \n
\n '),this.comment_capture_button=this.player_container.querySelector(".dplayer-comment-capture-icon"),this.capture_button=this.player_container.querySelector(".dplayer-capture-icon"),this.canvas="OffscreenCanvas"in window?new OffscreenCanvas(0,0):document.createElement("canvas"),this.canvas_context=this.canvas.getContext("2d",{alpha:!1,desynchronized:!0,willReadFrequently:!1}),this.canvas.width=0,this.canvas.height=0,t.on("loadedmetadata",(async()=>{this.canvas.width=t.video.videoWidth,this.canvas.height=t.video.videoHeight;while(0===this.canvas.width&&0===this.canvas.height)await I.sleep(.1),this.canvas.width=t.video.videoWidth,this.canvas.height=t.video.videoHeight}))}async captureAndSave(t,e){const s=I.time();if(!0===t.is_radiochannel)return void this.player.notice("ラジオチャンネルはキャプチャできません。");if(0===this.canvas.width&&0===this.canvas.height)return void this.player.notice("読み込み中はキャプチャできません。");if(!0===e&&!1===this.player.danmaku.showing)return void this.player.notice("コメントを付けてキャプチャするには、コメント表示をオンにしてください。");this.addHighlight(e);const i=`Capture_${A()().format("YYYYMMDD-HHmmss")}`,a=`${i}.jpg`,n=`${i}_caption.jpg`,o=this.player.plugins.aribb24Caption.getRawCanvas(),r=this.player.plugins.aribb24Superimpose.getRawCanvas(),l=!0===this.player.plugins.aribb24Caption.isShowing&&this.player.plugins.aribb24Caption.isPresent(),c=!0===this.player.plugins.aribb24Superimpose.isShowing&&this.player.plugins.aribb24Superimpose.isPresent(),_=l?this.player.plugins.aribb24Caption.getTextContent():null,d=async(t,e,s,i,a,n)=>{const o=I.time();let r;try{r=await this.exportToBlob(t)}catch(l){return this.player.notice("キャプチャの保存に失敗しました…"),!1}return console.log("[PlayerCaptureHandler] Export to Blob:",I.mathFloor(I.time()-o,3),"sec"),r=await this.setEXIFDataToCapture(r,s,i,a,n),["Browser","Both"].includes(I.getSettingsItem("capture_save_mode"))&&I.downloadBlobData(r,e),["UploadServer","Both"].includes(I.getSettingsItem("capture_save_mode"))&&this.uploadCaptureToServer(r,e),r};let m=null,u=null;const p=await createImageBitmap(this.player.video);if(!1!==e||!1!==c||!1!==l&&"VideoOnly"!==I.getSettingsItem("capture_caption_mode")){const s=[];this.canvas_context.drawImage(p,0,0,this.canvas.width,this.canvas.height),!0===c&&this.canvas_context.drawImage(r,0,0,this.canvas.width,this.canvas.height);let i=null;!0===e&&(i=await this.createCommentsImage(),await this.drawComments(i)),(["VideoOnly","Both"].includes(I.getSettingsItem("capture_caption_mode"))||!1===l)&&s.push((async()=>{const s="CompositingCaption"===I.getSettingsItem("capture_caption_mode")?n:a,i=await d(this.canvas,s,t.program_present,_,!1,e);m=!1!==i&&{blob:i,filename:s},!1!==m&&this.captured_callback(m.blob,m.filename)})()),["CompositingCaption","Both"].includes(I.getSettingsItem("capture_caption_mode"))&&!0===l&&s.push((async()=>{!0===e&&(this.canvas_context.drawImage(p,0,0,this.canvas.width,this.canvas.height),!0===c&&this.canvas_context.drawImage(r,0,0,this.canvas.width,this.canvas.height)),p.close(),this.canvas_context.drawImage(o,0,0,this.canvas.width,this.canvas.height),!0===e&&await this.drawComments(i);const s=await d(this.canvas,n,t.program_present,_,!0,e);if(u=!1!==s&&{blob:s,filename:n},!1!==u){if("Both"===I.getSettingsItem("capture_caption_mode"))while(null===m)await I.sleep(.01);this.captured_callback(u.blob,u.filename)}})()),await Promise.all(s)}else{const s="OffscreenCanvas"in window?new OffscreenCanvas(p.width,p.height):document.createElement("canvas");s.width=p.width,s.height=p.height;const i=s.getContext("bitmaprenderer",{alpha:!1});i.transferFromImageBitmap(p),p.close();const o="CompositingCaption"===I.getSettingsItem("capture_caption_mode")?n:a,r=await d(s,o,t.program_present,_,!1,e);m=!1!==r&&{blob:r,filename:o},!1!==m&&this.captured_callback(m.blob,m.filename)}console.log("[PlayerCaptureHandler] Total:",I.mathFloor(I.time()-s,3),"sec"),this.removeHighlight(e);for(const g of[m,u])if(I.getSettingsItem("capture_copy_to_clipboard")&&null!==g&&!1!==g)try{await(0,P.FH)(await(0,P.BD)(g.blob))}catch(h){this.player.notice("クリップボードへのキャプチャのコピーに失敗しました…"),console.error(h)}}addHighlight(t=!1){t?this.comment_capture_button.classList.add("dplayer-capturing"):this.capture_button.classList.add("dplayer-capturing")}removeHighlight(t=!1){t?this.comment_capture_button.classList.remove("dplayer-capturing"):this.capture_button.classList.remove("dplayer-capturing")}async commentsHTMLtoSVGImage(t,e,s){const i=`\n \n \n
\n \n ${t}\n
\n
\n
\n `.trim(),a=new Image;return a.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(i)}`,await a.decode(),a}async createCommentsImage(){let t=this.player.template.danmaku.outerHTML;for(const e of this.player_container.querySelectorAll(".dplayer-danmaku-move")){const s=e.getBoundingClientRect().left-this.player.video.getBoundingClientRect().left;t=t.replace(/transform: translateX\(.*?\);/,`left: ${s}px;`).replaceAll("border: 2px solid #E64F97;","")}return await this.commentsHTMLtoSVGImage(t,this.player.template.danmaku.offsetWidth,this.player.template.danmaku.offsetHeight)}async drawComments(t){const e=this.canvas.width/this.player.template.danmaku.offsetWidth,s=this.player.template.danmaku.offsetHeight*e;this.canvas_context.drawImage(t,0,0,this.canvas.width,s)}async exportToBlob(t){return t instanceof OffscreenCanvas?await t.convertToBlob({type:"image/jpeg",quality:.99}):new Promise(((e,s)=>{t.toBlob((t=>{null!==t?e(t):s(new Error("Failed to convert canvas to blob"))}),"image/jpeg",.99)}))}async setEXIFDataToCapture(t,e,s,i,a){const n=A()().diff(A()(e.start_time),"second",!0),o={captured_at:A()().format("YYYY-MM-DDTHH:mm:ss+09:00"),captured_playback_position:n,network_id:e.network_id,service_id:e.service_id,event_id:e.event_id,title:e.title,description:e.description,start_time:e.start_time,end_time:e.end_time,duration:e.duration,caption_text:s,is_caption_composited:i,is_comment_composited:a},r=A()().format("YYYY:MM:DD HH:mm:ss"),l={"0th":{[$.TagValues.ImageIFD.XResolution]:[72,1],[$.TagValues.ImageIFD.YResolution]:[72,1],[$.TagValues.ImageIFD.ResolutionUnit]:2,[$.TagValues.ImageIFD.YCbCrPositioning]:1,[$.TagValues.ImageIFD.DateTime]:r,[$.TagValues.ImageIFD.Software]:`KonomiTV version ${I.version}`,[$.TagValues.ImageIFD.XPComment]:[...j.lW.from(JSON.stringify(o),"ucs2")]},Exif:{[$.TagValues.ExifIFD.ExifVersion]:"0230",[$.TagValues.ExifIFD.ComponentsConfiguration]:"\0",[$.TagValues.ExifIFD.FlashpixVersion]:"0100",[$.TagValues.ExifIFD.ColorSpace]:1,[$.TagValues.ExifIFD.DateTimeOriginal]:r,[$.TagValues.ExifIFD.DateTimeDigitized]:r}},c=$.dump(l),_=await new Promise(((e,s)=>{const i=new FileReader;i.onload=()=>e(i.result),i.onerror=s,i.readAsBinaryString(t)})),d=$.insert(c,_),m=new Uint8Array(d.length);for(let u=0;u$1').replace(n,'$1')}{A().extend(E()),A().extend(V()),A().extend(K());const t=A()(),s=A()().hour(0).minute(0).second(0),i=A()().hour(6).minute(59).second(59),a=A()().hour(23).minute(0).second(0),n=A()().hour(23).minute(59).second(59);return t.isSameOrAfter(s)&&t.isSameOrBefore(i)||t.isSameOrAfter(a)&&t.isSameOrBefore(n)?"title"===e?"放送休止":"この時間は放送を休止しています。":"title"===e?"番組情報がありません":"この時間の番組情報を取得できませんでした。"}}static getAttribute(t,e,s){return null!==t&&void 0!==t[e]&&null!==t[e]?t[e]:s}static getProgramProgress(t){if(null!==t){const e=A()(A()()).diff(t.start_time,"second");return e/t.duration*100}return 0}static getProgramTime(t,e=!1){if(null!==t&&"2000-01-01T00:00:00+09:00"!==t.start_time){A().locale("ja");const s=A()(t.start_time),i=A()(t.end_time),a=t.duration/60;return!0===e?`${s.format("HH:mm")} ~ ${i.format("HH:mm")}`:`${s.format("YYYY/MM/DD (dd) HH:mm")} ~ ${i.format("HH:mm")} (${a}分)`}return!0===e?"--:-- ~ --:--":"----/--/-- (-) --:-- ~ --:-- (--分)"}}var R=I,F=s(196);const M=F.ZP.create();M.interceptors.request.use((t=>{void 0===t.baseURL&&(t.baseURL=R.api_base_url);const e=R.getAccessToken();return null!==e&&(t.headers["Authorization"]=`Bearer ${e}`),t.headers["X-KonomiTV-Version"]=R.version,t}));var U=M,G=s(1858),q=s(9258),W=s(4562),Y=s(4324);n["default"].use(G.Z),n["default"].component("v-snackbar",q.Z),n["default"].component("v-btn",W.Z),n["default"].component("v-icon",Y.Z);var X=new G.Z({theme:{dark:!0,themes:{dark:{primary:"#E64F97",secondary:"#E33157",twitter:{base:"#4F82E6",lighten1:"#799FEC",lighten2:"#41A5F1"},gray:"#66514C",black:"#110A09",background:{base:"#1E1310",lighten1:"#2F221F",lighten2:"#433532",lighten3:"#4c3c38"},text:{base:"#FFEAEA",darken1:"#D9C7C7",darken2:"#8E7F7E",darken3:"#786968"}}},options:{customProperties:!0}}}),J=s(8345),Q=s(8718),tt=s(626),et=s(7069),st=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"channels-container channels-container--home",class:{"channels-container--loading":t.is_loading}},[e("v-tabs-fix",{staticClass:"channels-tab",attrs:{centered:""},model:{value:t.tab,callback:function(e){t.tab=e},expression:"tab"}},t._l(Array.from(t.channels_list),(function([s]){return e(tt.Z,{key:s,staticClass:"channels-tab__item"},[t._v(t._s(s))])})),1),e("v-tabs-items-fix",{staticClass:"channels-list",model:{value:t.tab,callback:function(e){t.tab=e},expression:"tab"}},t._l(Array.from(t.channels_list),(function([s,i]){return e("v-tab-item-fix",{key:s,staticClass:"channels-tabitem"},[e("div",{staticClass:"channels",class:`channels--tab-${s} channels--length-${i.length}`},[t._l(i,(function(s){return e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],key:s.id,staticClass:"channel",attrs:{to:`/tv/watch/${s.channel_id}`}},[e("div",{staticClass:"channel__broadcaster"},[e("img",{staticClass:"channel__broadcaster-icon",attrs:{src:`${t.Utils.api_base_url}/channels/${s.channel_id}/logo`}}),e("div",{staticClass:"channel__broadcaster-content"},[e("span",{staticClass:"channel__broadcaster-name"},[t._v("Ch: "+t._s(s.channel_number)+" "+t._s(s.channel_name))]),e("div",{staticClass:"channel__broadcaster-status"},[e("div",{staticClass:"channel__broadcaster-status-force",class:`channel__broadcaster-status-force--${t.ChannelUtils.getChannelForceType(s.channel_force)}`},[e("Icon",{attrs:{icon:"fa-solid:fire-alt",height:"12px"}}),e("span",{staticClass:"ml-1"},[t._v("勢い:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(t.ProgramUtils.getAttribute(s,"channel_force","--")))]),e("span",{staticStyle:{"margin-left":"3px"}},[t._v(" コメ/分")])],1),e("div",{staticClass:"channel__broadcaster-status-viewers ml-4"},[e("Icon",{attrs:{icon:"fa-solid:eye",height:"14px"}}),e("span",{staticClass:"ml-1"},[t._v("視聴数:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(s.viewers))])],1)])]),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip",value:t.isPinnedChannel(s.channel_id)?"ピン留めを外す":"ピン留めする",expression:"isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'"}],staticClass:"channel__broadcaster-pin",class:{"channel__broadcaster-pin--pinned":t.isPinnedChannel(s.channel_id)},on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.isPinnedChannel(s.channel_id)?t.removePinnedChannel(s.channel_id):t.addPinnedChannel(s.channel_id)},mousedown:function(t){t.preventDefault(),t.stopPropagation()}}},[e("Icon",{attrs:{icon:"fluent:pin-20-filled",width:"24px"}})],1)]),e("div",{staticClass:"channel__program-present"},[e("div",{staticClass:"channel__program-present-title-wrapper"},[e("span",{staticClass:"channel__program-present-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_present,"title"))}}),e("span",{staticClass:"channel__program-present-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(s.program_present)))])]),e("span",{staticClass:"channel__program-present-description",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_present,"description"))}})]),e(Q.Z),e("div",{staticClass:"channel__program-following"},[e("div",{staticClass:"channel__program-following-title"},[e("span",{staticClass:"channel__program-following-title-decorate"},[t._v("NEXT")]),e("Icon",{staticClass:"channel__program-following-title-icon",attrs:{icon:"fluent:fast-forward-20-filled",width:"16px"}}),e("span",{staticClass:"channel__program-following-title-text",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_following,"title"))}})],1),e("span",{staticClass:"channel__program-following-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(s.program_following)))])]),e("div",{staticClass:"channel__progressbar"},[e("div",{staticClass:"channel__progressbar-progress",style:`width:${t.ProgramUtils.getProgramProgress(s.program_present)}%;`})])],1)})),"ピン留め"===s&&0===i.length?e("div",{staticClass:"pinned-container d-flex justify-center align-center w-100"},[e("div",{staticClass:"d-flex justify-center align-center flex-column"},[e("h2",[t._v("ピン留めされているチャンネルがありません。")]),e("div",{staticClass:"mt-4 text--text text--darken-1"},[t._v("各チャンネルの "),e("Icon",{staticStyle:{position:"relative",bottom:"-5px"},attrs:{icon:"fluent:pin-20-filled",width:"22px"}}),t._v(" アイコンから、よくみるチャンネルをこのタブにピン留めできます。")],1),e("div",{staticClass:"mt-2 text--text text--darken-1"},[t._v("チャンネルをピン留めすると、このタブが最初に表示されます。")])])]):t._e()],2)])})),1)],1)],1)],1)},it=[],at=function(){var t=this,e=t._self._c;return e("header",{staticClass:"header"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"konomitv-logo ml-3 ml-md-6",attrs:{to:"/tv/"}},[e("img",{staticClass:"konomitv-logo__image",attrs:{src:"/assets/images/logo.svg",height:"21"}})]),e(Q.Z)],1)},nt=[],ot={},rt=(0,p.Z)(ot,at,nt,!1,null,"506af489",null),lt=rt.exports,ct=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"navigation-container elevation-8"},[e("nav",{staticClass:"navigation"},[e("div",{staticClass:"navigation-scroll"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/tv/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:tv-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("テレビをみる")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/videos/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:movies-and-tv-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("ビデオをみる")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/timetable/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:calendar-ltr-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("番組表")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/captures/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:image-multiple-24-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("キャプチャ")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/watchlists/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"ic:round-playlist-play",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("ウォッチリスト")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/histories/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:history-16-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("視聴履歴")])],1),e(Q.Z),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/settings/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:settings-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("設定")])],1),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.top",value:t.is_update_available?`アップデートがあります (version ${t.latest_version})`:"",expression:"is_update_available ? `アップデートがあります (version ${latest_version})` : ''",modifiers:{top:!0}}],staticClass:"navigation__link",class:{"navigation__link--version":t.Utils.version.includes("-dev")},attrs:{"active-class":"navigation__link--active",href:"https://github.com/tsukumijima/KonomiTV"}},[e("Icon",{staticClass:"navigation__link-icon",class:{"navigation__link-icon--highlight":t.is_update_available},attrs:{icon:"fluent:info-16-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("version "+t._s(t.Utils.version))])],1)],1)])])},_t=[],dt=n["default"].extend({name:"Navigation",data(){return{Utils:R,latest_version:"",is_update_available:!1}},async created(){try{const t=(await n["default"].axios.get("/version")).data;this.latest_version=t.latest_version,(!1===t.version.includes("-dev")&&t.version!==t.latest_version||!0===t.version.includes("-dev")&&t.version.replace("-dev","")===t.latest_version)&&(this.is_update_available=!0)}catch(t){throw new Error(t)}}}),mt=dt,ut=(0,p.Z)(mt,ct,_t,!1,null,"3c027344",null),pt=ut.exports,ht=n["default"].extend({name:"TV-Home",components:{Header:lt,Navigation:pt},data(){return{Utils:R,ChannelUtils:T,ProgramUtils:L,tab:null,is_loading:!0,interval_ids:[],channels_list:new Map,pinned_channel_ids:[]}},created(){this.update();const t=60-Math.floor((new Date).getTime()/1e3)%60;this.interval_ids.push(window.setTimeout((()=>{this.update(),this.interval_ids.push(window.setInterval((()=>{this.update()}),3e4))}),1e3*t))},beforeDestroy(){for(const t of this.interval_ids)window.clearInterval(t)},methods:{async update(){let t;try{t=await n["default"].axios.get("/channels")}catch(s){return void console.error(s)}const e=t=>t.is_display;this.channels_list=new Map,t.data.GR.length>0&&this.channels_list.set("地デジ",t.data.GR.filter(e)),t.data.BS.length>0&&this.channels_list.set("BS",t.data.BS.filter(e)),t.data.CS.length>0&&this.channels_list.set("CS",t.data.CS.filter(e)),t.data.CATV.length>0&&this.channels_list.set("CATV",t.data.CATV.filter(e)),t.data.SKY.length>0&&this.channels_list.set("SKY",t.data.SKY.filter(e)),t.data.STARDIGIO.length>0&&this.channels_list.set("StarDigio",t.data.STARDIGIO.filter(e)),this.updatePinnedChannelList(!!this.is_loading),this.is_loading=!1},addPinnedChannel(t){this.pinned_channel_ids=R.getSettingsItem("pinned_channel_ids"),this.pinned_channel_ids.push(t),R.setSettingsItem("pinned_channel_ids",this.pinned_channel_ids),this.updatePinnedChannelList()},removePinnedChannel(t){this.pinned_channel_ids=R.getSettingsItem("pinned_channel_ids"),this.pinned_channel_ids.splice(this.pinned_channel_ids.indexOf(t),1),R.setSettingsItem("pinned_channel_ids",this.pinned_channel_ids),this.updatePinnedChannelList()},updatePinnedChannelList(t=!0){this.pinned_channel_ids=R.getSettingsItem("pinned_channel_ids");const e=[];for(const s of this.pinned_channel_ids){const t=T.getChannelType(s,!0),i=this.channels_list.get(t).find((t=>t.channel_id===s));void 0!==i&&e.push(i)}this.channels_list.has("ピン留め")?this.channels_list.set("ピン留め",e):this.channels_list=new Map([["ピン留め",e],...this.channels_list]),0===e.length&&!0===t&&(this.tab=1)},isPinnedChannel(t){return this.pinned_channel_ids.includes(t)}}}),gt=ht,vt=(0,p.Z)(gt,st,it,!1,null,"189c71d3",null),ft=vt.exports,wt=s(9582),yt=s(4886),bt=s(266),Ct=s(4061),kt=s(3305),xt=s(1713),Ot=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("main",{staticClass:"watch-container",class:{"watch-container--control-display":t.is_control_display,"watch-container--panel-display":t.is_panel_display,"watch-container--fullscreen":t.is_fullscreen}},[e("nav",{staticClass:"watch-navigation",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"watch-navigation__icon",attrs:{to:"/tv/"}},[e("img",{staticClass:"watch-navigation__icon-image",attrs:{src:"/assets/images/icon.svg",width:"23px"}})]),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"テレビをみる",expression:"'テレビをみる'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/tv/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:tv-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"ビデオをみる",expression:"'ビデオをみる'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/videos/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:movies-and-tv-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"番組表",expression:"'番組表'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/timetable/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:calendar-ltr-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"キャプチャ",expression:"'キャプチャ'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/captures/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:image-multiple-24-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"ウォッチリスト",expression:"'ウォッチリスト'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/watchlists/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"ic:round-playlist-play",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"視聴履歴",expression:"'視聴履歴'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/histories/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:history-16-regular",width:"26px"}})],1),e(Q.Z),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"設定",expression:"'設定'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/settings/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:settings-20-regular",width:"26px"}})],1),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:`version ${t.Utils.version}`,expression:"`version ${Utils.version}`",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",href:"https://github.com/tsukumijima/KonomiTV"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:info-16-regular",width:"26px"}})],1)],1),e("div",{staticClass:"watch-content",on:{mousemove:function(e){return t.controlDisplayTimer(e,!0)},touchmove:function(e){return t.controlDisplayTimer(e,!0)},click:function(e){return t.controlDisplayTimer(e,!0)}}},[e("header",{staticClass:"watch-header"},[e("img",{staticClass:"watch-header__broadcaster",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("span",{staticClass:"watch-header__program-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"title"))}}),e("span",{staticClass:"watch-header__program-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_present,!0)))]),e(Q.Z),e("span",{staticClass:"watch-header__now"},[t._v(t._s(t.time))])],1),e("div",{staticClass:"watch-player",class:{"watch-player--loading":t.is_loading,"watch-player--virtual-keyboard-display":t.is_virtual_keyboard_display&&t.Utils.hasActiveElementClass("dplayer-comment-input")}},[e("div",{staticClass:"watch-player__background",class:{"watch-player__background--display":t.is_background_display},style:{backgroundImage:`url(${t.background_url})`}},[e("img",{staticClass:"watch-player__background-logo",attrs:{src:"/assets/images/logo.svg"}})]),e(kt.Z,{staticClass:"watch-player__buffering",class:{"watch-player__buffering--display":t.is_video_buffering&&(t.is_loading||null!==t.player&&!t.player.video.paused)},attrs:{indeterminate:"",size:"60",width:"6"}}),e("div",{staticClass:"watch-player__dplayer"}),e("div",{staticClass:"watch-player__button",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.top",value:"前のチャンネル",expression:"'前のチャンネル'",modifiers:{top:!0}}],staticClass:"switch-button switch-button-up",attrs:{to:`/tv/watch/${t.channel_previous.channel_id}`}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:ios-arrow-left-24-filled",width:"32px",rotate:"1"}})],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"switch-button switch-button-panel switch-button-panel--open",on:{click:function(e){t.is_panel_display=!t.is_panel_display}}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:navigation-16-filled",width:"32px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.bottom",value:"次のチャンネル",expression:"'次のチャンネル'",modifiers:{bottom:!0}}],staticClass:"switch-button switch-button-down",attrs:{to:`/tv/watch/${t.channel_next.channel_id}`}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:ios-arrow-right-24-filled",width:"33px",rotate:"1"}})],1)],1)],1)]),e("div",{staticClass:"watch-panel",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("div",{staticClass:"watch-panel__header"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-close-button",on:{click:function(e){t.is_panel_display=!1}}},[e("Icon",{staticClass:"panel-close-button__icon",attrs:{icon:"akar-icons:chevron-right",width:"25px"}}),e("span",{staticClass:"panel-close-button__text"},[t._v("閉じる")])],1),e(Q.Z),e("div",{staticClass:"panel-broadcaster"},[e("img",{staticClass:"panel-broadcaster__icon",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("div",{staticClass:"panel-broadcaster__number"},[t._v(t._s(t.channel.channel_number))]),e("div",{staticClass:"panel-broadcaster__name"},[t._v(t._s(t.channel.channel_name))])])],1),e("div",{staticClass:"watch-panel__content-container"},[e("Program",{staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Program"===t.tv_panel_active_tab},attrs:{channel:t.channel}}),e("Channel",{staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Channel"===t.tv_panel_active_tab},attrs:{channels_list:t.channels_list}}),e("Comment",{ref:"Comment",staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Comment"===t.tv_panel_active_tab},attrs:{channel:t.channel,player:t.player}}),e("Twitter",{ref:"Twitter",staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Twitter"===t.tv_panel_active_tab},attrs:{channel:t.channel,player:t.player,is_virtual_keyboard_display:t.is_virtual_keyboard_display},on:{panel_folding_requested:function(e){t.is_panel_display=!1}}})],1),e("div",{staticClass:"watch-panel__navigation"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Program"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Program"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-solid:info-circle",width:"33px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("番組情報")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Channel"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Channel"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-solid:broadcast-tower",width:"34px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("チャンネル")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Comment"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Comment"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"bi:chat-left-text-fill",width:"29px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("コメント")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Twitter"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Twitter"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-brands:twitter",width:"34px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("Twitter")])],1)])])]),e(Ct.Z,{attrs:{"max-width":"1000",transition:"slide-y-transition"},model:{value:t.shortcut_key_modal,callback:function(e){t.shortcut_key_modal=e},expression:"shortcut_key_modal"}},[e(wt.Z,[e(yt.EB,{staticClass:"px-5 pt-4 pb-3 d-flex align-center font-weight-bold"},[e("Icon",{attrs:{icon:"fluent:keyboard-20-filled",height:"28px"}}),e("span",{staticClass:"ml-3"},[t._v("キーボードショートカット")]),e(Q.Z),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"d-flex align-center rounded-circle cursor-pointer px-2 py-2",on:{click:function(e){t.shortcut_key_modal=!1}}},[e("Icon",{attrs:{icon:"fluent:dismiss-12-filled",width:"23px",height:"23px"}})],1)],1),e("div",{staticClass:"px-5 pb-4"},[e(xt.Z,t._l(t.shortcut_key_list,(function(s,i){return e(bt.Z,{key:i,attrs:{cols:"6"}},t._l(s,(function(s){return e("div",{key:s.name,staticClass:"mt-3"},[e("div",{staticClass:"text-subtitle-1 d-flex align-center font-weight-bold"},[e("Icon",{attrs:{icon:s.icon,height:s.icon_height}}),e("span",{staticClass:"ml-2"},[t._v(t._s(s.name))])],1),t._l(s.shortcuts,(function(s){return e("div",{key:s.name,staticClass:"mt-3"},[e("div",{staticClass:"text-subtitle-2 mt-2 d-flex align-center font-weight-medium"},[e("span",{staticClass:"mr-2",domProps:{innerHTML:t._s(s.name)}}),e("div",{staticClass:"ml-auto d-flex align-center flex-shrink-0"},t._l(s.keys,(function(i,a){return e("div",{key:i.name,staticClass:"ml-auto d-flex align-center"},[e("span",{staticClass:"shortcut-key"},[t._l(i.name.split(";"),(function(t){return e("Icon",{directives:[{name:"show",rawName:"v-show",value:!0===i.icon,expression:"key.icon === true"}],key:t,attrs:{icon:t,height:"18px"}})})),!1===i.icon?e("span",{domProps:{innerHTML:t._s(i.name)}}):t._e()],2),a({id:Date.now()+e,match:t.match,pattern:t.pattern}))),muted_comment_keyword_match_type:[{text:"部分一致",value:"partial"},{text:"前方一致",value:"forward"},{text:"後方一致",value:"backward"},{text:"完全一致",value:"exact"},{text:"正規表現",value:"regex"}],muted_niconico_user_ids:R.getSettingsItem("muted_niconico_user_ids").map(((t,e)=>({id:Date.now()+e,user_id:t}))),settings:(()=>{const t={},e=["mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments"];for(const s of e)t[s]=R.getSettingsItem(s);return t})()}},created(){this.interval_timer_id=window.setInterval((()=>{const t=R.getSettingsItem("muted_comment_keywords");JSON.stringify(this.muted_comment_keywords)!==JSON.stringify(t)&&(this.muted_comment_keywords=t.map(((t,e)=>({id:Date.now()+e,match:t.match,pattern:t.pattern}))));const e=R.getSettingsItem("muted_niconico_user_ids");JSON.stringify(this.muted_niconico_user_ids)!==JSON.stringify(e)&&(this.muted_niconico_user_ids=e.map(((t,e)=>({id:Date.now()+e,user_id:t}))))}),1e3)},beforeDestroy(){window.clearInterval(this.interval_timer_id)},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))R.setSettingsItem(t,e)}},muted_comment_keywords:{deep:!0,handler(){R.setSettingsItem("muted_comment_keywords",this.muted_comment_keywords.map((t=>(delete t.id,t))))}},muted_niconico_user_ids:{deep:!0,handler(){R.setSettingsItem("muted_niconico_user_ids",this.muted_niconico_user_ids.map((t=>t.user_id)))}},showing(){this.comment_mute_settings_modal=this.showing},comment_mute_settings_modal(){this.$emit("change",this.comment_mute_settings_modal)}}}),Yt=Wt,Xt=(0,p.Z)(Yt,Gt,qt,!1,null,"5d831536",null),Jt=Xt.exports;const Qt=new RegExp(j.lW.from("cHJwcnzvvZDvvZLvvZDvvZJ8U0VYfFPjgIdYfFPil69YfFPil4tYfFPil49YfO+8s++8pe+8uHzvvLPjgIfvvLh877yz4pev77y4fO+8s+KXi++8uHzvvLPil4/vvLh844Ki44OA44Or44OIfOOCouODiuOCpXzjgqLjg4rjg6t844Kk44Kr6IetfOOCpOOBj3zjgYbjgpPjgZN844Km44Oz44KzfOOBhuOCk+OBoXzjgqbjg7Pjg4F844Ko44Kt44ObfOOBiOOBo+OBoXzjgqjjg4Pjg4F844GI44Gj44KNfOOCqOODg+ODrXzjgYjjgo1844Ko44OtfOW3peWPo3zjgYrjgZXjgo/jgorjgb7jgpN844GK44GX44Gj44GTfOOCquOCt+ODg+OCs3zjgqrjg4PjgrXjg7N844GK44Gj44Gx44GEfOOCquODg+ODkeOCpHzjgqrjg4rjg4vjg7x844Kq44OK44ObfOOBiuOBseOBhHzjgqrjg5HjgqR844GKcHzjgYrvvZB844Kq44OV44OR44KzfOOCrOOCpOOCuOODs3zjgq3jg7Pjgr/jg55844GP44Gx44GCfOOBj+OBseOBgXzjgq/jg6p844Kv44Oz44OLfOOBkeOBpHzjgrHjg4R844GU44GP44GU44GP44GU44GP44GU44GPfOOCs+ODs+ODieODvOODoHzjgrbjg7zjg6Hjg7N844K344KzfOOBl+OBk+OBl+OBk3zjgrfjgrPjgrfjgrN844GZ44GR44GZ44GRfOOBm+OBhOOBiOOBjXzjgZnjgYXjgYXjgYXjgYXjgYV844GZ44GG44GG44GG44GG44GGfOOCu+OCr+ODreOCuXzjgrvjg4Pjgq/jgrl844K744OV44OsfOOBoeOBo+OBseOBhHzjgaHjgaPjg5HjgqR844OB44OD44OR44KkfOOBoeOCk+OBk3zjgaHjgIfjgZN844Gh4pev44GTfOOBoeKXi+OBk3zjgaHil4/jgZN844OB44Oz44KzfOODgeOAh+OCs3zjg4Hil6/jgrN844OB4peL44KzfOODgeKXj+OCs3zjgaHjgpPjgb1844Gh44CH44G9fOOBoeKXr+OBvXzjgaHil4vjgb1844Gh4peP44G9fOODgeODs+ODnXzjg4HjgIfjg51844OB4pev44OdfOODgeKXi+ODnXzjg4Hil4/jg51844Gh44KT44Gh44KTfOODgeODs+ODgeODs3zjgabjgYPjgpPjgabjgYPjgpN844OG44Kj44Oz44OG44Kj44OzfOODhuOCo+ODs+ODnXzjg4fjgqvjgYR844OH44Oq44OY44OrfOiEseOBknzjgbHjgYTjgoLjgb9844OR44OR5rS7fOOBteOBhuODu3zjgbXjgYbigKZ844G144GFfO++jO+9qXzjgbXjgY/jgonjgb/jgYvjgZF844G144GP44KJ44KT44GnfOOBuuOBo+OBn3zjgbrjgo3jgbrjgo1844Oa44Ot44Oa44OtfO++je++n+++m+++je++n+++m3zjg5Xjgqfjg6l844G844Gj44GNfOODneODq+ODjnzjgbzjgo3jgpN844Oc44Ot44OzfO++ju++nu++m+++nXzjgb3jgo3jgop844Od44Ot44OqfO++ju++n+++m+++mHzjg57jg7PjgY3jgaR844Oe44Oz44Kt44OEfOOBvuOCk+OBk3zjgb7jgIfjgZN844G+4pev44GTfOOBvuKXi+OBk3zjgb7il4/jgZN844Oe44Oz44KzfOODnuOAh+OCs3zjg57il6/jgrN844Oe4peL44KzfOODnuKXj+OCs3zjgb7jgpPjgZXjgpN844KC44Gj44GT44KKfOODouODg+OCs+ODqnzjgoLjgb/jgoLjgb9844Oi44Of44Oi44OffOODpOOBo+OBpnzjg6Tjgol844KE44KJ44Gb44KNfOODpOOCinzjg6Tjgot844Ok44KMfOODpOOCjXzjg6njg5bjg5t844Ov44Os44OhfOWWmHzpmbDmoLh86Zmw6IyOfOmZsOWUh3zmt6vlpKJ86Zmw5q+bfOeUo+OCgeOCi3zlpbPjga7lrZDjga7ml6V85rGa44Gj44GV44KTfOWkluS6unzlp6Z86aiO5LmX5L2NfOmHkeeOiXzmnIjntYx85b6M6IOM5L2NfOWtkOS9nOOCinzlsITnsr585L+h6ICFfOeyvua2snzpgI/jgZF85oCn5LqkfOeyvuWtkHzmraPluLjkvY185oCn5b60fOaAp+eahHznlJ/nkIZ85a+45q2i44KBfOe0oOadkHzmirHjgYR85oqx44GLfOaKseOBjXzmirHjgY985oqx44GRfOaKseOBk3zkubPpppZ85oGl5Z6ifOS4reOBoOOBl3zkuK3lh7rjgZd85bC/fOaKnOOBhHzmipzjgZHjgarjgYR85oqc44GR44KLfOaKnOOBkeOCjHzohqjjgol85YuD6LW3fOaPieOBvnzmj4njgb985o+J44KAfOaPieOCgXzmvKvmuZZ844CH772efOKXr++9nnzil4vvvZ584peP772efOOAh+ODg+OCr+OCuXzil6/jg4Pjgq/jgrl84peL44OD44Kv44K5fOKXj+ODg+OCr+OCuQ==","base64").toString()),te=new RegExp(j.lW.from("44CCfOOCouOCueODmnzjgqLjg7Pjg4F844Kk44Kr44KMfOOCpOODqeOBpOOBj3zjgqbjgrh844Km44O844OofOOCpuODqHzjgqbjg6jjgq9844GN44KC44GEfOOCreODouOCpHzjgq3jg6LjgYR844KtL+ODoC/jg4F844Ks44Kk44K4fO+9tu++nu+9su+9vO++nnzjgqzjgq1844Kr44K5fOOCreODg+OCunzjgY3jgaHjgYzjgYR844Kt44OB44Ks44KkfOOCreODoOODgXzjgZTjgb9844K044OffOODgeODp+ODs3zljYPjg6fjg7N844Gk44KT44G8fOODhOODs+ODnHzjg4vjgqt844ON44OI44Km44OofOODi+ODgHzvvobvvoDvvp5844OR44O844OofOODkeODqHzjg5Hjg6jjgq9844G244Gj44GVfOODluODg+OCtXzjgbbjgZXjgYR844OW44K144KkfOOBvuOBrOOBkXzjg6Hjgq/jg6l844OQ44KrfOODoOOCq+OBpOOBj3zmhbDlronlqaZ85a6z5YWQfOWkluWtl3zlp6blm7186Z+T5Zu9fOWfuuWcsOWklnzmsJfmjIHjgaHmgqp85q66fOmgg+OBmXzlnKjml6V85q2744GtfOawj+OBrXzvvoDvvot85q255YyVfOatueODknzpmpzlrrN85pyd6a6ufOeymOedgHzlj43ml6V86aas6bm/fOeZuumBlHzmnLR85LiN5b+rfOmWk+aKnOOBkXzpnZblm70=","base64").toString());var ee=n["default"].extend({name:"Panel-CommentTab",components:{CommentMuteSettings:Jt},props:{channel:{type:Object,required:!0},player:{type:null,required:!0}},data(){return{is_manual_scroll:!1,is_auto_scrolling:!1,user:null,comment_list:[],comment_list_element:null,is_comment_list_dropdown_display:!1,comment_list_dropdown_top:0,comment_list_dropdown_comment:null,watch_session:null,comment_session:null,initialize_failed_message:null,vpos_base_timestamp:0,keep_seat_interval_id:0,resize_observer:null,resize_observer_element:null,comment_mute_settings_modal:!1}},beforeDestroy(){this.destroy(),null!==this.resize_observer&&this.resize_observer.unobserve(this.resize_observer_element)},watch:{async channel(t,e){if(t.channel_id!==e.channel_id){if("gr000"!==e.channel_id&&(await R.sleep(.5),this.channel.channel_id!==t.channel_id))return;this.destroy(),null===this.comment_list_element&&(this.comment_list_element=this.$el.querySelector(".comment-list"));let i=!1;this.comment_list_element.onmousedown=t=>{const e=t.clientX-this.comment_list_element.getBoundingClientRect().left;e>this.comment_list_element.clientWidth&&(i=!0)},this.comment_list_element.onmouseup=t=>{const e=t.clientX-this.comment_list_element.getBoundingClientRect().left;e>this.comment_list_element.clientWidth&&(i=!1)};const a=()=>{i=!0,window.setTimeout((()=>i=!1),100)};let o=!1;this.comment_list_element.ontouchstart=()=>o=!0,this.comment_list_element.ontouchend=()=>o=!1,this.comment_list_element.ontouchmove=()=>!0===o?a():"",this.comment_list_element.onwheel=a,this.comment_list_element.onscroll=async()=>{!1===this.is_auto_scrolling&&!0===i&&(this.is_manual_scroll=!0,await R.sleep(.1),this.comment_list_element.scrollTop+this.comment_list_element.offsetHeight>this.comment_list_element.scrollHeight-10&&(this.is_manual_scroll=!1))},await this.initReserveObserver();try{this.user=(await n["default"].axios.get("/users/me")).data}catch(s){this.user=null}try{const t=await this.initWatchSession();this.vpos_base_timestamp=100*A()(t["vpos_base_time"]).unix(),await this.initCommentSession(t)}catch(s){this.initialize_failed_message=s.message,console.error(s.toString())}}}},methods:{async initWatchSession(){let t;try{t=await n["default"].axios.get(`/channels/${this.channel.channel_id}/jikkyo`)}catch(e){throw new Error(e)}if(!1===t.data.is_success)throw"このチャンネルはニコニコ実況に対応していません。"!==t.data.detail&&"現在放送中のニコニコ実況がありません。"!==t.data.detail&&this.player.notice(t.data.detail),new Error(t.data.detail);return new Promise((e=>{this.watch_session=new WebSocket(t.data.audience_token),this.watch_session.addEventListener("open",(()=>{this.watch_session.send(JSON.stringify({type:"startWatching",data:{reconnect:!1}}))})),this.watch_session.addEventListener("message",(async t=>{const s=JSON.parse(t.data);switch(s.type){case"room":return e({message_server:s.data.messageServer.uri,thread_id:s.data.threadId,vpos_base_time:s.data.vposBaseTime,your_post_key:s.data.yourPostKey?s.data.yourPostKey:null});case"seat":this.keep_seat_interval_id=window.setInterval((()=>{1===this.watch_session.readyState?this.watch_session.send(JSON.stringify({type:"keepSeat"})):window.clearInterval(this.keep_seat_interval_id)}),1e3*s.data.keepIntervalSec);break;case"ping":this.watch_session.send(JSON.stringify({type:"pong"}));break;case"error":{let t;switch(s.data.code){case"CONNECT_ERROR":t="コメントサーバーに接続できません。";break;case"CONTENT_NOT_READY":t="ニコニコ実況が配信できない状態です。";break;case"NO_THREAD_AVAILABLE":t="コメントスレッドを取得できません。";break;case"NO_ROOM_AVAILABLE":t="コメント部屋を取得できません。";break;case"NO_PERMISSION":t="API にアクセスする権限がありません。";break;case"NOT_ON_AIR":t="ニコニコ実況が放送中ではありません。";break;case"BROADCAST_NOT_FOUND":t="ニコニコ実況の配信情報を取得できません。";break;case"INTERNAL_SERVERERROR":t="ニコニコ実況でサーバーエラーが発生しています。";break;default:t=`ニコニコ実況でエラーが発生しています。(${s.data.code})`;break}console.log(`error occurred. code: ${s.data.code}`),this.player.danmaku.showing&&this.player.notice(t);break}case"reconnect":{await R.sleep(s.data.waitTimeSec),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const t=await this.initWatchSession();await this.initCommentSession(t);break}case"disconnect":{let t;switch(this.watch_session&&(this.watch_session.onclose=null),s.data.reason){case"TAKEOVER":t="ニコニコ実況の番組から追い出されました。";break;case"NO_PERMISSION":t="ニコニコ実況の番組の座席を取得できませんでした。";break;case"END_PROGRAM":t="ニコニコ実況がリセットされたか、コミュニティの番組が終了しました。";break;case"PING_TIMEOUT":t="コメントサーバーとの接続生存確認に失敗しました。";break;case"TOO_MANY_CONNECTIONS":t="ニコニコ実況の同一ユーザからの接続数上限を越えています。";break;case"TOO_MANY_WATCHINGS":t="ニコニコ実況の同一ユーザからの視聴番組数上限を越えています。";break;case"CROWDED":t="ニコニコ実況の番組が満席です。";break;case"MAINTENANCE_IN":t="ニコニコ実況はメンテナンス中です。";break;case"SERVICE_TEMPORARILY_UNAVAILABLE":t="ニコニコ実況で一時的にサーバーエラーが発生しています。";break;default:t=`ニコニコ実況との接続が切断されました。(${s.data.reason})`;break}console.log(`disconnected. reason: ${s.data.reason}`),this.player.danmaku.showing&&this.player.notice(t),await R.sleep(5),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const e=await this.initWatchSession();await this.initCommentSession(e);break}}})),this.watch_session.onclose=async t=>{console.log(`disconnected. code: ${t.code}`),this.player.danmaku.showing&&this.player.notice(`ニコニコ実況との接続が切断されました。(code: ${t.code})`),await R.sleep(10),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const e=await this.initWatchSession();await this.initCommentSession(e)}}))},async initCommentSession(t){let e=[],s=!1;this.comment_session=new WebSocket(t.message_server),this.comment_session.addEventListener("open",(()=>{this.comment_session.send(JSON.stringify([{ping:{content:"rs:0"}},{ping:{content:"ps:0"}},{thread:{version:"20061206",thread:t.thread_id,threadkey:t.your_post_key,user_id:"",res_from:-50}},{ping:{content:"pf:0"}},{ping:{content:"rf:0"}}]))})),this.comment_session.addEventListener("message",(async t=>{const i=JSON.parse(t.data);if(void 0!==i.thread)if(0===i.thread.resultcode);else{const t="コメントサーバーに接続できませんでした。";console.error("Error: "+t)}void 0!==i.ping&&"rf:0"===i.ping.content&&(s=!0,this.scrollCommentList());const a=i.chat;if(void 0===a||void 0===a.content||a.content.match(/\/[a-z]+ /))return;if(a.yourpost&&1===a.yourpost)return;if(this.isMutedComment(a.content,a.user_id))return void console.log("Muted comment: "+a.content);let n="#FFEAEA",o="right",r="medium";if(void 0!==a.mail&&null!==a.mail){const t=a.mail.replace("184","").split(" ");for(const e of t)null!==this.getCommentColor(e)&&(n=this.getCommentColor(e)),null!==this.getCommentPosition(e)&&(o=this.getCommentPosition(e)),"big"!==e&&"medium"!==e&&"small"!==e||(r=e)}if(!0===R.getSettingsItem("mute_fixed_comments")&&("top"===o||"bottom"===o))return void console.log("Muted comment (Fixed): "+a.content);if(!0===R.getSettingsItem("mute_colored_comments")&&"#FFEAEA"!==n)return void console.log("Muted comment (Colored): "+a.content);if(!0===R.getSettingsItem("mute_big_size_comments")&&"big"===r)return void console.log("Muted comment (Big): "+a.content);if(s){const t=R.getSettingsItem("comment_delay_time");await R.sleep(t)}if(this.comment_list.length>=500&&!1===this.is_manual_scroll)while(this.comment_list.length>=500)this.comment_list.shift();const l={id:a.no,text:a.content,time:A()(1e3*a.date).format("HH:mm:ss"),user_id:a.user_id,my_post:!1};"hidden"!==document.visibilityState?(this.comment_list.push(l),s&&this.scrollCommentList(),s&&(this.player.video.paused||this.player.danmaku.draw({text:a.content,color:n,type:o,size:r}))):e.push(l)})),document.onvisibilitychange=()=>{"visible"===document.visibilityState&&(this.comment_list.push(...e),e=[],this.scrollCommentList())}},async sendComment(t){if(null!==this.initialize_failed_message)return void t.error(this.initialize_failed_message);if(null===this.user)return void t.error("コメントするには、KonomiTV アカウントにログインしてください。");if(null===this.user.niconico_user_id)return void t.error("コメントするには、ニコニコアカウントと連携してください。");if(!1===this.user.niconico_user_premium&&("top"===t.data.type||"bottom"===t.data.type))return void t.error("コメントを上下に固定するには、ニコニコアカウントのプレミアム会員登録が必要です。");if(!1===this.user.niconico_user_premium&&"big"===t.data.size)return void t.error("コメントサイズを大きめに設定するには、ニコニコアカウントのプレミアム会員登録が必要です。");const e={"#FFEAEA":"white","#F02840":"red","#FD7E80":"pink","#FDA708":"orange","#FFE133":"yellow","#64DD17":"green","#00D4F5":"cyan","#4763FF":"blue"},s={top:"ue",right:"naka",bottom:"shita"},i=Math.floor((new Date).getTime()/10)-this.vpos_base_timestamp;this.watch_session.send(JSON.stringify({type:"postComment",data:{text:t.data.text,color:e[t.data.color.toUpperCase()],position:s[t.data.type],size:t.data.size,vpos:i,isAnonymous:!0}})),this.comment_list.push({id:(new Date).getTime(),text:t.data.text,time:A()().format("HH:mm:ss"),user_id:`${this.user.niconico_user_id}`,my_post:!0}),this.watch_session.onmessage=e=>{const s=JSON.parse(e.data);switch(s.type){case"postCommentResult":t.success(),this.watch_session.onmessage=null;break;case"error":{let e=`コメントの送信に失敗しました。(${s.data.code})`;switch(s.data.code){case"COMMENT_POST_NOT_ALLOWED":e="コメントが許可されていません。";break;case"INVALID_MESSAGE":e="コメント内容が無効です。";break}t.error(e),this.watch_session.onmessage=null;break}}}},async initReserveObserver(){this.resize_observer_element=document.querySelector(".watch-player");let t=null;const e=()=>{const e=document.querySelector(".dplayer-video-wrap-aspect"),s=document.querySelector(".dplayer-danmaku");if(null===this.resize_observer_element||null===this.resize_observer_element.clientHeight)return;if(null===e||null===e.clientHeight)return;const i=(this.resize_observer_element.clientHeight-e.clientHeight)/2,a=window.matchMedia("(max-height: 450px)").matches?50:66;if(i0===e?t:l(e,t%e),c=l(o,r),_=`${o/c} / ${r/c}`;s.style.transition="none",s.style.setProperty("--comment-area-aspect-ratio",_),s.style.setProperty("--comment-area-vertical-margin",`${n}px`),window.clearTimeout(t),window.setTimeout((()=>{s.style.transition=""}),200)}else s.style.removeProperty("--comment-area-aspect-ratio"),s.style.removeProperty("--comment-area-vertical-margin")};this.resize_observer=new ResizeObserver(e),this.resize_observer.observe(this.resize_observer_element),window.setTimeout(e,600)},async scrollCommentList(t=!1){if(!0!==this.is_manual_scroll){this.is_auto_scrolling=!0;for(let e=0;e<3;e++)await R.sleep(.01),!0===t?this.comment_list_element.scrollTo({top:this.comment_list_element.scrollHeight,left:0,behavior:"smooth"}):this.comment_list_element.scrollTo(0,this.comment_list_element.scrollHeight);await R.sleep(.1),this.is_auto_scrolling=!1}},getCommentColor(t){const e={white:"#FFEAEA",red:"#F02840",pink:"#FD7E80",orange:"#FDA708",yellow:"#FFE133",green:"#64DD17",cyan:"#00D4F5",blue:"#4763FF",purple:"#D500F9",black:"#1E1310",white2:"#CCCC99",niconicowhite:"#CCCC99",red2:"#CC0033",truered:"#CC0033",pink2:"#FF33CC",orange2:"#FF6600",passionorange:"#FF6600",yellow2:"#999900",madyellow:"#999900",green2:"#00CC66",elementalgreen:"#00CC66",cyan2:"#00CCCC",blue2:"#3399FF",marineblue:"#3399FF",purple2:"#6633CC",nobleviolet:"#6633CC",black2:"#666666"};return void 0!==e[t]?e[t]:null},getCommentPosition(t){switch(t){case"ue":return"top";case"naka":return"right";case"shita":return"bottom";default:return null}},isMutedComment(t,e){const s=R.getSettingsItem("muted_comment_keywords");for(const a of s)if(""!==a.pattern)switch(a.match){case"partial":if(t.includes(a.pattern))return!0;break;case"forward":if(t.startsWith(a.pattern))return!0;break;case"backward":if(t.endsWith(a.pattern))return!0;break;case"exact":if(t===a.pattern)return!0;break;case"regex":if(new RegExp(a.pattern).test(t))return!0;break}if(!0===R.getSettingsItem("mute_vulgar_comments")&&Qt.test(t))return!0;if(!0===R.getSettingsItem("mute_abusive_discriminatory_prejudiced_comments")&&te.test(t))return!0;if(!0===R.getSettingsItem("mute_consecutive_same_characters_comments")&&/(.)\1{7,}/.test(t))return!0;if(/最高\d+米\/|計\d+ID|総\d+米/.test(t))return!0;const i=R.getSettingsItem("muted_niconico_user_ids");for(const a of i)if(e===a)return!0;return!1},addMutedKeywords(t){const e=R.getSettingsItem("muted_comment_keywords");e.push({match:"exact",pattern:t}),R.setSettingsItem("muted_comment_keywords",e)},addMutedNiconicoUserIDs(t){const e=R.getSettingsItem("muted_niconico_user_ids");e.push(t),R.setSettingsItem("muted_niconico_user_ids",e)},displayCommentListDropdown(t,e){this.is_comment_list_dropdown_display=!0,this.comment_list_dropdown_top=t.currentTarget.getBoundingClientRect().top-this.$refs.comment_list_wrapper.getBoundingClientRect().top,this.comment_list_dropdown_comment=e},destroy(){this.initialize_failed_message=null,this.comment_list=[],document.onvisibilitychange=null,null!==this.watch_session&&(this.watch_session.onclose=null,this.watch_session.close(),this.watch_session=null),null!==this.comment_session&&(this.comment_session.onclose=null,this.comment_session.close(),this.comment_session=null),window.clearInterval(this.keep_seat_interval_id)}}}),se=ee,ie=(0,p.Z)(se,Lt,Rt,!1,null,"3eadf094",null),ae=ie.exports,ne=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"program-container"},[e("section",{staticClass:"program-broadcaster"},[e("img",{staticClass:"program-broadcaster__icon",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("div",{staticClass:"program-broadcaster__number"},[t._v("Ch: "+t._s(t.channel.channel_number))]),e("div",{staticClass:"program-broadcaster__name"},[t._v(t._s(t.channel.channel_name))])]),e("section",{staticClass:"program-info"},[e("h1",{staticClass:"program-info__title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"title"))}}),e("div",{staticClass:"program-info__time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_present)))]),e("div",{staticClass:"program-info__description",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"description"))}}),e("div",{staticClass:"program-info__genre-container"},t._l(t.ProgramUtils.getAttribute(t.channel.program_present,"genre",[]),(function(s,i){return e("div",{key:i,staticClass:"program-info__genre"},[t._v(" "+t._s(s.major)+" / "+t._s(s.middle)+" ")])})),0),e("div",{staticClass:"program-info__next"},[e("span",{staticClass:"program-info__next-decorate"},[t._v("NEXT")]),e("Icon",{staticClass:"program-info__next-icon",attrs:{icon:"fluent:fast-forward-20-filled",width:"16px"}})],1),e("span",{staticClass:"program-info__next-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_following,"title"))}}),e("div",{staticClass:"program-info__next-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_following)))]),e("div",{staticClass:"program-info__status"},[e("div",{staticClass:"program-info__status-force",class:`program-info__status-force--${t.ChannelUtils.getChannelForceType(t.channel.channel_force)}`},[e("Icon",{attrs:{icon:"fa-solid:fire-alt",height:"14px"}}),e("span",{staticClass:"ml-2"},[t._v("勢い:")]),e("span",{staticClass:"ml-2"},[t._v(t._s(t.ProgramUtils.getAttribute(t.channel,"channel_force","--"))+" コメ/分")])],1),e("div",{staticClass:"program-info__status-viewers ml-5"},[e("Icon",{attrs:{icon:"fa-solid:eye",height:"14px"}}),e("span",{staticClass:"ml-2"},[t._v("視聴数:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(t.channel.viewers))])],1)])]),e("section",{staticClass:"program-detail-container"},t._l(t.ProgramUtils.getAttribute(t.channel.program_present,"detail",{}),(function(s,i){return e("div",{key:i,staticClass:"program-detail"},[e("h2",{staticClass:"program-detail__heading"},[t._v(t._s(i))]),e("div",{staticClass:"program-detail__text",domProps:{innerHTML:t._s(t.Utils.URLtoLink(s))}})])})),0)])},oe=[],re=n["default"].extend({name:"Panel-ProgramTab",props:{channel:{type:Object,required:!0}},data(){return{Utils:R,ChannelUtils:T,ProgramUtils:L}}}),le=re,ce=(0,p.Z)(le,ne,oe,!1,null,"3c7f1e0c",null),_e=ce.exports,de=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"twitter-container"},[e(Ct.Z,{attrs:{"content-class":"zoom-capture-modal-container","max-width":"980",transition:"slide-y-transition"},model:{value:t.zoom_capture_modal,callback:function(e){t.zoom_capture_modal=e},expression:"zoom_capture_modal"}},[e("div",{staticClass:"zoom-capture-modal"},[e("img",{staticClass:"zoom-capture-modal__image",attrs:{src:t.zoom_capture?t.zoom_capture.image_url:""}}),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"zoom-capture-modal__download",attrs:{href:t.zoom_capture?t.zoom_capture.image_url:"",download:t.zoom_capture?t.zoom_capture.filename:""}},[e("Icon",{attrs:{icon:"fa6-solid:download",width:"45px"}})],1)])]),e("div",{staticClass:"tab-container"},[e("div",{staticClass:"tab-content",class:{"tab-content--active":"Capture"===t.twitter_active_tab}},[e("div",{staticClass:"captures"},t._l(t.captures,(function(s){return e("div",{key:s.image_url,staticClass:"capture",class:{"capture--selected":s.selected,"capture--focused":s.focused,"capture--disabled":!s.selected&&t.tweet_captures.length>=4},on:{click:function(e){return t.clickCapture(s)}}},[e("img",{staticClass:"capture__image",attrs:{src:s.image_url}}),e("div",{staticClass:"capture__disabled-cover"}),e("div",{staticClass:"capture__selected-number"},[t._v(t._s(t.tweet_captures.findIndex((t=>t===s.blob))+1))]),e("Icon",{staticClass:"capture__selected-checkmark",attrs:{icon:"fluent:checkmark-circle-16-filled"}}),e("div",{staticClass:"capture__selected-border"}),e("div",{staticClass:"capture__focused-border"}),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"capture__zoom",on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.zoom_capture_modal=!0,t.zoom_capture=s},mousedown:function(t){t.preventDefault(),t.stopPropagation()}}},[e("Icon",{attrs:{icon:"fluent:zoom-in-16-regular",width:"32px"}})],1)],1)})),0),e("div",{directives:[{name:"show",rawName:"v-show",value:0===t.captures.length,expression:"captures.length === 0"}],staticClass:"capture-announce"},[e("div",{staticClass:"capture-announce__heading"},[t._v("まだキャプチャがありません。")]),t._m(0)])])]),e("div",{staticClass:"tab-button-container"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Search"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Search"}}},[e("Icon",{attrs:{icon:"fluent:search-16-filled",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("ツイート検索")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Timeline"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Timeline"}}},[e("Icon",{attrs:{icon:"fluent:home-16-regular",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("タイムライン")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Capture"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Capture"}}},[e("Icon",{attrs:{icon:"fluent:image-copy-20-regular",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("キャプチャ")])],1)]),e("div",{staticClass:"tweet-form",class:{"tweet-form--focused":t.is_tweet_hashtag_form_focused||t.is_tweet_text_form_focused,"tweet-form--virtual-keyboard-display":t.is_virtual_keyboard_display&&(t.Utils.hasActiveElementClass("tweet-form__hashtag-form")||t.Utils.hasActiveElementClass("tweet-form__textarea"))&&(()=>(t.is_hashtag_list_display=!1,!0))()}},[e("div",{staticClass:"tweet-form__hashtag"},[e("input",{directives:[{name:"model",rawName:"v-model",value:t.tweet_hashtag,expression:"tweet_hashtag"}],staticClass:"tweet-form__hashtag-form",attrs:{type:"search",placeholder:"#ハッシュタグ"},domProps:{value:t.tweet_hashtag},on:{input:[function(e){e.target.composing||(t.tweet_hashtag=e.target.value)},function(e){return t.updateTweetLetterCount()}],focus:function(e){t.is_tweet_hashtag_form_focused=!0},blur:function(e){t.is_tweet_hashtag_form_focused=!1},change:function(e){t.tweet_hashtag=t.formatHashtag(t.tweet_hashtag)}}}),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tweet-form__hashtag-list-button",on:{click:function(e){t.is_hashtag_list_display=!t.is_hashtag_list_display}}},[e("Icon",{attrs:{icon:"fluent:clipboard-text-ltr-32-regular",height:"22px"}})],1)]),e("textarea",{directives:[{name:"model",rawName:"v-model",value:t.tweet_text,expression:"tweet_text"}],ref:"tweet_text",staticClass:"tweet-form__textarea",attrs:{placeholder:"ツイート"},domProps:{value:t.tweet_text},on:{input:[function(e){e.target.composing||(t.tweet_text=e.target.value)},function(e){return t.updateTweetLetterCount()}],paste:function(e){return t.pasteClipboardData(e)},focus:function(e){t.is_tweet_text_form_focused=!0},blur:function(e){t.is_tweet_text_form_focused=!1}}}),e("div",{staticClass:"tweet-form__control"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"account-button",class:{"account-button--no-login":!t.is_logged_in_twitter},on:{click:function(e){return t.clickAccountButton()}}},[e("img",{staticClass:"account-button__icon",attrs:{src:t.is_logged_in_twitter?t.selected_twitter_account.icon_url:"/assets/images/account-icon-default.png"}}),e("span",{staticClass:"account-button__screen-name"},[t._v(" "+t._s(t.is_logged_in_twitter?`@${t.selected_twitter_account.screen_name}`:"連携されていません")+" ")]),e("Icon",{staticClass:"account-button__menu",attrs:{icon:"fluent:more-circle-20-regular",width:"22px"}})],1),e("div",{staticClass:"limit-meter"},[e("div",{staticClass:"limit-meter__content",class:{"limit-meter__content--yellow":t.tweet_letter_count<=20,"limit-meter__content--red":t.tweet_letter_count<=0}},[e("Icon",{staticStyle:{"margin-right":"-2px"},attrs:{icon:"fa-brands:twitter",width:"12px"}}),e("span",[t._v(t._s(t.tweet_letter_count))])],1),e("div",{staticClass:"limit-meter__content"},[e("Icon",{attrs:{icon:"fluent:image-16-filled",width:"14px"}}),e("span",[t._v(t._s(t.tweet_captures.length)+"/4")])],1)]),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tweet-button",attrs:{disabled:!t.is_logged_in_twitter||t.tweet_letter_count<0||140===t.tweet_letter_count&&0===t.tweet_captures.length},on:{click:function(e){return t.sendTweet()}}},[e("Icon",{attrs:{icon:"fa-brands:twitter",height:"16px"}}),e("span",{staticClass:"ml-1"},[t._v("ツイート")])],1)])]),e("div",{staticClass:"twitter-account-list",class:{"twitter-account-list--display":t.is_twitter_account_list_display}},t._l(t.user.twitter_accounts,(function(s){return e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],key:s.id,staticClass:"twitter-account",on:{click:function(e){return t.updateSelectedTwitterAccount(s)}}},[e("img",{staticClass:"twitter-account__icon",attrs:{src:s.icon_url}}),e("div",{staticClass:"twitter-account__info"},[e("div",{staticClass:"twitter-account__name"},[t._v(t._s(s.name))]),e("div",{staticClass:"twitter-account__screen-name"},[t._v("@"+t._s(s.screen_name))])]),e("Icon",{directives:[{name:"show",rawName:"v-show",value:s.id===t.selected_twitter_account_id,expression:"twitter_account.id === selected_twitter_account_id"}],staticClass:"twitter-account__check",attrs:{icon:"fluent:checkmark-16-filled",width:"24px"}})],1)})),0),e("div",{staticClass:"hashtag-list",class:{"hashtag-list--display":t.is_hashtag_list_display,"hashtag-list--virtual-keyboard-display":t.is_virtual_keyboard_display&&t.Utils.hasActiveElementClass("hashtag__input")}},[e("div",{staticClass:"hashtag-heading"},[e("div",{staticClass:"hashtag-heading__text"},[e("Icon",{attrs:{icon:"charm:hash",width:"17px"}}),e("span",{staticClass:"ml-1"},[t._v("ハッシュタグリスト")])],1),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag-heading__add-button",on:{click:function(e){t.saved_twitter_hashtags.push({id:Date.now(),text:"#ここにハッシュタグを入力",editing:!1})}}},[e("Icon",{attrs:{icon:"fluent:add-12-filled",width:"17px"}}),e("span",{staticClass:"ml-1"},[t._v("追加")])],1)]),e("draggable",{staticClass:"hashtag-container",attrs:{handle:".hashtag__sort-handle"},model:{value:t.saved_twitter_hashtags,callback:function(e){t.saved_twitter_hashtags=e},expression:"saved_twitter_hashtags"}},t._l(t.saved_twitter_hashtags,(function(s){return e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple",value:!s.editing,expression:"!hashtag.editing"}],key:s.id,staticClass:"hashtag",class:{"hashtag--editing":s.editing},on:{click:function(e){t.tweet_hashtag=s.text,t.tweet_hashtag=t.formatHashtag(t.tweet_hashtag),t.updateTweetLetterCount(),t.window.setTimeout((()=>t.is_hashtag_list_display=!1),150)}}},[e("input",{directives:[{name:"model",rawName:"v-model",value:s.text,expression:"hashtag.text"}],staticClass:"hashtag__input",attrs:{type:"search",disabled:!s.editing},domProps:{value:s.text},on:{click:function(t){t.stopPropagation()},input:function(e){e.target.composing||t.$set(s,"text",e.target.value)}}}),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag__edit-button",on:{click:function(e){e.preventDefault(),e.stopPropagation(),s.editing=!s.editing,s.text=t.formatHashtag(s.text,!0)}}},[e("Icon",{attrs:{icon:s.editing?"fluent:checkmark-16-filled":"fluent:edit-16-filled",width:"17px"}})],1),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag__delete-button",on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.saved_twitter_hashtags.splice(t.saved_twitter_hashtags.indexOf(s),1)}}},[e("Icon",{attrs:{icon:"fluent:delete-16-filled",width:"17px"}})],1),e("div",{staticClass:"hashtag__sort-handle"},[e("Icon",{attrs:{icon:"material-symbols:drag-handle-rounded",width:"17px"}})],1)])})),0)],1)],1)},me=[function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"capture-announce__text"},[e("p",{staticClass:"mt-0 mb-0"},[t._v("プレイヤーのキャプチャボタンやショートカットキーでキャプチャを撮ると、ここに表示されます。")]),e("p",{staticClass:"mt-2 mb-0"},[t._v("表示されたキャプチャを選択してからツイートすると、キャプチャを付けてツイートできます。")])])}],ue=s(9980),pe=s.n(ue),he=n["default"].extend({name:"Panel-TwitterTab",components:{draggable:pe()},props:{channel:{type:Object,required:!0},player:{type:null,required:!0},is_virtual_keyboard_display:{type:Boolean,required:!0}},data(){return{Utils:R,window:window,is_logged_in:null!==R.getAccessToken(),is_logged_in_twitter:!1,user:null,selected_twitter_account:null,selected_twitter_account_id:R.getSettingsItem("selected_twitter_account_id"),is_twitter_account_list_display:!1,saved_twitter_hashtags:R.getSettingsItem("saved_twitter_hashtags").map(((t,e)=>({id:Date.now()+e,text:t,editing:!1}))),is_hashtag_list_display:!1,twitter_active_tab:R.getSettingsItem("twitter_active_tab"),zoom_capture_modal:!1,zoom_capture:null,captures:[],captures_element:null,is_tweet_hashtag_form_focused:!1,is_tweet_text_form_focused:!1,tweet_hashtag:"",tweet_text:"",tweet_captures:[],tweet_letter_count:140}},async created(){if(this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&(await this.syncAccountInfo(),this.user.twitter_accounts.length>0)){this.is_logged_in_twitter=!0,null!==this.selected_twitter_account_id&&this.user.twitter_accounts.some((t=>t.id===this.selected_twitter_account_id))||(this.selected_twitter_account_id=this.user.twitter_accounts[0].id,R.setSettingsItem("selected_twitter_account_id",this.selected_twitter_account_id));const t=this.user.twitter_accounts.findIndex((t=>t.id===this.selected_twitter_account_id));this.selected_twitter_account=this.user.twitter_accounts[t]}this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag)},beforeDestroy(){for(const t of this.captures)URL.revokeObjectURL(t.image_url)},watch:{async channel(t,e){var s;if(t.channel_id!==e.channel_id){const t=null!==(s=this.getChannelHashtag(e.channel_name))&&void 0!==s?s:"";this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag.replaceAll(t,""))}},saved_twitter_hashtags:{deep:!0,handler(){R.setSettingsItem("saved_twitter_hashtags",this.saved_twitter_hashtags.map((t=>t.text)))}}},methods:{async syncAccountInfo(){try{this.user=(await n["default"].axios.get("/users/me")).data}catch(t){F.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},updateTweetLetterCount(){this.tweet_letter_count=140-[...this.tweet_hashtag].length-[...this.tweet_text].length},clickAccountButton(){if(!this.is_logged_in_twitter)return document.fullscreenElement&&document.exitFullscreen(),void this.$router.push({path:"/settings/twitter"});this.is_twitter_account_list_display=!this.is_twitter_account_list_display,!0===this.is_twitter_account_list_display&&(this.is_hashtag_list_display=!1)},pasteClipboardData(t){for(const e of t.clipboardData.items)if(e.type.startsWith("image/")){const t=e.getAsFile();this.addCaptureList(t,t.name)}},updateSelectedTwitterAccount(t){this.selected_twitter_account_id=t.id,R.setSettingsItem("selected_twitter_account_id",this.selected_twitter_account_id),this.selected_twitter_account=t,window.setTimeout((()=>this.is_twitter_account_list_display=!1),150)},clickCapture(t){if(this.tweet_captures.length<4&&!1===t.selected)t.selected=!0,this.tweet_captures.push(t.blob);else{const e=this.tweet_captures.findIndex((e=>e===t.blob));e>-1&&this.tweet_captures.splice(e,1),t.selected=!1}},async addCaptureList(t,e){null===this.captures_element&&(this.captures_element=this.$el.querySelector(".tab-content")),this.captures.length>50&&(URL.revokeObjectURL(this.captures[0].image_url),this.captures.shift());const s=URL.createObjectURL(t);this.captures.push({blob:t,filename:e,image_url:s,selected:!1,focused:!1}),this.$nextTick((()=>{this.captures_element.scrollTo({top:this.captures_element.scrollHeight,behavior:"smooth"})}))},async drawProgramTitleOnCapture(t){const e=await createImageBitmap(t),s="OffscreenCanvas"in window?new OffscreenCanvas(e.width,e.height):document.createElement("canvas"),i=s.getContext("2d");switch(i.drawImage(e,0,0),e.close(),i.font="bold 22px 'YakuHanJPs', 'Open Sans', 'Hiragino Sans', 'Noto Sans JP', sans-serif",i.fillStyle="rgba(255, 255, 255, 70%)",i.shadowColor="rgba(0, 0, 0, 100%)",i.shadowBlur=4,i.shadowOffsetX=0,i.shadowOffsetY=0,R.getSettingsItem("tweet_capture_watermark_position")){case"TopLeft":i.textAlign="left",i.textBaseline="top",i.fillText(this.channel.program_present.title,16,12);break;case"TopRight":i.textAlign="right",i.textBaseline="top",i.fillText(this.channel.program_present.title,s.width-16,12);break;case"BottomLeft":i.textAlign="left",i.textBaseline="bottom",i.fillText(this.channel.program_present.title,16,s.height-12);break;case"BottomRight":i.textAlign="right",i.textBaseline="bottom",i.fillText(this.channel.program_present.title,s.width-16,s.height-12);break}return"OffscreenCanvas"in window?await s.convertToBlob({type:"image/jpeg",quality:1}):new Promise((t=>s.toBlob((e=>t(e)),"image/jpeg",1)))},getChannelHashtag(t){return t.startsWith("NHK総合")?"#nhk":t.startsWith("NHKEテレ")?"#etv":t.startsWith("日テレ")?"#ntv":t.startsWith("読売テレビ")?"#ytv":t.startsWith("中京テレビ")?"#chukyotv":t.startsWith("テレビ朝日")?"#tvasahi":t.startsWith("ABCテレビ")?"#abc":t.startsWith("メ~テレ")?"#nagoyatv":t.startsWith("TBS")&&!t.includes("TBSチャンネル")?"#tbs":t.startsWith("MBS")?"#mbs":t.startsWith("CBC")?"#cbc":t.startsWith("テレビ東京")?"#tvtokyo":t.startsWith("テレビ大阪")?"#tvo":t.startsWith("テレビ愛知")?"#tva":t.startsWith("フジテレビ")?"#fujitv":t.startsWith("関西テレビ")?"#kantele":t.startsWith("東海テレビ")?"#tokaitv":t.startsWith("TOKYO MX")?"#tokyomx":t.startsWith("tvk")?"#tvk":t.startsWith("チバテレ")?"#chibatv":t.startsWith("テレ玉")?"#teletama":t.startsWith("サンテレビ")?"#suntv":t.startsWith("KBS京都")?"#kbs":t.startsWith("NHKBS1")?"#nhkbs1":t.startsWith("NHKBSプレミアム")?"#nhkbsp":t.startsWith("BS日テレ")?"#bsntv":t.startsWith("BS朝日")?"#bsasahi":t.startsWith("BS-TBS")?"#bstbs":t.startsWith("BSテレ東")?"#bstvtokyo":t.startsWith("BSフジ")?"#bsfuji":t.startsWith("BS11イレブン")?"#bs11":t.startsWith("BS12トゥエルビ")?"#bs12":t.startsWith("AT-X")?"#at_x":null},formatHashtag(t,e=!1){const s=t.trim().replaceAll("♯","#").replaceAll("#","#").replace(/#{2,}/g,"#").replaceAll(" "," ").replaceAll(/ +/g," ").split(" ").filter((t=>""!==t));for(let i in s)s[i].startsWith("#")||(s[i]=`#${s[i]}`);if(!0===R.getSettingsItem("auto_add_watching_channel_hashtag")&&!1===e){const t=this.getChannelHashtag(this.channel.channel_name);null!==t&&!1===s.includes(t)&&s.push(t)}return s.join(" ")},async sendTweet(){this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag);const t=this.tweet_hashtag;let e=this.tweet_text;if(""!==t)switch(R.getSettingsItem("tweet_hashtag_position")){case"Prepend":e=`${t} ${this.tweet_text}`;break;case"Append":e=`${this.tweet_text} ${t}`;break;case"PrependWithLineBreak":e=`${t}\n${this.tweet_text}`;break;case"AppendWithLineBreak":e=`${this.tweet_text}\n${t}`;break}const s=new FormData;s.append("tweet",e);for(let a of this.tweet_captures)"None"!==R.getSettingsItem("tweet_capture_watermark_position")&&(a=await this.drawProgramTitleOnCapture(a)),s.append("images",a);for(const a of this.captures)a.selected=!1,a.focused=!1;this.tweet_captures=[],this.tweet_text="",!0===R.getSettingsItem("fold_panel_after_sending_tweet")&&(this.$emit("panel_folding_requested"),this.$refs.tweet_text.blur());try{const t=await n["default"].axios.post(`/twitter/accounts/${this.selected_twitter_account.screen_name}/tweets`,s,{headers:{"Content-Type":"multipart/form-data"}});!0===t.data.is_success?this.player.notice(t.data.detail):this.player.notice("エラー: "+t.data.detail)}catch(i){console.error(i),this.player.notice("エラー: ツイートの送信に失敗しました。")}}}}),ge=he,ve=(0,p.Z)(ge,de,me,!1,null,"df5cea26",null),fe=ve.exports;const we=1.5,ye=3;var be=n["default"].extend({name:"TV-Watch",components:{Channel:Bt,Comment:ae,Program:_e,Twitter:fe},data(){return{Utils:R,ProgramUtils:L,time:A()().format("YYYY/MM/DD HH:mm:ss"),tv_panel_active_tab:R.getSettingsItem("tv_panel_active_tab"),background_url:"",is_loading:!0,is_video_buffering:!0,is_background_display:!1,is_control_display:!0,is_panel_display:(()=>{switch(R.getSettingsItem("panel_display_state")){case"AlwaysDisplay":return!0;case"AlwaysFold":return!1;case"RestorePreviousState":return R.getSettingsItem("showed_panel_last_time")}})(),is_fullscreen:!1,is_ime_composing:!1,is_virtual_keyboard_display:!1,is_comment_send_just_did:!1,interval_ids:[],control_interval_id:0,channel_id:this.$route.params.channel_id,channel:At,channel_previous:At,channel_next:At,channels_list:new Map,player:null,romsounds_context:null,romsounds_buffers:[],eventsource:null,fullscreen_handler:null,capture_handler:null,shortcut_key_handler:null,shortcut_key_pressed_at:Date.now(),shortcut_key_modal:!1,shortcut_key_list:{left_column:[{name:"全般",icon:"fluent:home-20-filled",icon_height:"22px",shortcuts:[{name:"数字キー・テンキーに対応するリモコン番号 (1~12) の地デジチャンネルに切り替える",keys:[{name:"1~9, 0, -(=), ^(~)",icon:!1}]},{name:"数字キー・テンキーに対応するリモコン番号 (1~12) の BS チャンネルに切り替える",keys:[{name:"Shift",icon:!1},{name:"1~9, 0, -(=), ^(~)",icon:!1}]},{name:"前のチャンネルに切り替える",keys:[{name:"fluent:arrow-up-12-filled",icon:!0}]},{name:"次のチャンネルに切り替える",keys:[{name:"fluent:arrow-down-12-filled",icon:!0}]},{name:"キーボードショートカットの一覧を表示する",keys:[{name:"/(?)",icon:!1}]}]},{name:"プレイヤー",icon:"fluent:play-20-filled",icon_height:"20px",shortcuts:[{name:"再生 / 一時停止の切り替え",keys:[{name:"Space",icon:!1}]},{name:"再生 / 一時停止の切り替え (キャプチャタブ表示時)",keys:[{name:"Shift",icon:!1},{name:"Space",icon:!1}]},{name:"プレイヤーの音量を上げる",keys:[{name:R.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-up-12-filled",icon:!0}]},{name:"プレイヤーの音量を下げる",keys:[{name:R.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-down-12-filled",icon:!0}]},{name:"停止して0.5秒早戻し",keys:[{name:R.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-left-12-filled",icon:!0}]},{name:"停止して0.5秒早送り",keys:[{name:R.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-right-12-filled",icon:!0}]},{name:"フルスクリーンの切り替え",keys:[{name:"F",icon:!1}]},{name:"ライブストリームの同期",keys:[{name:"W",icon:!1}]},{name:"Picture-in-Picture の表示切り替え",keys:[{name:"E",icon:!1}]},{name:"字幕の表示切り替え",keys:[{name:"S",icon:!1}]},{name:"コメントの表示切り替え",keys:[{name:"D",icon:!1}]},{name:"映像をキャプチャする",keys:[{name:"C",icon:!1}]},{name:"映像をコメントを付けてキャプチャする",keys:[{name:"V",icon:!1}]},{name:"コメント入力フォームにフォーカスする",keys:[{name:"M",icon:!1}]},{name:"コメント入力フォームを閉じる",keys:[{name:R.CtrlOrCmd(),icon:!1},{name:"M",icon:!1}]}]}],right_column:[{name:"パネル",icon:"fluent:panel-right-20-filled",icon_height:"24px",shortcuts:[{name:"パネルの表示切り替え",keys:[{name:"P",icon:!1}]},{name:"番組情報タブを表示する",keys:[{name:"K",icon:!1}]},{name:"チャンネルタブを表示する",keys:[{name:"L",icon:!1}]},{name:"コメントタブを表示する",keys:[{name:";(+)",icon:!1}]},{name:"Twitter タブを表示する",keys:[{name:":(*)",icon:!1}]}]},{name:"Twitter",icon:"fa-brands:twitter",icon_height:"22px",shortcuts:[{name:"ツイート検索タブを表示する",keys:[{name:"[ (「)",icon:!1}]},{name:"タイムラインタブを表示する",keys:[{name:"] (」)",icon:!1}]},{name:"キャプチャタブを表示する",keys:[{name:"\(¥)",icon:!1}]},{name:"ツイート入力フォームにフォーカスを当てる/フォーカスを外す",keys:[{name:"Tab",icon:!1}]},{name:"キャプチャにフォーカスする",keys:[{name:"キャプチャタブを表示",icon:!1},{name:"fluent:arrow-up-12-filled;fluent:arrow-down-12-filled;fluent:arrow-left-12-filled;fluent:arrow-right-12-filled",icon:!0}]},{name:"キャプチャを拡大表示する/
キャプチャの拡大表示を閉じる",keys:[{name:"キャプチャにフォーカス",icon:!1},{name:"Enter",icon:!1}]},{name:"キャプチャを選択する/
キャプチャの選択を解除する",keys:[{name:"キャプチャにフォーカス",icon:!1},{name:"Space",icon:!1}]},{name:"クリップボード内の画像を
キャプチャとして取り込む",keys:[{name:"ツイート入力
フォームにフォーカス",icon:!1},{name:R.CtrlOrCmd(),icon:!1},{name:"V",icon:!1}]},{name:"ツイートを送信する",keys:[{name:"Twitter タブを表示",icon:!1},{name:R.CtrlOrCmd(),icon:!1},{name:"Enter",icon:!1}]}]}]}}},async created(){"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!0,navigator.virtualKeyboard.ongeometrychange=t=>{0===t.target.boundingRect.width&&0===t.target.boundingRect.height?this.is_virtual_keyboard_display=!1:this.is_virtual_keyboard_display=!0}),this.init(),this.romsounds_context=new AudioContext;for(let t=1;t<=14;t++){const e=`/assets/romsounds/${t.toString().padStart(2,"0")}.wav`,s=await n["default"].axios.get(e,{baseURL:"",responseType:"arraybuffer"});this.romsounds_buffers.push(await this.romsounds_context.decodeAudioData(s.data))}},beforeDestroy(){"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!1),this.destroy(!0),this.romsounds_context.close()},beforeRouteUpdate(t,e,s){this.destroy(),this.channel_id=t.params.channel_id,[this.channel_previous,this.channel,this.channel_next]=T.getPreviousAndCurrentAndNextChannel(this.channels_list,this.channel_id),!0===R.getSettingsItem("reset_hashtag_when_program_switches")&&(this.$refs.Twitter.tweet_hashtag=""),this.interval_ids.push(window.setTimeout((()=>this.init()),500)),s()},watch:{is_panel_display(){R.setSettingsItem("showed_panel_last_time",this.is_panel_display)}},methods:{init(){this.background_url=N.generatePlayerBackgroundURL(),this.controlDisplayTimer(),this.update(),this.interval_ids.push(window.setInterval((()=>{this.time=A()().format("YYYY/MM/DD HH:mm:ss")}),1e3));const t=60-Math.floor((new Date).getTime()/1e3)%60;this.interval_ids.push(window.setTimeout((()=>{this.update(),this.interval_ids.push(window.setInterval((()=>{this.update()}),3e4))}),1e3*t))},async update(){if(void 0===this.$route.params.channel_id)return;let t;try{t=await n["default"].axios.get(`/channels/${this.channel_id}`)}catch(r){return console.error(r),void(r.response&&422===r.response.status&&"Specified channel_id was not found"===r.response.data.detail&&await this.$router.push({path:"/not-found/"}))}const e=t.data;let s;(this.channel.id!==e.id||null!==this.channel.program_present&&null===e.program_present||null===this.channel.program_present&&null!==e.program_present||this.channel.program_present.id!==e.program_present.id)&&!0===R.getSettingsItem("reset_hashtag_when_program_switches")&&(this.$refs.Twitter.tweet_hashtag=""),this.channel=e,null!==this.player&&!0!==this.player.KonomiTVCanDestroy||(this.initPlayer(),this.initEventHandler(),this.initCaptureHandler(),document.removeEventListener("keydown",this.shortcut_key_handler),this.initShortcutKeyHandler()),null===this.channel.program_present||"1/0+1/0モード(デュアルモノ)"!==this.channel.program_present.primary_audio_type&&null===this.channel.program_present.secondary_audio_type?(this.player.template.audioItem[1].classList.add("dplayer-setting-audio-item--disabled"),this.player.plugins.mpegts&&window.setTimeout((()=>{this.player.template.audioItem[0].classList.add("dplayer-setting-audio-current"),this.player.template.audioItem[1].classList.remove("dplayer-setting-audio-current"),this.player.template.audioValue.textContent=this.player.tran("Primary audio");try{this.player.plugins.mpegts.switchPrimaryAudio()}catch(r){}}),300)):this.player.template.audioItem[1].classList.remove("dplayer-setting-audio-item--disabled");try{s=await n["default"].axios.get("/channels")}catch(r){return void console.error(r)}const i=t=>t.is_display||this.channel_id===t.channel_id;this.channels_list=new Map,this.channels_list.set("ピン留め",[]),s.data.GR.length>0&&this.channels_list.set("地デジ",s.data.GR.filter(i)),s.data.BS.length>0&&this.channels_list.set("BS",s.data.BS.filter(i)),s.data.CS.length>0&&this.channels_list.set("CS",s.data.CS.filter(i)),s.data.CATV.length>0&&this.channels_list.set("CATV",s.data.CATV.filter(i)),s.data.SKY.length>0&&this.channels_list.set("SKY",s.data.SKY.filter(i)),s.data.STARDIGIO.length>0&&this.channels_list.set("StarDigio",s.data.STARDIGIO.filter(i));const a=R.getSettingsItem("pinned_channel_ids"),o=[];for(const n of a){const t=T.getChannelType(n,!0),e=this.channels_list.get(t).find((t=>t.channel_id===n));void 0!==e&&o.push(e)}if(o.length>0?this.channels_list.set("ピン留め",o):this.channels_list.delete("ピン留め"),[this.channel_previous,,this.channel_next]=T.getPreviousAndCurrentAndNextChannel(this.channels_list,this.channel_id),"mediaSession"in navigator){const t=[{src:"/assets/images/icons/icon-maskable-192px.png",sizes:"192x192",type:"image/png"},{src:"/assets/images/icons/icon-maskable-512px.png",sizes:"512x512",type:"image/png"}];navigator.mediaSession.metadata=new MediaMetadata({title:this.channel.program_present?this.channel.program_present.title:"放送休止",artist:this.channel.channel_name,artwork:t}),"setPositionState"in navigator.mediaSession&&navigator.mediaSession.setPositionState({duration:0,playbackRate:1}),navigator.mediaSession.setActionHandler("play",(()=>{this.player.play()})),navigator.mediaSession.setActionHandler("pause",(()=>{this.player.pause()})),navigator.mediaSession.setActionHandler("previoustrack",(async()=>{navigator.mediaSession.metadata=new MediaMetadata({title:this.channel_previous.program_present?this.channel_previous.program_present.title:"放送休止",artist:this.channel_previous.channel_name,artwork:t}),await this.$router.push({path:`/tv/watch/${this.channel_previous.channel_id}`})})),navigator.mediaSession.setActionHandler("nexttrack",(async()=>{navigator.mediaSession.metadata=new MediaMetadata({title:this.channel_next.program_present?this.channel_next.program_present.title:"放送休止",artist:this.channel_next.channel_name,artwork:t}),await this.$router.push({path:`/tv/watch/${this.channel_next.channel_id}`})}))}},controlDisplayTimer(t=null,e=!1){const s=/iPhone|iPad|iPod|Windows|Macintosh|Android|Mobile/i.test(navigator.userAgent)&&"ontouchend"in document;if(1==s&&null!==t&&"mousemove"===t.type)return;if(0==s&&null!==t&&("touchmove"===t.type||"click"===t.type))return;window.clearTimeout(this.control_interval_id);const i=()=>{null!==this.player&&this.player.template.controller.classList.contains("dplayer-controller-comment")?this.control_interval_id=window.setTimeout(i,3e3):(this.is_control_display=!1,null!==this.player&&(this.player.controller.hide(),this.player.setting.hide()))};!0===s&&!0===e?this.player.controller.isShow()?(this.is_control_display=!0,this.player.controller.show(),this.control_interval_id=window.setTimeout(i,3e3)):(this.is_control_display=!1,this.player.controller.hide(),this.player.setting.hide()):(this.is_control_display=!0,null!==this.player&&this.player.controller.show(),this.control_interval_id=window.setTimeout(i,3e3))},initPlayer(){if(window.mpegts=Pt(),null!==this.player&&!0===this.player.KonomiTVCanDestroy){try{this.player.destroy()}catch(a){void 0!==this.player.plugins.mpegts&&this.player.plugins.mpegts.destroy()}this.player=null}const t=R.getSettingsItem("tv_low_latency_mode")?we:ye;this.player=new(Tt())({container:this.$el.querySelector(".watch-player__dplayer"),theme:"#E64F97",lang:"ja-jp",live:!0,liveSyncMinBufferSize:t,loop:!1,airplay:!1,autoplay:!0,hotkey:!1,screenshot:!1,volume:1,video:{defaultQuality:this.channel.is_radiochannel?"48kHz/192kbps":R.getSettingsItem("tv_streaming_quality"),quality:(()=>{const t=[];if(this.channel.is_radiochannel)t.push({name:"48kHz/192kbps",type:"mpegts",url:`${R.api_base_url}/streams/live/${this.channel_id}/1080p/mpegts`});else{let e="";N.isHEVCVideoSupported()&&!0===R.getSettingsItem("tv_data_saver_mode")&&(e="-hevc");for(const s of["1080p-60fps","1080p","810p","720p","540p","480p","360p","240p"])t.push({name:"1080p-60fps"===s?"1080p (60fps)":s,type:"mpegts",url:`${R.api_base_url}/streams/live/${this.channel_id}/${s}${e}/mpegts`})}return t})()},danmaku:{user:"KonomiTV",speedRate:R.getSettingsItem("comment_speed_rate"),fontSize:R.getSettingsItem("comment_font_size")},apiBackend:{read:t=>{t.success([{}])},send:async t=>{await this.$refs.Comment.sendComment(t)}},pluginOptions:{mpegts:{config:{enableWorker:!0,liveSync:R.getSettingsItem("tv_low_latency_mode"),liveSyncMaxLatency:3,liveSyncTargetLatency:t,liveSyncPlaybackRate:1.1}},aribb24:{normalFont:`"${R.getSettingsItem("caption_font")}", sans-serif`,forceStrokeColor:!!R.getSettingsItem("always_border_caption_text"),forceBackgroundColor:R.getSettingsItem("specify_caption_background_color")?R.getSettingsItem("caption_background_color"):null,drcsReplacement:!0,enableRawCanvas:!0,useStrokeText:!0,usePUA:(()=>{const t=R.getSettingsItem("caption_font"),e=document.createElement("canvas").getContext("2d");return e.font=`10px ${t}`,e.fillText("Test",0,0),!!t.startsWith("Windows TV")})(),PRACallback:async t=>{if(!1===R.getSettingsItem("tv_show_superimpose"))return;"suspended"===this.romsounds_context.state&&await this.romsounds_context.resume();const e=this.romsounds_context.createBufferSource();e.buffer=this.romsounds_buffers[t];const s=this.romsounds_context.createGain();e.connect(s),s.connect(this.romsounds_context.destination),s.gain.value=3,e.start(0)}}},subtitle:{type:"aribb24"}}),window.player=this.player,this.player.controller.setAutoHide=t=>{},this.player.template.commentInput.addEventListener("keydown",(t=>{"Enter"===t.code&&(this.is_comment_send_just_did=!0,setTimeout((()=>this.is_comment_send_just_did=!1),100))})),this.player.comment.send=()=>{!0===R.getSettingsItem("close_comment_form_after_sending")&&this.player.template.commentInput.blur(),this.player.template.commentInput.value.replace(/^\s+|\s+$/g,"")?(this.player.danmaku.send({text:this.player.template.commentInput.value,color:this.player.container.querySelector(".dplayer-comment-setting-color input:checked").value,type:this.player.container.querySelector(".dplayer-comment-setting-type input:checked").value,size:this.player.container.querySelector(".dplayer-comment-setting-size input:checked").value},(()=>{!0===R.getSettingsItem("close_comment_form_after_sending")&&this.player.comment.hide()}),!0),this.player.template.commentInput.value=""):this.player.notice(this.player.tran("Please input danmaku content!"))};const e=/iPhone|iPad|iPod|Macintosh|Android|Mobile/i.test(navigator.userAgent)&&"ontouchend"in document;if(!1===e){this.player.template.settingOriginPanel.insertAdjacentHTML("beforeend",'\n
\n キーボードショートカット\n
\n \n \n \n
\n
');const t=this.player.template.settingOriginPanel.scrollHeight;this.player.template.settingBox.style.clipPath=`inset(calc(100% - ${t}px) 0 0 round 7px)`,this.$el.querySelector(".dplayer-setting-keyboard-shortcut").addEventListener("click",(()=>{this.player.setting.hide(),this.shortcut_key_modal=!0}))}const s=document.querySelector(".v-application");this.fullscreen_handler=()=>this.is_fullscreen=this.player.fullScreen.isFullScreen(),void 0!==s.onfullscreenchange?s.addEventListener("fullscreenchange",this.fullscreen_handler):s.addEventListener("webkitfullscreenchange",this.fullscreen_handler),this.player.fullScreen.isFullScreen=t=>!(!document.fullscreenElement&&!document.webkitFullscreenElement),this.player.fullScreen.request=t=>{this.player.fullScreen.isFullScreen()?this.player.fullScreen.cancel():(s.requestFullscreen=s.requestFullscreen||s.webkitRequestFullscreen,s.requestFullscreen&&s.requestFullscreen(),screen.orientation&&screen.orientation.lock("landscape").catch((()=>{})))},this.player.fullScreen.cancel=t=>{document.exitFullscreen=document.exitFullscreen||document.webkitExitFullscreen,document.exitFullscreen&&document.exitFullscreen(),screen.orientation&&screen.orientation.unlock()};const i=()=>{this.player.setting.hide(),this.controlDisplayTimer()};this.player.on("play",i),this.player.on("pause",i),this.player.on("quality_start",(()=>{this.background_url=N.generatePlayerBackgroundURL(),null!==this.eventsource&&(this.eventsource.close(),this.eventsource=null),this.initEventHandler()})),this.interval_ids.push(window.setInterval((()=>{this.player.video.paused&&this.player.video.buffered.end(0)-this.player.video.currentTime>30&&this.player.sync()}),6e4)),!0===R.getSettingsItem("tv_show_superimpose")?(this.player.plugins.aribb24Superimpose.show(),this.player.on("subtitle_hide",(()=>{this.player.plugins.aribb24Superimpose.show()}))):(this.player.plugins.aribb24Superimpose.hide(),this.player.on("subtitle_show",(()=>{this.player.plugins.aribb24Superimpose.hide()})))},initEventHandler(){this.is_loading=!0,this.player.video.volume=0;const t=()=>{this.player.video.oncanplay=null,this.player.video.oncanplaythrough=null,this.player.video.playbackRate=0,window.setTimeout((async()=>{const t=()=>{try{return Math.round(1e3*(this.player.video.buffered.end(0)-this.player.video.currentTime))/1e3}catch(t){return 0}},e=R.getSettingsItem("tv_low_latency_mode")?we:ye;let s=t();while(sthis.is_video_buffering=!0)),this.player.video.addEventListener("playing",(()=>this.is_video_buffering=!1)),this.is_loading=!1,this.is_video_buffering=!1,this.channel.is_radiochannel?this.is_background_display=!0:this.is_background_display=!1;const i=this.player.user.get("volume");while(this.player.video.volume+.05{const e=JSON.parse(t.data);"Standby"===e.status&&(this.is_video_buffering=!0,this.is_background_display=!0)})),this.eventsource.addEventListener("status_update",(t=>{const e=JSON.parse(t.data);switch(console.log(`Status: ${e.status} / Detail: ${e.detail}`),this.channel.viewers=e.clients_count,e.status){case"Standby":this.player.template.notice.textContent.includes("画質を")||this.player.notice(e.detail,-1),this.is_video_buffering=!0,this.is_background_display=!0;break;case"ONAir":this.player.template.notice.textContent.includes("画質を")||this.player.notice(this.player.template.notice.textContent,1e-6),document.pictureInPictureElement&&(document.exitPictureInPicture(),this.player.video.requestPictureInPicture());break;case"Restart":this.player.notice(e.detail,-1),this.player.switchVideo({url:this.player.quality.url,type:this.player.quality.type}),this.player.play(),this.is_video_buffering=!0,this.is_background_display=!0;break;case"Offline":this.player.notice(e.detail,-1),this.player.video.onerror=()=>{this.player.notice(e.detail,-1),this.player.video.onerror=null},this.player.danmaku.clear(),this.player.video.pause(),this.eventsource.close(),this.is_background_display=!0,this.is_loading=!1,this.is_video_buffering=!1;break}})),this.eventsource.addEventListener("detail_update",(t=>{const e=JSON.parse(t.data);console.log(`Status: ${e.status} Detail:${e.detail}`),this.channel.viewers=e.clients_count,"Standby"===e.status&&(this.player.notice(e.detail,-1),this.is_background_display||(this.is_background_display=!0))})),this.eventsource.addEventListener("clients_update",(t=>{const e=JSON.parse(t.data);this.channel.viewers=e.clients_count}))},initShortcutKeyHandler(){const t=this.$refs.Twitter,e=t.$el.querySelector(".tweet-form__textarea");for(const s of document.querySelectorAll("input[type=text],input[type=search],textarea"))s.addEventListener("compositionstart",(()=>this.is_ime_composing=!0)),s.addEventListener("compositionend",(()=>this.is_ime_composing=!1));this.shortcut_key_handler=async s=>{const i=document.activeElement.tagName.toUpperCase(),a=document.activeElement.getAttribute("contenteditable");["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(s.code)&&"INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a&&s.preventDefault();let n=!1;s.repeat&&(n=!0);const o=Date.now();if(o-this.shortcut_key_pressed_at<50)return;this.shortcut_key_pressed_at=o;const r=await(async()=>{if(("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a||document.activeElement===e)&&!1===this.is_ime_composing&&"Tab"===s.code)return document.activeElement===e?(e.blur(),!0):(this.is_panel_display=!0,this.tv_panel_active_tab="Twitter",e.focus(),this.$el.scrollLeft=0,window.setTimeout((()=>{e.focus(),this.$el.scrollLeft=0}),100),!0);if(("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a||document.activeElement===e)&&"Twitter"===this.tv_panel_active_tab&&!1===this.is_ime_composing&&(s.ctrlKey||s.metaKey||s.shiftKey)&&"Enter"===s.code)return t.$el.querySelector(".tweet-button").click(),!0;if(null!==this.player&&!s.shiftKey&&!s.altKey&&this.player.template.controller.classList.contains("dplayer-controller-comment")&&(s.ctrlKey||s.metaKey)&&"KeyM"===s.code)return this.player.comment.hide(),!0;if("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a){if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.altKey){const t=s.shiftKey?"BS":"GR";let e=null;if("Digit1"!==s.code&&"Digit2"!==s.code&&"Digit3"!==s.code&&"Digit4"!==s.code&&"Digit5"!==s.code&&"Digit6"!==s.code&&"Digit7"!==s.code&&"Digit8"!==s.code&&"Digit9"!==s.code||(e=Number(s.code.replace("Digit",""))),"Digit0"===s.code&&(e=10),"Minus"===s.code&&(e=11),"Equal"===s.code&&(e=12),"Numpad1"!==s.code&&"Numpad2"!==s.code&&"Numpad3"!==s.code&&"Numpad4"!==s.code&&"Numpad5"!==s.code&&"Numpad6"!==s.code&&"Numpad7"!==s.code&&"Numpad8"!==s.code&&"Numpad9"!==s.code||(e=Number(s.code.replace("Numpad",""))),"Numpad0"===s.code&&(e=10),null!==e){const s=T.getChannelFromRemoconID(this.channels_list,t,e);if(null!==s&&s.channel_id!==this.channel_id)return await this.$router.push({path:`/tv/watch/${s.channel_id}`}),!0}}if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("Slash"===s.code)return this.shortcut_key_modal=!this.shortcut_key_modal,!0;if("KeyP"===s.code)return this.is_panel_display=!this.is_panel_display,!0;if("KeyK"===s.code)return this.tv_panel_active_tab="Program",!0;if("KeyL"===s.code)return this.tv_panel_active_tab="Channel",!0;if("Semicolon"===s.code)return this.tv_panel_active_tab="Comment",!0;if("Quote"===s.code)return this.tv_panel_active_tab="Twitter",!0;if("BracketRight"===s.code)return t.twitter_active_tab="Search",!0;if("Backslash"===s.code)return t.twitter_active_tab="Timeline",!0;if("IntlRo"===s.code)return t.twitter_active_tab="Capture",!0}if("Twitter"===this.tv_panel_active_tab&&"Capture"===t.twitter_active_tab&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(s.code)){if(0===t.captures.length)return!1;if(!1===t.captures.some((t=>!0===t.focused)))return t.captures[t.captures.length-1].focused=!0,!0;const e=t.captures.findIndex((t=>!0===t.focused));if("ArrowUp"===s.code){if(e-2<0)return!1;t.captures[e-2].focused=!0}if("ArrowDown"===s.code){if(e+2>t.captures.length-1)return!1;t.captures[e+2].focused=!0}if("ArrowLeft"===s.code){if(e-1<0)return!1;t.captures[e-1].focused=!0}if("ArrowRight"===s.code){if(e+1>t.captures.length-1)return!1;t.captures[e+1].focused=!0}t.captures[e].focused=!1;const i=t.captures.find((t=>!0===t.focused));!0===t.zoom_capture_modal&&(t.zoom_capture=i);const a=t.$el.querySelector(`img[src="${i.image_url}"]`).parentElement;return n?a.scrollIntoView({block:"nearest",inline:"nearest",behavior:"auto"}):a.scrollIntoView({block:"nearest",inline:"nearest",behavior:"smooth"}),!0}if("Enter"===s.code){if(this.is_comment_send_just_did)return!1;if(!0===t.zoom_capture_modal)return t.zoom_capture_modal=!1,!0;const e=t.captures.find((t=>!0===t.focused));return void 0!==e&&(t.zoom_capture=e,t.zoom_capture_modal=!0,!0)}if("Space"===s.code){const e=t.captures.find((t=>!0===t.focused));return void 0!==e&&(t.clickCapture(e),!0)}}if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("ArrowUp"===s.code)return await this.$router.push({path:`/tv/watch/${this.channel_previous.channel_id}`}),!0;if("ArrowDown"===s.code)return await this.$router.push({path:`/tv/watch/${this.channel_next.channel_id}`}),!0}if(null!==this.player&&!s.shiftKey&&!s.altKey){if((s.ctrlKey||s.metaKey)&&"ArrowUp"===s.code)return this.player.volume(this.player.volume()+.05),!0;if((s.ctrlKey||s.metaKey)&&"ArrowDown"===s.code)return this.player.volume(this.player.volume()-.05),!0;if((s.ctrlKey||s.metaKey)&&"ArrowLeft"===s.code)return!1===this.player.video.paused&&this.player.video.pause(),this.player.video.currentTime=this.player.video.currentTime-.5,!0;if((s.ctrlKey||s.metaKey)&&"ArrowRight"===s.code)return!1===this.player.video.paused&&this.player.video.pause(),this.player.video.currentTime=this.player.video.currentTime+.5,!0}if(null!==this.player&&!s.ctrlKey&&!s.metaKey&&!s.altKey&&!0===s.shiftKey&&"Space"===s.code&&!1===n&&"Twitter"===this.tv_panel_active_tab&&"Capture"===t.twitter_active_tab)return this.player.toggle(),!0;if(null!==this.player&&!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("Space"===s.code)return this.player.toggle(),!0;if("KeyF"===s.code)return this.player.fullScreen.toggle(),!0;if("KeyW"===s.code)return this.player.sync(),!0;if("KeyE"===s.code)return document.pictureInPictureEnabled&&this.player.template.pipButton.click(),!0;if("KeyS"===s.code)return this.player.subtitle.toggle(),this.player.subtitle.container.classList.contains("dplayer-subtitle-hide")?this.player.notice(`${this.player.tran("Hide subtitle")}`):this.player.notice(`${this.player.tran("Show subtitle")}`),!0;if("KeyD"===s.code)return this.player.template.showDanmaku.click(),this.player.template.showDanmakuToggle.checked?this.player.notice(`${this.player.tran("Show comment")}`):this.player.notice(`${this.player.tran("Hide comment")}`),!0;if("KeyC"===s.code)return await this.capture_handler.captureAndSave(this.channel,!1),!0;if("KeyV"===s.code)return await this.capture_handler.captureAndSave(this.channel,!0),!0;if("KeyM"===s.code)return this.player.controller.show(),this.player.comment.show(),this.controlDisplayTimer(),window.setTimeout((()=>this.player.template.commentInput.focus()),100),!0}}return!1})();!0===r&&s.preventDefault()},document.addEventListener("keydown",this.shortcut_key_handler)},initCaptureHandler(){this.capture_handler=new D(this.player,((t,e)=>{this.$refs.Twitter.addCaptureList(t,e)}));const t=this.$el.querySelector(".dplayer-icon.dplayer-capture-icon");t.addEventListener("click",(async()=>{await this.capture_handler.captureAndSave(this.channel,!1)}));const e=this.$el.querySelector(".dplayer-icon.dplayer-comment-capture-icon");e.addEventListener("click",(async()=>{await this.capture_handler.captureAndSave(this.channel,!0)}))},destroy(t=!1){for(const e of this.interval_ids)window.clearInterval(e);window.clearTimeout(this.control_interval_id),this.interval_ids=[],this.is_loading=!0,this.is_background_display=!1,this.player.KonomiTVCanDestroy=!0,null!==this.eventsource&&(this.eventsource.close(),this.eventsource=null),this.interval_ids.push(window.setTimeout((()=>{if(this.player.video.pause(),!0===t&&null!==this.player){try{this.player.destroy()}catch(e){void 0!==this.player.plugins.mpegts&&this.player.plugins.mpegts.destroy()}this.player=null}}),400))}}}),Ce=be,ke=(0,p.Z)(Ce,Ot,St,!1,null,"6a0c19bf",null),xe=ke.exports,Oe=s(9223),Se=s(4611),Ie=s(4650),Te=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e(wt.Z,{staticClass:"settings-container d-flex px-5 py-5 mx-auto background",attrs:{elevation:"0",width:"100%","max-width":"1000"}},[e(Ie.Z,{staticClass:"settings-navigation flex-shrink-0 background",attrs:{permanent:"",width:"100%",height:"auto"}},[e(Ht.Z,{staticClass:"px-4"},[e(Vt.km,[e("h1",[t._v("設定")])])],1),e(Et.Z,{staticClass:"mt-2 px-0",attrs:{nav:""}},[e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/general"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 3px"},attrs:{icon:"fa-solid:sliders-h",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("全般")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/account"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("アカウント")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/jikkyo"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 2px"},attrs:{icon:"bi:chat-left-text-fill",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("ニコニコ実況")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/twitter"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 1px"},attrs:{icon:"fa-brands:twitter",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("Twitter")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/environment"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("環境設定")])],1)],1)],1)],1)],1)],1)],1)},je=[],Pe=n["default"].extend({name:"Settings-Index",components:{Header:lt,Navigation:pt}}),Ze=Pe,Ae=(0,p.Z)(Ze,Te,je,!1,null,"036b263a",null),$e=Ae.exports,De=s(3422),Ne=s(1625),ze=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fa-solid:sliders-h",width:"19px"}}),e("span",{staticClass:"ml-3"},[t._v("全般")])],1),e("div",{staticClass:"settings__content"},[e("div",{staticClass:"settings__item settings__item--sync-disabled"},[e("div",{staticClass:"settings__item-heading"},[t._v("テレビのストリーミング画質")]),e("div",{staticClass:"settings__item-label"},[t._v(" テレビをライブストリーミングするときの既定の画質を設定します。"),e("br"),t._v(" ストリーミング画質はプレイヤーの設定からいつでも切り替えられます。"),e("br")]),e("div",{staticClass:"settings__item-label"},[t._v(" [1080p (60fps)] は、通常 30fps (60i) の映像を補間することで、ほかの画質よりも滑らか(ぬるぬる)な映像で再生できます。ただし、再生負荷が少し高くなります。"),e("br"),t._v(" [1080p (60fps)] で視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーでは CPU 使用率が高くなり、再生に支障が出ることがあります。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tv_streaming_quality},model:{value:t.settings.tv_streaming_quality,callback:function(e){t.$set(t.settings,"tv_streaming_quality",e)},expression:"settings.tv_streaming_quality"}})],1),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled",class:{"settings__item--disabled":!1===t.PlayerUtils.isHEVCVideoSupported()}},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_data_saver_mode"}},[t._v("テレビを通信節約モードで視聴する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_data_saver_mode"}},[t._v(" テレビをライブストリーミングするときに、通信節約モードで視聴するかを設定します。"),e("br"),t._v(" 通信節約モードでは、H.265 / HEVC という圧縮率の高いコーデックを使い、画質はほぼそのまま、通信量を通常の 2/3 程度に抑えながら視聴できます。ただし、再生負荷が高くなります。"),e("br"),t._v(" 通信節約モードで視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーではまともに再生できない可能性が高いです。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_data_saver_mode",inset:"","hide-details":"",disabled:!1===t.PlayerUtils.isHEVCVideoSupported()},model:{value:t.settings.tv_data_saver_mode,callback:function(e){t.$set(t.settings,"tv_data_saver_mode",e)},expression:"settings.tv_data_saver_mode"}})],1),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_low_latency_mode"}},[t._v("テレビを低遅延で視聴する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_low_latency_mode"}},[t._v(" テレビをライブストリーミングするときに、低遅延で視聴するかを設定します。"),e("br"),t._v(" 低遅延ストリーミングがオンのときは、放送波との遅延を最短 1.9 秒に抑えて視聴できます。"),e("br"),t._v(" また、約 3 秒以上遅延したときに少しだけ再生速度を早める (1.1x) ことで、滑らかにストリーミングの遅れを取り戻します。"),e("br"),t._v(" 宅外視聴などのネットワークが不安定になりがちな環境では、一度低遅延ストリーミングをオフにしてみると、映像のカクつきを改善できるかもしれません。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_low_latency_mode",inset:"","hide-details":""},model:{value:t.settings.tv_low_latency_mode,callback:function(e){t.$set(t.settings,"tv_low_latency_mode",e)},expression:"settings.tv_low_latency_mode"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_show_superimpose"}},[t._v("テレビをみるときに文字スーパーを表示する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_show_superimpose"}},[t._v(" テレビをライブストリーミングするときに、文字スーパーを表示するかを設定します。"),e("br"),t._v(" 文字スーパーは、緊急地震速報の赤テロップや、NHK BS のニュース速報のテロップなどで利用されています。とくに理由がなければ、オンのままにしておくことをおすすめします。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_show_superimpose",inset:"","hide-details":""},model:{value:t.settings.tv_show_superimpose,callback:function(e){t.$set(t.settings,"tv_show_superimpose",e)},expression:"settings.tv_show_superimpose"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("既定のパネルの表示状態")]),e("div",{staticClass:"settings__item-label"},[t._v(" 視聴画面を開いたときに、右側のパネルをどう表示するかを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.panel_display_state},model:{value:t.settings.panel_display_state,callback:function(e){t.$set(t.settings,"panel_display_state",e)},expression:"settings.panel_display_state"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("テレビをみるときに既定で表示されるパネルのタブ")]),e("div",{staticClass:"settings__item-label"},[t._v(" テレビの視聴画面を開いたときに、右側のパネルで最初に表示されるタブを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tv_panel_active_tab},model:{value:t.settings.tv_panel_active_tab,callback:function(e){t.$set(t.settings,"tv_panel_active_tab",e)},expression:"settings.tv_panel_active_tab"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("label",{staticClass:"settings__item-heading"},[t._v("字幕のフォント")]),e("label",{staticClass:"settings__item-label"},[t._v(" プレイヤーで字幕表示をオンにしているときの、字幕のフォントを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.caption_font},model:{value:t.settings.caption_font,callback:function(e){t.$set(t.settings,"caption_font",e)},expression:"settings.caption_font"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"always_border_caption_text"}},[t._v("字幕の文字を常に縁取って描画する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"always_border_caption_text"}},[t._v(" プレイヤーで字幕表示をオンにしているときに、字幕の文字を常に縁取って描画するかを設定します。"),e("br"),t._v(" 字幕は縁取られていた方が視認性が良く、見た目的にもきれいです。とくに理由がなければ、オンのままにしておくことをおすすめします。"),e("br"),t._v(" この設定をオフにしているときも、字幕データ側で明示的に縁取りするように指定されていれば、オンにしているとき同様に文字が縁取られて描画されます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"always_border_caption_text",inset:"","hide-details":""},model:{value:t.settings.always_border_caption_text,callback:function(e){t.$set(t.settings,"always_border_caption_text",e)},expression:"settings.always_border_caption_text"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"specify_caption_background_color"}},[t._v("字幕の背景色を指定する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"specify_caption_background_color"}},[t._v(" プレイヤーで字幕表示をオンにしているときに、字幕の背景色を明示的に指定するかを設定します。"),e("br"),t._v(" この設定をオフにしているときは、字幕データ側で指定されている背景色で描画します。とくに理由がなければ、オフのままにしておくことをおすすめします。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"specify_caption_background_color",inset:"","hide-details":""},model:{value:t.settings.specify_caption_background_color,callback:function(e){t.$set(t.settings,"specify_caption_background_color",e)},expression:"settings.specify_caption_background_color"}})],1),e("div",{staticClass:"settings__item",class:{"settings__item--disabled":!1===t.settings.specify_caption_background_color}},[e("label",{staticClass:"settings__item-heading"},[t._v("字幕の背景色")]),e("label",{staticClass:"settings__item-label"},[t._v(" プレイヤーで字幕表示をオンにしているときの、字幕の背景色を設定します。"),e("br"),t._v(" 上の [字幕の背景色を指定する] をオンにしているときのみ有効です。透明度 (アルファチャンネル) を 0 に設定すれば、字幕の背景を非表示にできます。"),e("br")]),e("div",{ref:"caption_background_color",staticClass:"settings__item-label"},[e(De.Z,{staticClass:"settings__item-form",attrs:{"hide-details":"",flat:!0,"show-alpha":!0,"show-swatches":!1,"hide-inputs":!1,width:690,"canvas-height":80,disabled:!1===t.settings.specify_caption_background_color},model:{value:t.settings.caption_background_color,callback:function(e){t.$set(t.settings,"caption_background_color",e)},expression:"settings.caption_background_color"}})],1)]),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"capture_copy_to_clipboard"}},[t._v("キャプチャをクリップボードにコピーする")]),e("label",{staticClass:"settings__item-label",attrs:{for:"capture_copy_to_clipboard"}},[t._v(" プレイヤーでキャプチャを撮ったときに、撮ったキャプチャをクリップボードにもコピーするかを設定します。"),e("br"),t._v(" クリップボードの履歴をサポートしていない OS では、この設定をオンにした状態でキャプチャを撮ると、以前のクリップボードが上書きされます。注意してください。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"capture_copy_to_clipboard",inset:"","hide-details":""},model:{value:t.settings.capture_copy_to_clipboard,callback:function(e){t.$set(t.settings,"capture_copy_to_clipboard",e)},expression:"settings.capture_copy_to_clipboard"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("キャプチャの保存先")]),e("div",{staticClass:"settings__item-label"},[e("p",[t._v(" キャプチャした画像をブラウザでダウンロードするか、KonomiTV サーバーにアップロードするかを設定します。"),e("br"),t._v(" ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方同時に行うこともできます。"),e("br")]),e("p",[t._v(" ブラウザでダウンロードすると、視聴中のデバイスのダウンロードフォルダに保存されます。"),e("br"),t._v(" 視聴中のデバイスにそのまま保存されるためシンプルですが、保存先のフォルダを変更できないこと、PC 版 Chrome では毎回ダウンロードバーが表示されてしまうことがデメリットです。"),e("br")]),e("p",[t._v(" KonomiTV サーバーにアップロードすると、環境設定で指定されたキャプチャ保存フォルダに保存されます。視聴したデバイスにかかわらず、今までに撮ったキャプチャをひとつのフォルダにまとめて保存できます。"),e("br"),t._v(" 他のデバイスでキャプチャを見るにはキャプチャ保存フォルダをネットワークに共有する必要があること、スマホ・タブレットではネットワーク上のフォルダへのアクセスがやや面倒なことがデメリットです。"),e("br")])]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.capture_save_mode},model:{value:t.settings.capture_save_mode,callback:function(e){t.$set(t.settings,"capture_save_mode",e)},expression:"settings.capture_save_mode"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("字幕が表示されているときのキャプチャの保存モード")]),e("div",{staticClass:"settings__item-label"},[t._v(" 字幕が表示されているときに、キャプチャした画像に字幕を合成するかを設定します。"),e("br"),t._v(" 映像のみのキャプチャと、字幕を合成したキャプチャを両方同時に保存することもできます。"),e("br"),t._v(" なお、字幕が表示されていない場合は、常に映像のみ (+コメント付きキャプチャではコメントを合成して) 保存されます。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.capture_caption_mode},model:{value:t.settings.capture_caption_mode,callback:function(e){t.$set(t.settings,"capture_caption_mode",e)},expression:"settings.capture_caption_mode"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("設定をエクスポート")]),e("div",{staticClass:"settings__item-label"},[t._v(" このデバイス(ブラウザ)に保存されている設定データをエクスポート(ダウンロード)できます。"),e("br"),t._v(" ダウンロードした設定データ (KonomiTV-Settings.json) は、[設定をインポート] からインポートできます。異なるサーバーの KonomiTV を同じ設定で使いたいときなどに使ってください。"),e("br")])]),e(W.Z,{staticClass:"settings__save-button mt-4",attrs:{depressed:""},on:{click:function(e){return t.exportSettings()}}},[e("Icon",{staticClass:"mr-3",attrs:{icon:"fa6-solid:download",height:"19px"}}),t._v("設定をエクスポート ")],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading error--text text--lighten-1"},[t._v("設定をインポート")]),e("div",{staticClass:"settings__item-label"},[t._v(" [設定をエクスポート] でダウンロードした設定データを、このデバイス(ブラウザ)にインポートできます。"),e("br"),t._v(" 設定をインポートすると、それまでこのデバイス(ブラウザ)に保存されていた設定が、すべてインポート先の設定データで上書きされます。元に戻すことはできません。 ")]),e(Ne.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",placeholder:"設定データ (KonomiTV-Settings.json) を選択",dense:t.is_form_dense,accept:"application/json","prepend-icon":"","prepend-inner-icon":"mdi-paperclip"},model:{value:t.import_settings_file,callback:function(e){t.import_settings_file=e},expression:"import_settings_file"}})],1),e(W.Z,{staticClass:"settings__save-button error mt-5",attrs:{depressed:""},on:{click:function(e){return t.importSettings()}}},[e("Icon",{staticClass:"mr-3",attrs:{icon:"fa6-solid:upload",height:"19px"}}),t._v("設定をインポート ")],1)],1)])},Ke=[],Be=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e(wt.Z,{staticClass:"settings-container d-flex px-5 py-5 mx-auto background",attrs:{elevation:"0",width:"100%","max-width":"1000"}},[e("div",[e(Ie.Z,{staticClass:"settings-navigation flex-shrink-0 background",attrs:{permanent:"",width:"195",height:"auto"}},[e(Ht.Z,{staticClass:"px-4"},[e(Vt.km,[e("h1",[t._v("設定")])])],1),e(Et.Z,{staticClass:"mt-2 px-0",attrs:{nav:""}},[e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/general"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 3px"},attrs:{icon:"fa-solid:sliders-h",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("全般")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/account"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("アカウント")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/jikkyo"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 2px"},attrs:{icon:"bi:chat-left-text-fill",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("ニコニコ実況")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/twitter"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 1px"},attrs:{icon:"fa-brands:twitter",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("Twitter")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/environment"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"26px"}})],1),e(Vt.km,[e(Vt.V9,[t._v("環境設定")])],1)],1)],1)],1)],1),e(wt.Z,{staticClass:"settings ml-5 px-7 py-7 background lighten-1",attrs:{width:"100%"}},[t._t("default")],2)],1)],1)],1)},Ee=[],He=n["default"].extend({name:"Settings-Base",components:{Header:lt,Navigation:pt}}),Ve=He,Le=(0,p.Z)(Ve,Be,Ee,!1,null,"03345d7e",null),Re=Le.exports,Fe=n["default"].extend({name:"Settings-General",components:{Base:Re},data(){return{PlayerUtils:N,is_form_dense:R.isSmartphoneHorizontal(),tv_streaming_quality:[{text:"1080p (60fps) (1時間あたり約3.24GB / 7.2Mbps)",value:"1080p-60fps"},{text:"1080p (1時間あたり約2.31GB / 5.1Mbps)",value:"1080p"},{text:"810p (1時間あたり約1.92GB / 4.2Mbps)",value:"810p"},{text:"720p (1時間あたり約1.33GB / 3.0Mbps)",value:"720p"},{text:"540p (1時間あたり約1.00GB / 2.2Mbps)",value:"540p"},{text:"480p (1時間あたり約0.74GB / 1.6Mbps)",value:"480p"},{text:"360p (1時間あたり約0.40GB / 0.9Mbps)",value:"360p"},{text:"240p (1時間あたり約0.23GB / 0.5Mbps)",value:"240p"}],panel_display_state:[{text:"前回の状態を復元する",value:"RestorePreviousState"},{text:"常に表示する",value:"AlwaysDisplay"},{text:"常に折りたたむ",value:"AlwaysFold"}],tv_panel_active_tab:[{text:"番組情報タブ",value:"Program"},{text:"チャンネルタブ",value:"Channel"},{text:"コメントタブ",value:"Comment"},{text:"Twitter タブ",value:"Twitter"}],caption_font:[{text:"Windows TV ゴシック",value:"Windows TV Gothic"},{text:"Windows TV 丸ゴシック",value:"Windows TV MaruGothic"},{text:"Windows TV 太丸ゴシック",value:"Windows TV FutoMaruGothic"},{text:"ヒラギノTV丸ゴ",value:"Hiragino TV Sans Rd S"},{text:"新丸ゴ ARIB",value:"TT-ShinMGo-regular"},{text:"Rounded M+ 1m for ARIB",value:"Rounded M+ 1m for ARIB"},{text:"Noto Sans JP",value:"Noto Sans JP Caption"},{text:"デフォルトのフォント",value:"sans-serif"}],capture_save_mode:[{text:"ブラウザでダウンロード",value:"Browser"},{text:"KonomiTV サーバーにアップロード",value:"UploadServer"},{text:"ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方行う",value:"Both"}],capture_caption_mode:[{text:"映像のみのキャプチャを保存する",value:"VideoOnly"},{text:"字幕を合成したキャプチャを保存する",value:"CompositingCaption"},{text:"映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する",value:"Both"}],import_settings_file:null,settings:(()=>{const t={},e=["tv_streaming_quality","tv_data_saver_mode","tv_low_latency_mode","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_copy_to_clipboard","capture_save_mode","capture_caption_mode"];for(const s of e)t[s]=R.getSettingsItem(s);return t})()}},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))R.setSettingsItem(t,e)}}},methods:{exportSettings(){const t=localStorage.getItem("KonomiTV-Settings")||JSON.stringify(R.default_settings),e=new Blob([t],{type:"application/json"});R.downloadBlobData(e,"KonomiTV-Settings.json"),this.$message.success("設定をエクスポートしました。")},async importSettings(){if(null!==this.import_settings_file)try{const t=JSON.parse(await this.import_settings_file.text());localStorage.setItem("KonomiTV-Settings",JSON.stringify(t)),await R.syncClientSettingsToServer(),this.$message.success("設定をインポートしました。"),window.setTimeout((()=>this.$router.go(0)),300)}catch(t){return void this.$message.error("設定データが不正なため、インポートできませんでした。")}else this.$message.error("インポートする設定データを選択してください!")}}}),Me=Fe,Ue=(0,p.Z)(Me,ze,Ke,!1,null,null,null),Ge=Ue.exports,qe=s(5125),We=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"25px"}}),e("span",{staticClass:"ml-2"},[t._v("アカウント")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[null===t.user?e("div",{staticClass:"account"},[e("div",{staticClass:"account-wrapper"},[e("img",{staticClass:"account__icon",attrs:{src:"/assets/images/account-icon-default.png"}}),e("div",{staticClass:"account__info"},[e("div",{staticClass:"account__info-name"},[e("span",{staticClass:"account__info-name-text"},[t._v("ログインしていません")])]),e("span",{staticClass:"account__info-id"},[t._v("Not logged in")])])]),e(W.Z,{staticClass:"account__login ml-auto",attrs:{color:"secondary",width:"140",height:"56",depressed:"",to:"/login/"}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-in"}}),t._v("ログイン ")],1)],1):t._e(),null!==t.user?e("div",{staticClass:"account"},[e("div",{staticClass:"account-wrapper"},[e("img",{staticClass:"account__icon",attrs:{src:t.user_icon_blob}}),e("div",{staticClass:"account__info"},[e("div",{staticClass:"account__info-name"},[e("span",{staticClass:"account__info-name-text"},[t._v(t._s(t.user.name))]),t.user.is_admin?e("span",{staticClass:"account__info-admin"},[t._v("管理者")]):t._e()]),e("span",{staticClass:"account__info-id"},[t._v("User ID: "+t._s(t.user.id))])])]),e(W.Z,{staticClass:"account__login ml-auto",attrs:{color:"secondary",width:"140",height:"56",depressed:""},on:{click:function(e){return t.logout()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-out"}}),t._v("ログアウト ")],1)],1):t._e(),!1===t.is_logged_in?e("div",{staticClass:"account-register"},[e("div",{staticClass:"account-register__heading"},[t._v(" KonomiTV アカウントにログインすると、"),e("br"),t._v("より便利な機能が使えます! ")]),e("div",{staticClass:"account-register__feature"},[e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"bi:chat-left-text-fill"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("ニコニコ実況にコメントする")]),e("span",{staticClass:"account-feature__info-text"},[t._v("テレビを見ながらニコニコ実況にコメントできます。別途、ニコニコアカウントとの連携が必要です。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fa-brands:twitter"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("Twitter 連携機能")]),e("span",{staticClass:"account-feature__info-text"},[t._v("テレビを見ながら Twitter にツイートしたり、検索したツイートをリアルタイムで表示できます。別途、Twitter アカウントとの連携が必要です。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fluent:arrow-sync-20-filled"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("設定をデバイス間で同期")]),e("span",{staticClass:"account-feature__info-text"},[t._v("ピン留めしたチャンネルなど、ブラウザに保存されている各種設定をブラウザやデバイスをまたいで同期できます。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fa-solid:sliders-h"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("環境設定をブラウザから変更")]),e("span",{staticClass:"account-feature__info-text"},[t._v("管理者権限があれば、環境設定をブラウザから変更できます。一番最初に作成されたアカウントには、自動で管理者権限が付与されます。")])])],1)]),e("div",{staticClass:"account-register__description"},[t._v(" KonomiTV アカウントの作成に必要なものはユーザー名とパスワードだけです。"),e("br"),t._v(" アカウントはローカルにインストールした KonomiTV サーバーごとに保存されます。"),e("br"),t._v(" 外部のサービスには保存されませんので、ご安心ください。"),e("br")]),e(W.Z,{staticClass:"account-register__button",attrs:{color:"secondary",width:"100%","max-width":"250",height:"50",depressed:"",to:"/register/"}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-add-20-filled",height:"24"}}),t._v("アカウントを作成 ")],1)],1):t._e(),!0===t.is_logged_in?e("div",[e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"sync_settings"}},[t._v("設定をデバイス間で同期する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"sync_settings"}},[t._v(" KonomiTV の設定を、同じアカウントにログインしているデバイス同士で同期するかを設定します。"),e("br"),t._v(" 同期を有効にすると、同期が有効なデバイスすべてで同じ設定が使えます。ピン留めしたチャンネルやハッシュタグリストなども同期されます。"),e("br"),t._v(" ストリーミング画質やコメントの遅延時間など、デバイスごとに最適な設定が異なるものは、同期を有効にしたあとも引き続きこのデバイス(ブラウザ)のみに反映されます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"sync_settings",inset:"","hide-details":""},model:{value:t.sync_settings,callback:function(e){t.sync_settings=e},expression:"sync_settings"}})],1),e(Ct.Z,{attrs:{"max-width":"530"},model:{value:t.sync_settings_dialog,callback:function(e){t.sync_settings_dialog=e},expression:"sync_settings_dialog"}},[e(wt.Z,[e(yt.EB,{staticClass:"justify-center"},[t._v("設定データの競合")]),e(yt.ZB,[t._v(" このデバイスの設定と、サーバーに保存されている設定が競合しています。"),e("br"),t._v(" 一度上書きすると、元に戻すことはできません。慎重に選択してください。"),e("br")]),e("div",{staticClass:"d-flex flex-column px-4 pb-4"},[e(W.Z,{staticClass:"settings__save-button error--text text--lighten-1",attrs:{depressed:""},on:{click:function(e){return t.overrideServerSettingsFromClient()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:document-arrow-up-16-filled",height:"22px"}}),t._v(" サーバーに保存されている設定を、このデバイスの設定で上書きする ")],1),e(W.Z,{staticClass:"settings__save-button error--text text--lighten-1 mt-3",attrs:{depressed:""},on:{click:function(e){return t.overrideClientSettingsFromServer()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:document-arrow-down-16-filled",height:"22px"}}),t._v(" このデバイスの設定を、サーバーに保存されている設定で上書きする ")],1),e(W.Z,{staticClass:"settings__save-button mt-3",attrs:{depressed:""},on:{click:function(e){t.sync_settings_dialog=!1}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:dismiss-16-filled",height:"22px"}}),t._v(" キャンセル ")],1)],1)],1)],1),e(qe.Z,{ref:"settings_username",staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("ユーザー名")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントのユーザー名を設定します。アルファベットだけでなく日本語や記号も使えます。"),e("br"),t._v(" 同じ KonomiTV サーバー上の他のアカウントと同じユーザー名には変更できません。"),e("br")]),e(Ut.Z,{staticClass:"settings__item-form",attrs:{outlined:"",placeholder:"ユーザー名",dense:t.is_form_dense,rules:[t.settings_username_validation]},model:{value:t.settings_username,callback:function(e){t.settings_username=e},expression:"settings_username"}})],1),e(W.Z,{staticClass:"settings__save-button",attrs:{depressed:""},on:{click:function(e){return t.updateAccountInfo("username")}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("ユーザー名を更新 ")],1),e(qe.Z,{staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("アイコン画像")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントのアイコン画像を設定します。"),e("br"),t._v(" アップロードされた画像は自動的に 400×400 の正方形にリサイズされます。"),e("br")]),e(Ne.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",placeholder:"アイコン画像を選択",dense:t.is_form_dense,accept:"image/jpeg, image/png","prepend-icon":"","prepend-inner-icon":"mdi-paperclip"},model:{value:t.settings_icon,callback:function(e){t.settings_icon=e},expression:"settings_icon"}})],1),e(W.Z,{staticClass:"settings__save-button mt-5",attrs:{depressed:""},on:{click:function(e){return t.updateAccountIcon()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("アイコン画像を更新 ")],1),e(qe.Z,{ref:"settings_password",staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("新しいパスワード")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントの新しいパスワードを設定します。"),e("br")]),e(Ut.Z,{staticClass:"settings__item-form",attrs:{outlined:"",placeholder:"新しいパスワード",dense:t.is_form_dense,type:t.settings_password_showing?"text":"password","append-icon":t.settings_password_showing?"mdi-eye":"mdi-eye-off",rules:[t.settings_password_validation]},on:{"click:append":function(e){t.settings_password_showing=!t.settings_password_showing}},model:{value:t.settings_password,callback:function(e){t.settings_password=e},expression:"settings_password"}})],1),e(W.Z,{staticClass:"settings__save-button",attrs:{depressed:""},on:{click:function(e){return t.updateAccountInfo("password")}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("パスワードを更新 ")],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item mt-6"},[e("div",{staticClass:"settings__item-heading error--text text--lighten-1"},[t._v("アカウントを削除")]),e("div",{staticClass:"settings__item-label"},[t._v(" 現在ログインしている KonomiTV アカウントを削除します。"),e("br"),t._v(" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。"),e("br")])]),e(Ct.Z,{attrs:{"max-width":"385"},scopedSlots:t._u([{key:"activator",fn:function({on:s,attrs:i}){return[e(W.Z,t._g(t._b({staticClass:"settings__save-button error mt-5",attrs:{depressed:""}},"v-btn",i,!1),s),[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:delete-16-filled",height:"24px"}}),t._v("アカウントを削除 ")],1)]}}],null,!1,974850237),model:{value:t.account_delete_confirm_dialog,callback:function(e){t.account_delete_confirm_dialog=e},expression:"account_delete_confirm_dialog"}},[e(wt.Z,[e(yt.EB,{staticClass:"justify-center"},[t._v("本当にアカウントを削除しますか?")]),e(yt.ZB,[t._v(" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。"),e("br"),t._v(" 本当にアカウントを削除しますか? ")]),e(yt.h7,[e(Q.Z),e(W.Z,{attrs:{color:"text",text:""},on:{click:function(e){t.account_delete_confirm_dialog=!1}}},[t._v("キャンセル")]),e(W.Z,{attrs:{color:"error",text:""},on:{click:function(e){return t.deleteAccount()}}},[t._v("削除")])],1)],1)],1)],1):t._e()])])},Ye=[],Xe=n["default"].extend({name:"Settings-Account",components:{Base:Re},data(){return{Utils:R,is_form_dense:R.isSmartphoneHorizontal(),is_loading:!0,is_logged_in:null!==R.getAccessToken(),user:null,user_icon_blob:"",settings_username:null,settings_username_validation:t=>""===t||null===t?"ユーザー名を入力してください。":!1!==/^.{2,}$/.test(t)||"ユーザー名は2文字以上で入力してください。",settings_password:null,settings_password_showing:!0,settings_password_validation:t=>""===t||null===t?"パスワードを入力してください。":!1!==/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(t)||"パスワードは4文字以上の半角英数記号を入力してください。",settings_icon:null,account_delete_confirm_dialog:null,sync_settings:R.getSettingsItem("sync_settings"),sync_settings_dialog:!1}},async created(){await this.syncAccountInfo(),this.is_loading=!1},watch:{async sync_settings(){if(!0===this.sync_settings&&!1===this.sync_settings_dialog)try{null===localStorage.getItem("KonomiTV-Settings")&&localStorage.setItem("KonomiTV-Settings",JSON.stringify(R.default_settings));const t=JSON.parse(localStorage.getItem("KonomiTV-Settings")),e={};for(const a of R.sync_settings_keys)e[a]=a in t?t[a]:R.default_settings[a];const s=JSON.stringify(e),i=JSON.stringify((await n["default"].axios.get("/settings/client")).data);s!==i?(this.sync_settings_dialog=!0,this.sync_settings=!1):R.setSettingsItem("sync_settings",!0)}catch(t){this.$message.error(`サーバーから設定データを取得できませんでした。(HTTP Error ${t.response.status})`)}else!1===this.sync_settings&&!1===this.sync_settings_dialog&&R.setSettingsItem("sync_settings",!1)}},methods:{async overrideServerSettingsFromClient(){await R.syncClientSettingsToServer(!0),this.sync_settings=!0,R.setSettingsItem("sync_settings",!0),this.sync_settings_dialog=!1},async overrideClientSettingsFromServer(){await R.syncServerSettingsToClient(!0),this.sync_settings=!0,R.setSettingsItem("sync_settings",!0),this.sync_settings_dialog=!1},async syncAccountInfo(){try{const t=await n["default"].axios.get("/users/me");this.user=t.data,this.settings_username=this.user.name,await this.syncAccountIcon()}catch(t){F.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(console.log("Not logged in."),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",R.deleteAccessToken())}},async syncAccountIcon(){const t=await n["default"].axios.get("/users/me/icon",{responseType:"arraybuffer"});this.user_icon_blob=URL.createObjectURL(new Blob([t.data],{type:"image/png"}))},async updateAccountInfo(t){if("username"===t){if(!1===this.$refs.settings_username.validate())return}else if(!1===this.$refs.settings_password.validate())return;try{"username"===t?(await n["default"].axios.put("/users/me",{username:this.settings_username}),this.$message.show("ユーザー名を更新しました。")):(await n["default"].axios.put("/users/me",{password:this.settings_password}),this.$message.show("パスワードを更新しました。")),await this.syncAccountInfo()}catch(e){if(F.ZP.isAxiosError(e)&&e.response&&422===e.response.status)switch(e.response.data.detail){case"Specified username is duplicated":this.$message.error("ユーザー名が重複しています。");break;case"Specified username is not accepted due to system limitations":this.$message.error("ユーザー名に token と me は使えません。");break;default:this.$message.error(`アカウント情報を更新できませんでした。(HTTP Error ${e.response.status})`);break}}},async updateAccountIcon(){if(null===this.settings_icon)return void this.$message.error("アップロードする画像を選択してください!");const t=new FormData;t.append("image",this.settings_icon);try{await n["default"].axios.put("/users/me/icon",t,{headers:{"Content-Type":"multipart/form-data"}}),await this.syncAccountIcon()}catch(e){if(F.ZP.isAxiosError(e)&&e.response&&422===e.response.status)switch(e.response.data.detail){case"Please upload JPEG or PNG image":this.$message.error("JPEG または PNG 画像をアップロードしてください。");break;default:this.$message.error(`アイコン画像を更新できませんでした。(HTTP Error ${e.response.status})`);break}}},async deleteAccount(){this.account_delete_confirm_dialog=!1,await n["default"].axios["delete"]("/users/me"),R.setSettingsItem("sync_settings",!1),R.deleteAccessToken(),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",this.$message.show("アカウントを削除しました。")},logout(){R.setSettingsItem("sync_settings",!1),R.deleteAccessToken(),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",this.$message.success("ログアウトしました。")}}}),Je=Xe,Qe=(0,p.Z)(Je,We,Ye,!1,null,"12036e32",null),ts=Qe.exports,es=s(7414),ss=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"bi:chat-left-text-fill",width:"19px"}}),e("span",{staticClass:"ml-3"},[t._v("ニコニコ実況")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[null===t.user.niconico_user_id?e("div",{staticClass:"niconico-account niconico-account--anonymous"},[e("div",{staticClass:"niconico-account-wrapper"},[e("Icon",{staticClass:"flex-shrink-0",attrs:{icon:"bi:chat-left-text-fill",width:"45px"}}),e("div",{staticClass:"niconico-account__info ml-4"},[e("div",{staticClass:"niconico-account__info-name"},[e("span",{staticClass:"niconico-account__info-name-text"},[t._v("ニコニコアカウントと連携していません")])]),e("span",{staticClass:"niconico-account__info-description"},[t._v(" ニコニコアカウントと連携すると、テレビを見ながらニコニコ実況にコメントできるようになります。 ")])])],1),e(W.Z,{staticClass:"niconico-account__login ml-auto",attrs:{color:"secondary",width:"130",height:"56",depressed:""},on:{click:function(e){return t.loginNiconicoAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-connected-20-filled",height:"26"}}),t._v("連携する ")],1)],1):t._e(),null!==t.user.niconico_user_id?e("div",{staticClass:"niconico-account"},[e("div",{staticClass:"niconico-account-wrapper"},[e("img",{staticClass:"niconico-account__icon",attrs:{src:this.niconico_user_icon_url}}),e("div",{staticClass:"niconico-account__info"},[e("div",{staticClass:"niconico-account__info-name"},[e("span",{staticClass:"niconico-account__info-name-text"},[t._v(t._s(t.user.niconico_user_name)+" と連携しています")])]),e("span",{staticClass:"niconico-account__info-description"},[e("span",{staticClass:"mr-2"},[t._v("Niconico User ID:")]),e("a",{staticClass:"mr-2",attrs:{href:`https://www.nicovideo.jp/user/${t.user.niconico_user_id}`,target:"_blank"}},[t._v(t._s(t.user.niconico_user_id))]),1==t.user.niconico_user_premium?e("span",{staticClass:"secondary--text"},[t._v("(Premium)")]):t._e()])])]),e(W.Z,{staticClass:"niconico-account__login ml-auto",attrs:{color:"secondary",width:"130",height:"56",depressed:""},on:{click:function(e){return t.logoutNiconicoAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-disconnected-20-filled",height:"26"}}),t._v("連携解除 ")],1)],1):t._e(),e("div",{staticClass:"settings__item mt-7"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントのミュート設定")]),e("div",{staticClass:"settings__item-label"},[t._v(" 表示したくないコメントを、画面やコメントリストに表示しないようにミュートできます。"),e("br")])]),e(W.Z,{staticClass:"settings__save-button mt-4",attrs:{depressed:""},on:{click:function(e){t.comment_mute_settings_modal=!t.comment_mute_settings_modal}}},[e("Icon",{attrs:{icon:"heroicons-solid:filter",height:"19px"}}),e("span",{staticClass:"ml-1"},[t._v("コメントのミュート設定を開く")])],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの速さ")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーに流れるコメントの速さを設定します。"),e("br"),t._v(" たとえば 1.2 に設定すると、コメントが 1.2 倍速く流れます。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",step:.1,min:.5,max:2},model:{value:t.settings.comment_speed_rate,callback:function(e){t.$set(t.settings,"comment_speed_rate",e)},expression:"settings.comment_speed_rate"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの文字サイズ")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーに流れるコメントの文字サイズの基準値を設定します。"),e("br"),t._v(" 実際の文字サイズは画面の大きさに合わせて調整されます。既定の文字サイズは 34px です。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",min:20,max:60},model:{value:t.settings.comment_font_size,callback:function(e){t.$set(t.settings,"comment_font_size",e)},expression:"settings.comment_font_size"}})],1),e("div",{staticClass:"settings__item settings__item--sync-disabled"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの遅延時間")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーやコメントリストに表示されるコメントを何秒遅らせて反映するかを設定します。"),e("br"),t._v(" 通常は 1.75 秒程度で大丈夫です。ネットワークが遅いなどでタイムラグが大きいときだけ、映像の遅延に合わせて調整してください。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",step:.25,min:0,max:10},model:{value:t.settings.comment_delay_time,callback:function(e){t.$set(t.settings,"comment_delay_time",e)},expression:"settings.comment_delay_time"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"close_comment_form_after_sending"}},[t._v("コメント送信後にコメント入力フォームを閉じる")]),e("label",{staticClass:"settings__item-label",attrs:{for:"close_comment_form_after_sending"}},[t._v(" コメントを送信したあとに、コメント入力フォームを自動的に閉じるかを設定します。"),e("br"),t._v(" 基本的にはオンのままにしておくことをおすすめします。コメント入力フォームが表示されたままだと、大部分のショートカットキーが文字入力と競合して使えないためです。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"close_comment_form_after_sending",inset:"","hide-details":""},model:{value:t.settings.close_comment_form_after_sending,callback:function(e){t.$set(t.settings,"close_comment_form_after_sending",e)},expression:"settings.close_comment_form_after_sending"}})],1)],1),e("CommentMuteSettings",{model:{value:t.comment_mute_settings_modal,callback:function(e){t.comment_mute_settings_modal=e},expression:"comment_mute_settings_modal"}})],1)},is=[],as=n["default"].extend({name:"Settings-Jikkyo",components:{Base:Re,CommentMuteSettings:Jt},data(){return{Utils:R,comment_mute_settings_modal:!1,is_loading:!0,is_logged_in:null!==R.getAccessToken(),user:null,niconico_user_icon_url:"",settings:(()=>{const t={},e=["comment_speed_rate","comment_font_size","comment_delay_time","close_comment_form_after_sending"];for(const s of e)t[s]=R.getSettingsItem(s);return t})()}},async created(){this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&await this.syncAccountInfo(),this.is_loading=!1},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))R.setSettingsItem(t,e)}}},methods:{async syncAccountInfo(){try{const t=await n["default"].axios.get("/users/me");if(this.user=t.data,null!==this.user.niconico_user_id){const t=this.user.niconico_user_id.toString().slice(0,4);this.niconico_user_icon_url=`https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/${t}/${this.user.niconico_user_id}.jpg`}}catch(t){F.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},async loginNiconicoAccount(){if(!1===this.is_logged_in)return void this.$message.warning("連携をはじめるには、KonomiTV アカウントにログインしてください。");const t=(await n["default"].axios.get("/niconico/auth")).data.authorization_url,e=window.open(t,"KonomiTV-OAuthPopup",R.getWindowFeatures()),s=async t=>{if(e.closed)return;if("object"!==R["typeof"](t.data))return;if("KonomiTV-OAuthPopup"in t.data===!1)return;e&&e.close(),window.removeEventListener("message",s);const i=t.data["KonomiTV-OAuthPopup"]["status"],a=t.data["KonomiTV-OAuthPopup"]["detail"];if(console.log(`NiconicoAuthCallbackAPI: Status: ${i} / Detail: ${a}`),200===i)await this.syncAccountInfo(),this.$message.success("ニコニコアカウントと連携しました。");else if(a.startsWith("Authorization was denied (access_denied)"))this.$message.error("ニコニコアカウントとの連携がキャンセルされました。");else if(a.startsWith("Failed to get access token (HTTP Error ")){const t=a.replace("Failed to get access token ","");this.$message.error(`アクセストークンの取得に失敗しました。${t}`)}else if(a.startsWith("Failed to get access token (Connection Timeout)"))this.$message.error("アクセストークンの取得に失敗しました。ニコニコで障害が発生している可能性があります。");else if(a.startsWith("Failed to get user information (HTTP Error ")){const t=a.replace("Failed to get user information ","");this.$message.error(`ニコニコアカウントのユーザー情報の取得に失敗しました。${t}`)}else a.startsWith("Failed to get user information (Connection Timeout)")?this.$message.error("ニコニコアカウントのユーザー情報の取得に失敗しました。ニコニコで障害が発生している可能性があります。"):this.$message.error(`ニコニコアカウントとの連携に失敗しました。(${a})`)};window.addEventListener("message",s)},async logoutNiconicoAccount(){await n["default"].axios["delete"]("/niconico/logout"),await this.syncAccountInfo(),this.$message.success("ニコニコアカウントとの連携を解除しました。")}}}),ns=as,os=(0,p.Z)(ns,ss,is,!1,null,"786083d5",null),rs=os.exports,ls=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fa-brands:twitter",width:"22px"}}),e("span",{staticClass:"ml-3"},[t._v("Twitter")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[e("div",{staticClass:"twitter-accounts"},[t.user.twitter_accounts.length>0?e("div",{staticClass:"twitter-accounts__heading"},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-board-20-filled",height:"30"}}),t._v("連携中のアカウント ")],1):t._e(),0===t.user.twitter_accounts.length?e("div",{staticClass:"twitter-accounts__guide"},[e("Icon",{staticClass:"flex-shrink-0",attrs:{icon:"fa-brands:twitter",width:"45px"}}),e("div",{staticClass:"ml-4"},[e("div",{staticClass:"font-weight-bold text-h6"},[t._v("Twitter アカウントと連携していません")]),e("div",{staticClass:"text--text text--darken-1 text-subtitle-2 mt-1"},[t._v(" Twitter アカウントと連携すると、テレビを見ながら Twitter にツイートしたり、ほかの実況ツイートをリアルタイムで表示できるようになります。 ")])])],1):t._e(),t._l(t.user.twitter_accounts,(function(s){return e("div",{key:s.id,staticClass:"twitter-account"},[e("img",{staticClass:"twitter-account__icon",attrs:{src:s.icon_url}}),e("div",{staticClass:"twitter-account__info"},[e("div",{staticClass:"twitter-account__info-name"},[e("span",{staticClass:"twitter-account__info-name-text"},[t._v(t._s(s.name))])]),e("span",{staticClass:"twitter-account__info-screen-name"},[t._v("@"+t._s(s.screen_name))])]),e(W.Z,{staticClass:"twitter-account__logout ml-auto",attrs:{width:"124",height:"52",depressed:""},on:{click:function(e){return t.logoutTwitterAccount(s.screen_name)}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-disconnected-20-filled",height:"24"}}),t._v("連携解除 ")],1)],1)})),e(W.Z,{staticClass:"twitter-account__login",attrs:{color:"secondary","max-width":"250",height:"50",depressed:""},on:{click:function(e){return t.loginTwitterAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-connected-20-filled",height:"24"}}),t._v("連携するアカウントを追加 ")],1)],2),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"fold_panel_after_sending_tweet"}},[t._v("ツイート送信後にパネルを閉じる")]),e("label",{staticClass:"settings__item-label",attrs:{for:"fold_panel_after_sending_tweet"}},[t._v(" ツイートを送信した後に、表示中のパネルを閉じる(折りたたむ)かを設定します。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"fold_panel_after_sending_tweet",inset:"","hide-details":""},model:{value:t.settings.fold_panel_after_sending_tweet,callback:function(e){t.$set(t.settings,"fold_panel_after_sending_tweet",e)},expression:"settings.fold_panel_after_sending_tweet"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"reset_hashtag_when_program_switches"}},[t._v("番組が切り替わったときにハッシュタグフォームをリセットする")]),e("label",{staticClass:"settings__item-label",attrs:{for:"reset_hashtag_when_program_switches"}},[t._v(" チャンネルを切り替えたときや、視聴中の番組が終了し次の番組の放送が開始されたときに、ハッシュタグフォームをリセットするかを設定します。"),e("br"),t._v(" この設定をオンにしておけば、「誤って前番組のハッシュタグをつけたまま次番組の実況ツイートをしてしまう」といったミスを回避できます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"reset_hashtag_when_program_switches",inset:"","hide-details":""},model:{value:t.settings.reset_hashtag_when_program_switches,callback:function(e){t.$set(t.settings,"reset_hashtag_when_program_switches",e)},expression:"settings.reset_hashtag_when_program_switches"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"auto_add_watching_channel_hashtag"}},[t._v("視聴中のチャンネルに対応する局タグを自動的に追加する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"auto_add_watching_channel_hashtag"}},[t._v(" ハッシュタグフォームに、視聴中のチャンネルに対応する局タグ (#nhk, #tokyomx など) を自動的に追加するかを設定します。"),e("br"),t._v(" 現時点で、局タグは三大首都圏の地上波・BS の一部チャンネル・AT-X にのみ対応しています。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"auto_add_watching_channel_hashtag",inset:"","hide-details":""},model:{value:t.settings.auto_add_watching_channel_hashtag,callback:function(e){t.$set(t.settings,"auto_add_watching_channel_hashtag",e)},expression:"settings.auto_add_watching_channel_hashtag"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("既定で表示される Twitter タブ内のタブ")]),e("div",{staticClass:"settings__item-label"},[t._v(" 視聴画面を開いたときに、パネルの Twitter タブの中で最初に表示されるタブを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.twitter_active_tab},model:{value:t.settings.twitter_active_tab,callback:function(e){t.$set(t.settings,"twitter_active_tab",e)},expression:"settings.twitter_active_tab"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("ツイートにつけるハッシュタグの位置")]),e("div",{staticClass:"settings__item-label"},[t._v(" ツイート本文から見て、ハッシュタグをどの位置につけてツイートするかを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tweet_hashtag_position},model:{value:t.settings.tweet_hashtag_position,callback:function(e){t.$set(t.settings,"tweet_hashtag_position",e)},expression:"settings.tweet_hashtag_position"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("ツイートするキャプチャに番組タイトルの透かしを描画する")]),e("div",{staticClass:"settings__item-label"},[t._v(" ツイートするキャプチャに、視聴中の番組タイトルの透かしを描画するかを設定します。"),e("br")]),e(Ft.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tweet_capture_watermark_position},model:{value:t.settings.tweet_capture_watermark_position,callback:function(e){t.$set(t.settings,"tweet_capture_watermark_position",e)},expression:"settings.tweet_capture_watermark_position"}})],1)])])},cs=[],_s=n["default"].extend({name:"Settings-Twitter",components:{Base:Re},data(){return{is_form_dense:R.isSmartphoneHorizontal(),twitter_active_tab:[{text:"ツイート検索タブ",value:"Search"},{text:"タイムラインタブ",value:"Timeline"},{text:"キャプチャタブ",value:"Capture"}],tweet_hashtag_position:[{text:"ツイート本文の前に追加する",value:"Prepend"},{text:"ツイート本文の後に追加する",value:"Append"},{text:"ツイート本文の前に追加してから改行する",value:"PrependWithLineBreak"},{text:"ツイート本文の後に改行してから追加する",value:"AppendWithLineBreak"}],tweet_capture_watermark_position:[{text:"透かしを描画しない",value:"None"},{text:"透かしをキャプチャの左上に描画する",value:"TopLeft"},{text:"透かしをキャプチャの右上に描画する",value:"TopRight"},{text:"透かしをキャプチャの左下に描画する",value:"BottomLeft"},{text:"透かしをキャプチャの右下に描画する",value:"BottomRight"}],is_loading:!0,is_logged_in:null!==R.getAccessToken(),user:null,settings:(()=>{const t={},e=["fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position"];for(const s of e)t[s]=R.getSettingsItem(s);return t})()}},async created(){this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&await this.syncAccountInfo(),this.is_loading=!1},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))R.setSettingsItem(t,e)}}},methods:{async syncAccountInfo(){try{this.user=(await n["default"].axios.get("/users/me")).data}catch(t){F.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},async loginTwitterAccount(){if(!1===this.is_logged_in)return void this.$message.warning("連携をはじめるには、KonomiTV アカウントにログインしてください。");const t=(await n["default"].axios.get("/twitter/auth")).data.authorization_url,e=window.open(t,"KonomiTV-OAuthPopup",R.getWindowFeatures()),s=async t=>{if(e.closed)return;if("object"!==R["typeof"](t.data))return;if("KonomiTV-OAuthPopup"in t.data===!1)return;e&&e.close(),window.removeEventListener("message",s);const i=t.data["KonomiTV-OAuthPopup"]["status"],a=t.data["KonomiTV-OAuthPopup"]["detail"];if(console.log(`TwitterAuthCallbackAPI: Status: ${i} / Detail: ${a}`),200!==i)return void(a.startsWith("Authorization was denied by user")?this.$message.error("Twitter アカウントとの連携がキャンセルされました。"):a.startsWith("Failed to get access token")?this.$message.error("アクセストークンの取得に失敗しました。"):a.startsWith("Failed to get user information")?this.$message.error("Twitter アカウントのユーザー情報の取得に失敗しました。"):this.$message.error(`Twitter アカウントとの連携に失敗しました。(${a})`));await this.syncAccountInfo();const n=[...this.user.twitter_accounts].sort(((t,e)=>t.updated_ate.updated_at?-1:0))[0];this.$message.success(`Twitter @${n.screen_name} と連携しました。`)};window.addEventListener("message",s)},async logoutTwitterAccount(t){await n["default"].axios["delete"](`/twitter/accounts/${t}`),await this.syncAccountInfo(),this.$message.success(`Twitter @${t} との連携を解除しました。`)}}}),ds=_s,ms=(0,p.Z)(ds,ls,cs,!1,null,"1970b264",null),us=ms.exports,ps=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"22px"}}),e("span",{staticClass:"ml-3"},[t._v("環境設定")])],1),e("div",{staticClass:"settings__content"},[e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("鋭意開発中…")])])])])},hs=[],gs=n["default"].extend({name:"Settings-Environment",components:{Base:Re},data(){return{settings:(()=>{const t={},e=[];for(const s of e)t[s]=R.getSettingsItem(s);return t})()}},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))R.setSettingsItem(t,e)}}}}),vs=gs,fs=(0,p.Z)(vs,ps,hs,!1,null,null,null),ws=fs.exports,ys=s(5495),bs=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"login-container-wrapper d-flex align-center w-100 mb-13"},[e(wt.Z,{staticClass:"login-container px-10 pt-8 pb-11 mx-auto background lighten-1",attrs:{elevation:"10",width:"100%","max-width":"450"}},[e(yt.EB,{staticClass:"login__logo justify-center pb-7"},[e(ys.Z,{attrs:{"max-width":"250",src:"/assets/images/logo.svg"}})],1),e(Oe.Z),e(qe.Z,{ref:"login",on:{submit:function(t){t.preventDefault()}}},[e(Ut.Z,{staticClass:"mt-12",attrs:{outlined:"",placeholder:"ユーザー名","hide-details":"",autofocus:"",dense:t.is_form_dense},model:{value:t.username,callback:function(e){t.username=e},expression:"username"}}),e(Ut.Z,{staticClass:"mt-8",attrs:{outlined:"",placeholder:"パスワード","hide-details":"",dense:t.is_form_dense,type:t.password_showing?"text":"password","append-icon":t.password_showing?"mdi-eye":"mdi-eye-off"},on:{"click:append":function(e){t.password_showing=!t.password_showing}},model:{value:t.password,callback:function(e){t.password=e},expression:"password"}}),e(W.Z,{staticClass:"login-button mt-5",attrs:{color:"secondary",depressed:"",width:"100%",height:"56"},on:{click:function(e){return t.login()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-in"}}),t._v("ログイン ")],1)],1)],1)],1)],1)],1)},Cs=[],ks=n["default"].extend({name:"Login",components:{Header:lt,Navigation:pt},data(){return{is_form_dense:R.isSmartphoneHorizontal(),username:"",password:"",password_showing:!1}},methods:{async login(){if(""!==this.username&&""!==this.password)try{const t=await n["default"].axios.post("/users/token",new URLSearchParams({username:this.username,password:this.password}));console.log("Login successful."),console.log(t.data),R.saveAccessToken(t.data.access_token),this.$message.success("ログインしました。"),await this.$router.push({path:"/settings/account"})}catch(t){if(F.ZP.isAxiosError(t)&&t.response&&401===t.response.status)switch(console.log("Failed to login."),console.log(t.response.data),t.response.data.detail){case"Incorrect username":this.$message.error("ログインできませんでした。そのユーザー名のアカウントは存在しません。");break;case"Incorrect password":this.$message.error("ログインできませんでした。パスワードを間違えていませんか?");break;default:this.$message.error(`ログインできませんでした。(HTTP Error ${t.response.status})`);break}}}}}),xs=ks,Os=(0,p.Z)(xs,bs,Cs,!1,null,"0c2bb32a",null),Ss=Os.exports,Is=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"register-container-wrapper d-flex align-center w-100 mb-13"},[e(wt.Z,{staticClass:"register-container px-10 pt-8 pb-11 mx-auto background lighten-1",attrs:{elevation:"10",width:"100%","max-width":"450"}},[e(yt.EB,{staticClass:"register__logo flex-column justify-center"},[e(ys.Z,{attrs:{"max-width":"250",src:"/assets/images/logo.svg"}}),e("h4",{staticClass:"mt-10"},[t._v("アカウントを作成")])],1),e(Oe.Z),e(qe.Z,{ref:"register",on:{submit:function(t){t.preventDefault()}}},[e(Ut.Z,{staticClass:"mt-10",attrs:{outlined:"",placeholder:"ユーザー名",autofocus:"",dense:t.is_form_dense,rules:[t.username_validation]},model:{value:t.username,callback:function(e){t.username=e},expression:"username"}}),e(Ut.Z,{staticClass:"mt-2",attrs:{outlined:"",placeholder:"パスワード",dense:t.is_form_dense,type:t.password_showing?"text":"password","append-icon":t.password_showing?"mdi-eye":"mdi-eye-off",rules:[t.password_validation]},on:{"click:append":function(e){t.password_showing=!t.password_showing}},model:{value:t.password,callback:function(e){t.password=e},expression:"password"}}),e(W.Z,{staticClass:"register-button mt-5",attrs:{color:"secondary",depressed:"",width:"100%",height:"56"},on:{click:function(e){return t.register()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-add-20-filled",height:"24"}}),t._v("アカウントを作成 ")],1)],1)],1)],1)],1)],1)},Ts=[],js=n["default"].extend({name:"Register",components:{Header:lt,Navigation:pt},data(){return{is_form_dense:R.isSmartphoneHorizontal(),username:null,username_validation:t=>""===t||null===t?"ユーザー名を入力してください。":!1!==/^.{2,}$/.test(t)||"ユーザー名は2文字以上で入力してください。",password:null,password_showing:!0,password_validation:t=>""===t||null===t?"パスワードを入力してください。":!1!==/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(t)||"パスワードは4文字以上の半角英数記号を入力してください。"}},methods:{async register(){if(!1!==this.$refs.register.validate()){try{const t=await n["default"].axios.post("/users",{username:this.username,password:this.password});console.log("Account created."),console.log(t.data)}catch(t){if(F.ZP.isAxiosError(t)&&t.response&&422===t.response.status)switch(console.log("Failed to create account."),console.log(t.response.data),t.response.data.detail){case"Specified username is duplicated":this.$message.error("ユーザー名が重複しています。");break;case"Specified username is not accepted due to system limitations":this.$message.error("ユーザー名に token と me は使えません。");break;default:this.$message.error(`アカウントを作成できませんでした。(HTTP Error ${t.response.status})`);break}return}try{const t=await n["default"].axios.post("/users/token",new URLSearchParams({username:this.username,password:this.password}));console.log("Login successful."),console.log(t.data),R.saveAccessToken(t.data.access_token),this.$message.success("アカウントを作成しました。"),await this.$router.push({path:"/settings/account"})}catch(t){if(F.ZP.isAxiosError(t)&&t.response&&401===t.response.status)switch(console.log("Failed to login."),console.log(t.response.data),t.response.data.detail){case"Incorrect username":this.$message.error("ログインできませんでした。そのユーザー名のアカウントは存在しません。");break;case"Incorrect password":this.$message.error("ログインできませんでした。パスワードを間違えていませんか?");break;default:this.$message.error(`ログインできませんでした。(HTTP Error ${t.response.status})`);break}}}}}}),Ps=js,Zs=(0,p.Z)(Ps,Is,Ts,!1,null,"d0eaf0ae",null),As=Zs.exports,$s=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),t._m(0)],1)],1)},Ds=[function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"d-flex justify-center align-center w-100"},[e("div",{staticClass:"d-flex justify-center align-center flex-column"},[e("h1",[t._v("Not Found, or Under Development...")]),e("span",{staticClass:"mt-4 text--text text--darken-1"},[t._v("お探しのページは存在しないか、鋭意開発中です。")])])])}],Ns=n["default"].extend({name:"NotFound",components:{Header:lt,Navigation:pt}}),zs=Ns,Ks=(0,p.Z)(zs,$s,Ds,!1,null,"daa4530a",null),Bs=Ks.exports;n["default"].use(J.ZP);const Es=new J.ZP({mode:"history",base:"/",routes:[{path:"/",redirect:"/tv/"},{path:"/tv/",name:"TV Home",component:ft},{path:"/tv/watch/:channel_id",name:"TV Watch",component:xe},{path:"/settings/",name:"Settings Index",component:$e,beforeEnter:(t,e,s)=>{R.isSmartphoneVertical()||R.isSmartphoneHorizontal()||R.isTabletVertical()?s():s({path:"/settings/general/"})}},{path:"/settings/general",name:"Settings General",component:Ge},{path:"/settings/account",name:"Settings Account",component:ts},{path:"/settings/jikkyo",name:"Settings Jikkyo",component:rs},{path:"/settings/twitter",name:"Settings Twitter",component:us},{path:"/settings/environment",name:"Settings Environment",component:ws},{path:"/login/",name:"Login",component:Ss},{path:"/register/",name:"Register",component:As},{path:"*",name:"NotFound",component:Bs}],scrollBehavior(t,e,s){return s||{x:0,y:0}}});var Hs=Es,Vs=s(5205);(0,Vs.z)("/service-worker.js",{ready(){console.log("App is being served from cache by a service worker.\nFor more details, visit https://goo.gl/AFskqB")},registered(){console.log("Service worker has been registered.")},cached(){console.log("Content has been cached for offline use.")},updatefound(){console.log("New content is downloading.")},updated(){console.log("New content is available; please refresh.")},offline(){console.log("No internet connection found. App is running in offline mode.")},error(t){console.error("Error during service worker registration:",t)}}),(0,a.OK)(),n["default"].config.productionTip=!1,n["default"].use(o.Z,U),n["default"].use(r.ZP),n["default"].use(c(),{top:!1,bottom:!0,color:"#433532",dark:!0,elevation:8,timeout:2500,autoRemove:!0,closeButtonContent:"閉じる",vuetifyInstance:X});const Ls=window.matchMedia("(hover: none)").matches?[]:["hover","focus","touch"];_.ZP.options.themes.tooltip.showTriggers=Ls,_.ZP.options.themes.tooltip.hideTriggers=Ls,_.ZP.options.themes.tooltip.delay.show=0,_.ZP.options.offset=[0,7],n["default"].use(_.ZP),n["default"].component("Icon",i.JO),n["default"].component("v-tab-item-fix",w),n["default"].component("v-tabs-fix",x),n["default"].component("v-tabs-items-fix",S),new n["default"]({router:Hs,vuetify:X,render:t=>t(v)}).$mount("#app"),window.setInterval((async()=>{null!==R.getAccessToken()&&!0===R.getSettingsItem("sync_settings")&&R.syncServerSettingsToClient()}),3e3)}},e={};function s(i){var a=e[i];if(void 0!==a)return a.exports;var n=e[i]={id:i,loaded:!1,exports:{}};return t[i].call(n.exports,n,n.exports,s),n.loaded=!0,n.exports}s.m=t,function(){s.amdO={}}(),function(){var t=[];s.O=function(e,i,a,n){if(!i){var o=1/0;for(_=0;_=n)&&Object.keys(s.O).every((function(t){return s.O[t](i[l])}))?i.splice(l--,1):(r=!1,n0&&t[_-1][2]>n;_--)t[_]=t[_-1];t[_]=[i,a,n]}}(),function(){s.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return s.d(e,{a:e}),e}}(),function(){s.d=function(t,e){for(var i in e)s.o(e,i)&&!s.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})}}(),function(){s.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"===typeof window)return window}}()}(),function(){s.hmd=function(t){return t=Object.create(t),t.children||(t.children=[]),Object.defineProperty(t,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+t.id)}}),t}}(),function(){s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)}}(),function(){s.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}}(),function(){var t={143:0};s.O.j=function(e){return 0===t[e]};var e=function(e,i){var a,n,o=i[0],r=i[1],l=i[2],c=0;if(o.some((function(e){return 0!==t[e]}))){for(a in r)s.o(r,a)&&(s.m[a]=r[a]);if(l)var _=l(s)}for(e&&e(i);c {\n\n // VueComponent の key が一致する this.$slots.default 内の VNode を探す\n const index_a = this.$slots.default.findIndex((element) => {\n return a.$vnode.key === element.key;\n });\n const index_b = this.$slots.default.findIndex((element) => {\n return b.$vnode.key === element.key;\n });\n\n // index 順で並び替え\n return index_a - index_b;\n });\n\n item.$on('change', () => (this as any).onClick(item));\n if ((this as any).mandatory && !(this as any).selectedValues.length) {\n (this as any).updateMandatory();\n }\n\n // 追加された要素のソート後のインデックスを取得して更新する\n (this as any).updateItem(item, this.items.indexOf(item));\n\n // ソート後の現在アクティブなタブのインデックスを取得し直し、設定する\n // 配列の末尾以外に追加された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n },\n\n unregister(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 継承元の unregister() の処理を呼び出す(いわゆる super() )\n // ref: https://github.com/vuejs/vue/issues/2977\n (this.constructor as any).super.options.methods.unregister.call(this, item);\n\n // 配列の末尾以外から削除された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n }\n }\n});\n","\nimport { VueConstructor, VNode } from 'vue';\n\nimport { convertToUnit } from 'vuetify/lib/util/helpers'\nimport VTabs from 'vuetify/lib/components/VTabs/VTabs';\nimport VTabsBar from '@/components/VTabsBar';\n\nexport default (VTabs as VueConstructor).extend({\n methods: {\n\n // VTabsBar は VTabs から暗黙的に生成されるコンポーネントのため、直接上書きすることができない\n // そこで VTabs 自体も上書きし、VTabs で $createElement() される時の VTabsBar を自前でオーバーライドしたものに差し替える\n // ビルド済みのファイルには型定義が入っていないので any を多用せざるを得ない…\n genBar(items: VNode[], slider: VNode | null) {\n const data = {\n style: {\n height: convertToUnit((this as any).height),\n },\n props: {\n activeClass: (this as any).activeClass,\n centerActive: (this as any).centerActive,\n dark: (this as any).dark,\n light: (this as any).light,\n mandatory: !(this as any).optional,\n mobileBreakpoint: (this as any).mobileBreakpoint,\n nextIcon: (this as any).nextIcon,\n prevIcon: (this as any).prevIcon,\n showArrows: (this as any).showArrows,\n value: (this as any).internalValue,\n },\n on: {\n 'call:slider': (this as any).callSlider,\n change: (val: any) => {\n (this as any).internalValue = val;\n },\n },\n ref: 'items',\n };\n\n (this as any).setTextColor((this as any).computedColor, data);\n (this as any).setBackgroundColor((this as any).backgroundColor, data);\n\n // ここでオーバーライドした VTabsBar を使うのが最重要\n // これをやるためだけにわざわざ VTabs に関してもオーバーライドする羽目になってる…\n return (this as any).$createElement(VTabsBar, data, [\n (this as any).genSlider(slider),\n items,\n ]);\n }\n }\n});\n","\nimport { VueConstructor } from 'vue';\n\nimport { GroupableInstance } from 'vuetify/lib/components/VItemGroup/VItemGroup';\nimport VTabsItems from 'vuetify/lib/components/VTabs/VTabsItems';\n\n// VTabsItems は VItemGroup と VWindow を extend() して実装されている\nexport default (VTabsItems as VueConstructor).extend({\n data() {\n return {\n // 一応型定義をしておく\n items: [] as GroupableInstance[],\n }\n },\n methods: {\n\n // タブのデータ配列の先頭に新しい要素が追加されるとそのタブのアニメーションの向きが逆になるバグがあるので、VItemGroup 側の挙動をオーバーライドする\n // DOM 上も VNode 上も正しい順序で並んでいるが、this.items に関しては追加された順になっていてしまっていて齟齬が発生するのが原因\n // ref: https://github.com/vuetifyjs/vuetify/issues/13862\n register(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 要素を items に追加\n this.items.push(item);\n\n // this.$slots.default に VNode が、items には単に VueComponent が入っているので、事前に VNode の順番に合わせて並べ替える\n // こうすることで、追加された順ではなく元のデータ配列通りの順番になる\n this.items.sort((a, b) => {\n\n // VueComponent の key が一致する this.$slots.default 内の VNode を探す\n const index_a = this.$slots.default.findIndex((element) => {\n return a.$vnode.key === element.key;\n });\n const index_b = this.$slots.default.findIndex((element) => {\n return b.$vnode.key === element.key;\n });\n\n // index 順で並び替え\n return index_a - index_b;\n });\n\n item.$on('change', () => (this as any).onClick(item));\n if ((this as any).mandatory && !(this as any).selectedValues.length) {\n (this as any).updateMandatory();\n }\n\n // 追加された要素のソート後のインデックスを取得して更新する\n (this as any).updateItem(item, this.items.indexOf(item));\n\n // ソート後の現在アクティブなタブのインデックスを取得し直し、設定する\n // 配列の末尾以外に追加された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n // 値が異なるときだけ更新する\n // こうしないと、Safari で変なアニメーションがついてしまう\n if (this.items.indexOf(activeItem) !== (this as any).internalValue) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n }\n },\n\n unregister(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 継承元の unregister() の処理を呼び出す(いわゆる super() )\n // ref: https://github.com/vuejs/vue/issues/2977\n (this.constructor as any).super.options.methods.unregister.call(this, item);\n\n // 配列の末尾以外から削除された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n },\n\n // 最初のタブから最後のタブに遷移するとアニメーションの向きが逆になるバグがあるので、VWindow 側の挙動をオーバーライドする\n // 本来は VCarousel 用の動作だが、VTabsItems も VWindow を継承しているので、それが適用されてしまっているらしい\n // ref: https://github.com/yuwu9145/vuetify/blob/master/packages/vuetify/src/components/VWindow/VWindow.ts#L239-L252\n updateReverse(val: number, oldVal: number) {\n\n const itemsLength = this.items.length;\n const lastIndex = itemsLength - 1;\n\n if (itemsLength <= 2) return val < oldVal;\n\n // continuous が false の時、常に val < oldVal の結果を返す\n if (!(this as any).continuous) return val < oldVal;\n\n if (val === lastIndex && oldVal === 0) {\n return true;\n } else if (val === 0 && oldVal === lastIndex) {\n return false;\n } else {\n return val < oldVal;\n }\n }\n }\n});\n","\nimport Vue from 'vue';\n\nimport { IMutedCommentKeywords } from '@/interface';\n\n/**\n * 共通ユーティリティ\n */\nexport default class Utils {\n\n // バージョン情報\n // ビルド時の環境変数 (vue.config.js に記載) から取得\n static readonly version: string = process.env.VUE_APP_VERSION;\n\n // バックエンドの API のベース URL\n static readonly api_base_url = (() => {\n if (process.env.NODE_ENV === 'development') {\n // デバッグ時はポートを 7000 に強制する\n return `${window.location.protocol}//${window.location.hostname}:7000/api`;\n } else {\n // ビルド後は同じポートを使う\n return `${window.location.protocol}//${window.location.host}/api`;\n }\n })();\n\n // デフォルトの設定値\n // (同期無効) とある項目は、デバイス間で同期するとかえって面倒なことになりそうなため同期されない設定\n // ここを変えたときはサーバー側の app.schemas.ClientSettings も変更すること\n static readonly default_settings = {\n\n // ***** 設定画面から直接変更できない設定値 *****\n\n // ピン留めしているチャンネルの ID (ex: gr011) が入るリスト\n pinned_channel_ids: [] as string[],\n\n // 前回視聴画面を開いた際にパネルが表示されていたかどうか (同期無効)\n showed_panel_last_time: true as boolean,\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID (同期無効)\n selected_twitter_account_id: null as number | null,\n\n // 保存している Twitter のハッシュタグが入るリスト\n saved_twitter_hashtags: [] as string[],\n\n // ***** 設定 → 全般 *****\n\n // テレビのストリーミング画質 (Default: 1080p) (同期無効)\n tv_streaming_quality: '1080p' as ('1080p-60fps' | '1080p' | '810p' | '720p' | '540p' | '480p' | '360p' | '240p'),\n\n // テレビを通信節約モードで視聴する (Default: オフ) (同期無効)\n tv_data_saver_mode: false as boolean,\n\n // テレビを低遅延で視聴する (Default: 低遅延で視聴する) (同期無効)\n tv_low_latency_mode: true as boolean,\n\n // テレビをみるときに文字スーパーを表示する (Default: 表示する)\n tv_show_superimpose: true as boolean,\n\n // 既定のパネルの表示状態 (Default: 前回の状態を復元する)\n panel_display_state: 'RestorePreviousState' as ('RestorePreviousState' | 'AlwaysDisplay' | 'AlwaysFold'),\n\n // テレビをみるときに既定で表示されるパネルのタブ (Default: 番組情報タブ)\n tv_panel_active_tab: 'Program' as ('Program' | 'Channel' | 'Comment' | 'Twitter'),\n\n // 字幕のフォント (Default: Windows TV 丸ゴシック)\n caption_font: 'Windows TV MaruGothic' as string,\n\n // 字幕の文字を常に縁取って描画する (Default: 常に縁取る)\n always_border_caption_text: true as boolean,\n\n // 字幕の背景色を指定する (Default: 指定しない)\n specify_caption_background_color: false as boolean,\n\n // 字幕の背景色 (Default: 不透明度が 50% の黒)\n caption_background_color: '#00000080' as string,\n\n // キャプチャをクリップボードにコピーする (Default: 有効) (同期無効)\n capture_copy_to_clipboard: true as boolean,\n\n // キャプチャの保存先 (Default: ブラウザでダウンロード)\n capture_save_mode: 'Browser' as ('Browser' | 'UploadServer' | 'Both'),\n\n // 字幕が表示されているときのキャプチャの保存モード (Default: 映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する)\n capture_caption_mode: 'Both' as ('VideoOnly' | 'CompositingCaption' | 'Both'),\n\n // ***** 設定 → アカウント *****\n\n // 設定を同期する (Default: 同期しない) (同期無効)\n sync_settings: false as boolean,\n\n // ***** 設定 → ニコニコ実況 *****\n\n // コメントの速さ (Default: 1倍)\n comment_speed_rate: 1 as number,\n\n // コメントのフォントサイズ (Default: 34px)\n comment_font_size: 34 as number,\n\n // コメントの遅延時間 (Default: 1.75秒) (同期無効)\n comment_delay_time: 1.75 as number,\n\n // コメント送信後にコメント入力フォームを閉じる (Default: オン)\n close_comment_form_after_sending: true as boolean,\n\n // ***** 設定 → ニコニコ実況 (ミュート設定) *****\n\n // ミュート済みのコメントのキーワードが入るリスト\n muted_comment_keywords: [] as IMutedCommentKeywords[],\n\n // ミュート済みのニコニコユーザー ID が入るリスト\n muted_niconico_user_ids: [] as string[],\n\n // 露骨な表現を含むコメントをミュートする (Default: ミュートする)\n mute_vulgar_comments: true as boolean,\n\n // 罵倒や誹謗中傷、差別的な表現、政治的に偏った表現を含むコメントをミュートする (Default: ミュートする)\n mute_abusive_discriminatory_prejudiced_comments: true as boolean,\n\n // 文字サイズが大きいコメントをミュートする (Default: ミュートする)\n mute_big_size_comments: true as boolean,\n\n // 映像の上下に固定表示されるコメントをミュートする (Default: ミュートしない)\n mute_fixed_comments: false as boolean,\n\n // 色付きのコメントをミュートする (Default: ミュートしない)\n mute_colored_comments: false as boolean,\n\n // 8文字以上同じ文字が連続しているコメントをミュートする (Default: ミュートしない)\n mute_consecutive_same_characters_comments: false as boolean,\n\n // ***** 設定 → Twitter *****\n\n // ツイート送信後にパネルを閉じる (Default: オフ)\n fold_panel_after_sending_tweet: false as boolean,\n\n // 番組が切り替わったときにハッシュタグフォームをリセットする (Default: オン)\n reset_hashtag_when_program_switches: true as boolean,\n\n // 視聴中のチャンネルに対応する局タグを自動的に追加する (Default: オン)\n auto_add_watching_channel_hashtag: true as boolean,\n\n // 既定で表示される Twitter タブ内のタブ (Default: キャプチャタブ)\n twitter_active_tab: 'Capture' as ('Search' | 'Timeline' | 'Capture'),\n\n // ツイートにつけるハッシュタグの位置 (Default: ツイート本文の後に追加する)\n tweet_hashtag_position: 'Append' as ('Prepend' | 'Append' | 'PrependWithLineBreak' | 'AppendWithLineBreak'),\n\n // ツイートするキャプチャに番組名の透かしを描画する (Default: 透かしを描画しない)\n tweet_capture_watermark_position: 'None' as ('None' | 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight'),\n };\n\n // 同期対象の設定キー\n // サーバー側の app.schemas.ClientSettings に定義されているものと同じ\n static readonly sync_settings_keys = [\n 'pinned_channel_ids',\n 'saved_twitter_hashtags',\n 'tv_show_superimpose',\n 'panel_display_state',\n 'tv_panel_active_tab',\n 'caption_font',\n 'always_border_caption_text',\n 'specify_caption_background_color',\n 'caption_background_color',\n 'capture_save_mode',\n 'capture_caption_mode',\n 'comment_speed_rate',\n 'comment_font_size',\n 'close_comment_form_after_sending',\n 'muted_comment_keywords',\n 'muted_niconico_user_ids',\n 'mute_vulgar_comments',\n 'mute_abusive_discriminatory_prejudiced_comments',\n 'mute_big_size_comments',\n 'mute_fixed_comments',\n 'mute_colored_comments',\n 'mute_consecutive_same_characters_comments',\n 'fold_panel_after_sending_tweet',\n 'reset_hashtag_when_program_switches',\n 'auto_add_watching_channel_hashtag',\n 'twitter_active_tab',\n 'tweet_hashtag_position',\n 'tweet_capture_watermark_position',\n ];\n\n // 設定をサーバーにアップロード中かどうか\n // これが true のときは、定期的なサーバーからの設定ダウンロードを行わない\n static uploading_settings: boolean = false;\n\n\n /**\n * 設定を LocalStorage から取得する\n * @param key 設定のキー名\n * @returns 設定されている値\n */\n static getSettingsItem(key: string): any | null {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n // データは JSON で管理し、LocalStorage 上の一つのキーにまとめる\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // そのキーが保存されているときだけ、設定値を返す\n if (key in settings) {\n return settings[key];\n } else {\n // デフォルトの設定値にあればそれを使う\n if (key in Utils.default_settings) {\n return Utils.default_settings[key];\n } else {\n return null;\n }\n }\n }\n\n\n /**\n * 設定を LocalStorage に保存する\n * @param key 設定のキー名\n * @param value 設定する値\n */\n static setSettingsItem(key: string, value: any): void {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // 設定値を新しい値で置き換え\n settings[key] = value;\n\n // (名前が変わった、廃止されたなどの理由で) 現在の default_settings に存在しない設定キーを排除した上で並び替え\n // 並び替えられていないと設定データの比較がうまくいかない\n const new_settings: {[key: string]: any} = {};\n for (const default_settings_key of Object.keys(Utils.default_settings)) {\n if (default_settings_key in settings) {\n new_settings[default_settings_key] = settings[default_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n new_settings[default_settings_key] = Utils.default_settings[default_settings_key];\n }\n }\n\n // LocalStorage に保存\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(new_settings));\n\n // 更新された設定をサーバーに同期 (同期有効時のみ)\n Utils.syncClientSettingsToServer();\n }\n\n\n /**\n * ログイン時かつ同期が有効な場合、サーバーに保存されている設定データをこのクライアントに同期する\n * @param force ログイン中なら同期が有効かに関わらず実行する (デフォルト: false)\n */\n static async syncServerSettingsToClient(force = false): Promise {\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // ログインしていない時、同期が無効なときは実行しない\n if (Utils.getAccessToken() === null || (settings.sync_settings === false && force === false)) {\n return;\n }\n\n // 設定データをアップロード中のときは、動作が競合しないように終わるまで待つ\n while (Utils.uploading_settings === true) {\n await Utils.sleep(0.1);\n }\n\n try {\n\n // サーバーから設定データをダウンロード\n const server_settings: {[key: string]: any} = (await Vue.axios.get('/settings/client')).data;\n\n // クライアントの設定値をサーバーからの設定値で上書き\n for (const [server_settings_key, server_settings_value] of Object.entries(server_settings)) {\n settings[server_settings_key] = server_settings_value;\n }\n\n // LocalStorage に保存\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(settings));\n\n } catch (error) {\n // 何らかの理由でエラーになったときは何もしない\n }\n }\n\n\n /**\n * ログイン時かつ同期が有効な場合、このクライアントの設定をサーバーに同期する\n * @param force ログイン中なら同期が有効かに関わらず実行する (デフォルト: false)\n */\n static async syncClientSettingsToServer(force = false): Promise {\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // ログインしていない時、同期が無効なときは実行しない\n if (Utils.getAccessToken() === null || (settings.sync_settings === false && force === false)) {\n return;\n }\n\n // 設定データのアップロード開始\n Utils.uploading_settings = true;\n\n // 同期対象の設定キーのみで設定データをまとめ直す\n // sync_settings には同期対象外の設定は含まれない\n const sync_settings: {[key: string]: any} = {};\n for (const sync_settings_key of Utils.sync_settings_keys) {\n if (sync_settings_key in settings) {\n sync_settings[sync_settings_key] = settings[sync_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n sync_settings[sync_settings_key] = Utils.default_settings[sync_settings_key];\n }\n }\n\n // サーバーに設定データをアップロード\n try {\n await Vue.axios.put('/settings/client', sync_settings);\n } catch (error) {\n // 何もしない\n }\n\n // 設定データのアップロード終了\n Utils.uploading_settings = false;\n }\n\n\n /**\n * アクセストークンを LocalStorage から取得する\n * @returns JWT アクセストークン(ログインしていない場合は null が返る)\n */\n static getAccessToken(): string | null {\n\n // LocalStorage の取得結果をそのまま返す\n // LocalStorage.getItem() はキーが存在しなければ(=ログインしていなければ)null を返す\n return localStorage.getItem('KonomiTV-AccessToken');\n }\n\n\n /**\n * アクセストークンを LocalStorage に保存する\n * @param access_token 発行された JWT アクセストークン\n */\n static saveAccessToken(access_token: string): void {\n\n // そのまま LocalStorage に保存\n localStorage.setItem('KonomiTV-AccessToken', access_token);\n }\n\n\n /**\n * アクセストークンを LocalStorage から削除する\n * アクセストークンを削除することで、ログアウト相当になる\n */\n static deleteAccessToken(): void {\n\n // LocalStorage に KonomiTV-AccessToken キーが存在しない\n if (localStorage.getItem('KonomiTV-AccessToken') === null) return;\n\n // KonomiTV-AccessToken キーを削除\n localStorage.removeItem('KonomiTV-AccessToken');\n }\n\n\n /**\n * ブラウザが実行されている OS に応じて、\"Ctrl\" または \"Cmd\" を返す\n * キーボードショートカットのコンビネーションキーの説明を OS によって分けるために使う\n * @returns ブラウザが実行されている OS が Mac なら Cmd を、それ以外なら Ctrl を返す\n */\n static CtrlOrCmd(): 'Ctrl' | 'Cmd' {\n // iPhone・iPad で純正キーボードを接続した場合も一応想定して、iPhone・iPad も含める(動くかは未検証)\n return /iPhone|iPad|Macintosh/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl';\n }\n\n\n /**\n * Blob に格納されているデータをブラウザにダウンロードさせる\n * @param blob Blob オブジェクト\n * @param filename 保存するファイル名\n */\n static downloadBlobData(blob: Blob, filename: string): void {\n\n // Blob URL を発行\n const blob_url = URL.createObjectURL(blob);\n\n // 画像をダウンロード\n const link = document.createElement('a');\n link.download = filename;\n link.href = blob_url;\n link.click();\n\n // Blob URL を破棄\n URL.revokeObjectURL(blob_url);\n }\n\n\n /**\n * innerHTML しても問題ないように HTML の特殊文字をエスケープする\n * PHP の htmlspecialchars() と似たようなもの\n * @param content HTML エスケープされてないテキスト\n * @returns HTML エスケープされたテキスト\n */\n static escapeHTML(content: string): string {\n\n // HTML エスケープが必要な文字\n // ref: https://www.php.net/manual/ja/function.htmlspecialchars.php\n const html_escape_table = {\n '&': '&',\n '\"': '"',\n '\\'': ''',\n '<': '<',\n '>': '>',\n };\n\n // ref: https://qiita.com/noriaki/items/4bfef8d7cf85dc1035b3\n return content.replace(/[&\"'<>]/g, (match) => {\n return html_escape_table[match];\n });\n }\n\n\n /**\n * OAuth 連携時のポップアップを画面中央に表示するための windowFeatures 文字列を取得する\n * ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1\n * @returns window.open() で使う windowFeatures 文字列\n */\n static getWindowFeatures(): string {\n\n // ポップアップウインドウのサイズ\n const popupSizeWidth = 650;\n const popupSizeHeight = window.screen.height >= 800 ? 800 : window.screen.height - 100;\n\n // ポップアップウインドウの位置\n const posTop = (window.screen.height - popupSizeHeight) / 2;\n const posLeft = (window.screen.width - popupSizeWidth) / 2;\n\n return `toolbar=0,status=0,top=${posTop},left=${posLeft},width=${popupSizeWidth},height=${popupSizeHeight},modal=yes,alwaysRaised=yes`;\n }\n\n\n /**\n * 現在フォーカスを持っている要素に指定された CSS クラスが付与されているか\n * @param class_name 存在を確認する CSS クラス名\n * @returns document.activeElement が class_name で指定したクラスを持っているかどうか\n */\n static hasActiveElementClass(class_name: string): boolean {\n if (document.activeElement === null) return false;\n return document.activeElement.classList.contains(class_name);\n }\n\n\n /**\n * 表示画面がスマホ横画面かどうか\n * @returns スマホ横画面なら true を返す\n */\n static isSmartphoneHorizontal(): boolean {\n return window.matchMedia('(max-width: 1000px) and (max-height: 450px)').matches;\n }\n\n\n /**\n * 表示画面がスマホ縦画面かどうか\n * @returns スマホ縦画面なら true を返す\n */\n static isSmartphoneVertical(): boolean {\n return window.matchMedia('(max-width: 600px) and (min-height: 450.01px)').matches;\n }\n\n\n /**\n * 表示画面がタブレット横画面かどうか\n * @returns タブレット横画面なら true を返す\n */\n static isTabletHorizontal(): boolean {\n return window.matchMedia('(max-width: 1264px) and (max-height: 850px)').matches;\n }\n\n\n /**\n * 表示画面がタブレット縦画面かどうか\n * @returns タブレット縦画面なら true を返す\n */\n static isTabletVertical(): boolean {\n return window.matchMedia('(max-width: 850px) and (min-height: 850.01px)').matches;\n }\n\n\n /**\n * 任意の桁で切り捨てする\n * ref: https://qiita.com/nagito25/items/0293bc317067d9e6c560#comment-87f0855f388953843037\n * @param value 切り捨てする数値\n * @param base どの桁で切り捨てするか (-1 → 10の位 / 3 → 小数第3位)\n * @return 切り捨てした値\n */\n static mathFloor(value: number, base: number = 0): number {\n return Math.floor(value * (10**base)) / (10**base);\n }\n\n\n /**\n * async/await でスリープ的なもの\n * @param seconds 待機する秒数\n * @returns Promise を返すので、await sleep(1); のように使う\n */\n static async sleep(seconds: number): Promise {\n return await new Promise(resolve => setTimeout(resolve, seconds * 1000));\n }\n\n\n /**\n * 現在時刻の UNIX タイムスタンプを取得する (デバッグ用)\n * @returns 現在時刻の UNIX タイムスタンプ\n */\n static time(): number {\n return Date.now() / 1000;\n }\n\n\n /**\n * 指定された値の型の名前を取得する\n * ref: https://qiita.com/amamamaou/items/ef0b797156b324bb4ef3\n * @returns 指定された値の型の名前\n */\n static typeof(value: any): string {\n return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();\n }\n\n\n /**\n * 文字列中に含まれる URL をリンクの HTML に置き換える\n * @param text 置換対象の文字列\n * @returns URL をリンクに置換した文字列\n */\n static URLtoLink(text: string): string {\n\n // HTML の特殊文字で表示がバグらないように、事前に HTML エスケープしておく\n text = Utils.escapeHTML(text);\n\n // ref: https://www.softel.co.jp/blogs/tech/archives/6099\n const pattern = /(https?:\\/\\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig;\n return text.replace(pattern, '$1');\n }\n}\n","\nimport { ChannelType, ChannelTypePretty, IChannel } from '@/interface';\n\n/**\n * チャンネル周りのユーティリティ\n */\nexport class ChannelUtils {\n\n /**\n * チャンネル ID からチャンネルタイプを取得する\n * @param channel_id チャンネル ID\n * @param is_pretty ChannelTypePretty 型で返すかどうか\n * @returns チャンネルタイプ\n */\n static getChannelType(channel_id: string, is_pretty: boolean = false): ChannelType | ChannelTypePretty {\n const result = channel_id.match('(?[a-z]+)[0-9]+').groups.channel_type.toUpperCase();\n if (is_pretty === true) {\n switch (result) {\n case 'GR':\n return '地デジ';\n case 'STARDIGIO':\n return 'StarDigio';\n default:\n return result as ChannelTypePretty;\n }\n } else {\n return result as ChannelType;\n }\n }\n\n\n /**\n * チャンネルの実況勢いから「多」「激多」「祭」を取得する\n * ref: https://ja.wikipedia.org/wiki/%E3%83%8B%E3%82%B3%E3%83%8B%E3%82%B3%E5%AE%9F%E6%B3%81\n * @param channel_force チャンネルの実況勢い\n * @returns normal(普通)or many(多)or so-many(激多)or festival(祭)\n */\n static getChannelForceType(channel_force: number | null): 'normal' | 'many' | 'so-many' | 'festival' {\n\n // 実況勢いが null(=対応する実況チャンネルがない)\n if (channel_force === null) return 'normal';\n\n // 実況勢いが 1000 コメント以上(祭)\n if (channel_force >= 1000) return 'festival';\n // 実況勢いが 200 コメント以上(激多)\n if (channel_force >= 200) return 'so-many';\n // 実況勢いが 100 コメント以上(多)\n if (channel_force >= 100) return 'many';\n\n // それ以外\n return 'normal';\n }\n\n\n /**\n * チャンネルタイプとリモコン番号からチャンネル情報を取得する\n * @param channels_list チャンネルリスト\n * @param channel_type チャンネルタイプ\n * @param remocon_id リモコン番号\n * @returns チャンネル情報\n */\n static getChannelFromRemoconID(channels_list: Map, channel_type: ChannelType, remocon_id: number): IChannel | null {\n\n // ChannelTypePretty 型に変換する\n const channel_type_pretty = channel_type.replace('GR', '地デジ').replace('STARDIGIO', 'StarDigio') as ChannelTypePretty;\n\n // 指定されたチャンネルタイプのチャンネルを取得\n const channels = channels_list.get(channel_type_pretty); //「GR」は「地デジ」に置換してから取得\n\n // リモコン番号が一致するチャンネルを見つけ、一番最初に見つかったものを返す\n for (let index = 0; index < channels.length; index++) {\n const channel = channels[index];\n if (channel.remocon_id === remocon_id) {\n return channel;\n }\n }\n\n // リモコン番号が一致するチャンネルを見つけられなかった\n return null;\n }\n\n\n /**\n * 前・現在・次のチャンネル情報を取得する\n * @param channels_list チャンネルリスト\n * @param channel_id 起点にする現在のチャンネル ID\n * @returns 前・現在・次のチャンネル情報\n */\n static getPreviousAndCurrentAndNextChannel(channels_list: Map, channel_id: string): IChannel[] {\n\n // 前後のチャンネルを取得\n const channels = channels_list.get(this.getChannelType(channel_id, true) as ChannelTypePretty);\n for (let index = 0; index < channels.length; index++) {\n const element = channels[index];\n\n // チャンネル ID が一致したときだけ\n if (element.channel_id === channel_id) {\n\n // インデックスが最初か最後の時はそれぞれ最後と最初にインデックスを一周させる\n let previous_index = index - 1;\n if (previous_index === -1) previous_index = channels.length - 1;\n let next_index = index + 1;\n if (next_index === channels.length) next_index = 0;\n\n // 前・現在・次のチャンネル情報を返す\n return [channels[previous_index], channels[index], channels[next_index]];\n }\n }\n }\n}\n","\nimport { Buffer } from 'buffer';\nimport { convertBlobToPng, copyBlobToClipboard } from 'copy-image-clipboard';\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/ja';\nimport * as piexif from 'piexifjs';\nimport Vue from 'vue';\n\nimport { IChannel, ICaptureExifData, IProgram } from '@/interface';\nimport Utils from './Utils';\n\nexport class PlayerCaptureHandler {\n\n private player: any;\n private player_container: HTMLDivElement;\n private captured_callback: (blob: Blob, filename: string) => void;\n private capture_button: HTMLDivElement;\n private comment_capture_button: HTMLDivElement;\n private canvas: OffscreenCanvas | HTMLCanvasElement;\n private canvas_context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n constructor(player: any, captured_callback: (blob: Blob, filename: string) => void) {\n\n this.player = player;\n this.player_container = this.player.container;\n this.captured_callback = captured_callback;\n\n // コメント付きキャプチャボタンの HTML を追加\n // insertAdjacentHTML で .dplayer-icons-right の一番左側に配置する\n // この後に通常のキャプチャボタンが insert されるので、実際は左から2番目\n // TODO: ボタンのデザインをコメント付きだと分かるようなものに変更する\n this.player_container.querySelector('.dplayer-icons.dplayer-icons-right').insertAdjacentHTML('afterbegin', `\n
\n \n \n \n
\n `);\n\n // キャプチャボタンの HTML を追加\n // 標準のスクリーンショット機能は貧弱なので、あえて独自に実装している(そのほうが自由度も高くてやりやすい)\n // insertAdjacentHTML で .dplayer-icons-right の一番左側に配置する\n this.player_container.querySelector('.dplayer-icons.dplayer-icons-right').insertAdjacentHTML('afterbegin', `\n
\n \n \n \n
\n `);\n\n this.comment_capture_button = this.player_container.querySelector('.dplayer-comment-capture-icon');\n this.capture_button = this.player_container.querySelector('.dplayer-capture-icon');\n\n // キャプチャ用の Canvas を初期化\n // パフォーマンス向上のため、一度作成した Canvas は使い回す\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n this.canvas = ('OffscreenCanvas' in window) ? new OffscreenCanvas(0, 0) : document.createElement('canvas');\n this.canvas_context = this.canvas.getContext('2d', {alpha: false, desynchronized: true, willReadFrequently: false});\n\n // 映像の解像度を Canvas サイズとして設定\n // 映像が読み込まれた / 画質が変わった際に Canvas のサイズを映像のサイズに合わせる\n this.canvas.width = 0;\n this.canvas.height = 0;\n player.on('loadedmetadata', async () => {\n this.canvas.width = player.video.videoWidth;\n this.canvas.height = player.video.videoHeight;\n // 映像サイズがちゃんと設定されるまで繰り返す (Safari 対策)\n while (this.canvas.width === 0 && this.canvas.height === 0) {\n await Utils.sleep(0.1);\n this.canvas.width = player.video.videoWidth;\n this.canvas.height = player.video.videoHeight;\n }\n });\n }\n\n\n /**\n * 映像をキャプチャして保存する\n * 映像のみと字幕付き (字幕表示時のみ) の両方のキャプチャを生成できる\n * @param channel チャンネル情報 (キャプチャの EXIF メタデータに番組情報を書き込むのに必要)\n * @param with_comments キャプチャにコメントを合成するかどうか\n */\n public async captureAndSave(channel: IChannel, with_comments: boolean): Promise {\n\n const total_time = Utils.time();\n\n // ***** バリデーション *****\n\n // ラジオチャンネルを視聴している場合 (当然映像がないのでキャプチャできない)\n if (channel.is_radiochannel === true) {\n this.player.notice('ラジオチャンネルはキャプチャできません。');\n return;\n }\n\n // まだ映像の表示準備が終わっていない (Canvas の幅/高さが 0 のまま)\n if (this.canvas.width === 0 && this.canvas.height === 0) {\n this.player.notice('読み込み中はキャプチャできません。');\n return;\n }\n\n // コメントが表示されていないのにコメント付きキャプチャしようとした\n if (with_comments === true && this.player.danmaku.showing === false) {\n this.player.notice('コメントを付けてキャプチャするには、コメント表示をオンにしてください。');\n return;\n }\n\n // ***** キャプチャの下準備 *****\n\n // キャプチャ中はキャプチャボタンをハイライトする\n this.addHighlight(with_comments);\n\n // ファイル名(拡張子なし)\n // TODO: ファイル名パターンを変更できるようにする\n const filename_base = `Capture_${dayjs().format('YYYYMMDD-HHmmss')}`;\n const filename = `${filename_base}.jpg`; // 字幕なしキャプチャ\n const filename_caption = `${filename_base}_caption.jpg`; // 字幕ありキャプチャ\n\n // 字幕・文字スーパーの Canvas を取得\n // getRawCanvas() で映像と同じ解像度の Canvas が取得できる\n const caption_canvas: HTMLCanvasElement = this.player.plugins.aribb24Caption.getRawCanvas();\n const superimpose_canvas: HTMLCanvasElement = this.player.plugins.aribb24Superimpose.getRawCanvas();\n\n // 字幕が表示されているか\n const is_caption_showing = (this.player.plugins.aribb24Caption.isShowing === true &&\n this.player.plugins.aribb24Caption.isPresent());\n\n // 文字スーパーが表示されているか\n const is_superimpose_showing = (this.player.plugins.aribb24Superimpose.isShowing === true &&\n this.player.plugins.aribb24Superimpose.isPresent());\n\n // 字幕が表示されている場合、表示中の字幕のテキストを取得\n // 取得した字幕のテキストは、キャプチャに字幕が合成されているかに関わらず、常に EXIF メタデータに書き込まれる\n // 字幕が表示されていない場合は null を入れ、キャプチャしたシーンで字幕が表示されていなかったことを明示する\n const caption_text = is_caption_showing ? (this.player.plugins.aribb24Caption.getTextContent() as string) : null;\n\n // エクスポートして保存する共通処理\n const export_and_save = async (\n canvas: OffscreenCanvas | HTMLCanvasElement,\n filename: string,\n program: IProgram,\n caption_text: string | null,\n is_caption_composited: boolean,\n is_comment_composited: boolean,\n ): Promise => {\n\n // Canvas を Blob にエクスポート\n const time = Utils.time();\n let blob: Blob;\n try {\n blob = await this.exportToBlob(canvas);\n } catch (error) {\n this.player.notice('キャプチャの保存に失敗しました…');\n return false;\n }\n console.log('[PlayerCaptureHandler] Export to Blob:', Utils.mathFloor(Utils.time() - time, 3), 'sec');\n\n // キャプチャに番組情報などのメタデータ (EXIF) をセット\n blob = await this.setEXIFDataToCapture(blob, program, caption_text, is_caption_composited, is_comment_composited);\n\n // キャプチャの保存先: ブラウザでダウンロード or 両方\n if (['Browser', 'Both'].includes(Utils.getSettingsItem('capture_save_mode'))) {\n Utils.downloadBlobData(blob, filename);\n }\n\n // キャプチャの保存先: KonomiTV サーバーにアップロード or 両方\n // 時間がかかるし完了を待つ必要がないので非同期\n if (['UploadServer', 'Both'].includes(Utils.getSettingsItem('capture_save_mode'))) {\n this.uploadCaptureToServer(blob, filename);\n }\n\n return blob;\n }\n\n // ***** 映像のキャプチャ *****\n\n // null はまだキャプチャしていないことを、false はキャプチャに失敗したことを表す\n let capture_normal: {blob: Blob, filename: string} | null | false = null;\n let capture_caption: {blob: Blob, filename: string} | null | false = null;\n\n // 映像の ImageBitmap を取得\n const image_bitmap = await createImageBitmap(this.player.video);\n\n // もし映像以外に追加で合成するものがないなら、処理の高速化のために ImageBitmap をそのまま Canvas に転送して Blob 化する\n // コメントキャプチャではない & 文字スーパーが表示されていない (=合成処理を行う必要がない) &\n // (字幕が表示されていない or 字幕が表示されているが合成しないように設定されている) 場合\n // コメント付きキャプチャではなく、かつ字幕のない番組では大半がここの処理を通ることになる\n if (with_comments === false && is_superimpose_showing === false &&\n (is_caption_showing === false || Utils.getSettingsItem('capture_caption_mode') === 'VideoOnly')) {\n\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n const bitmap_canvas = ('OffscreenCanvas' in window) ?\n new OffscreenCanvas(image_bitmap.width, image_bitmap.height) : document.createElement('canvas');\n bitmap_canvas.width = image_bitmap.width;\n bitmap_canvas.height = image_bitmap.height;\n const canvas_context = bitmap_canvas.getContext('bitmaprenderer', {alpha: false});\n\n // Canvas に映像がキャプチャされた ImageBitmap を転送\n // 描画ではなくゼロコピーで転送しているらしい…?\n canvas_context.transferFromImageBitmap(image_bitmap);\n image_bitmap.close(); // 今後使うことはないので明示的に閉じる\n\n // ファイル名\n // 保存モードが「字幕キャプチャのみ」のとき (=字幕キャプチャのみをキャプチャする設定にしていたが、字幕がそもそもないとき) は、\n // 便宜上字幕ありキャプチャと同じファイル名で保存する\n const filename_real =\n (Utils.getSettingsItem('capture_caption_mode') === 'CompositingCaption') ? filename_caption : filename;\n\n // Blob にエクスポートして保存\n // false が返ってきた場合は失敗を意味する\n const blob = await export_and_save(\n bitmap_canvas, filename_real, channel.program_present, caption_text, false, with_comments);\n if (blob !== false) {\n capture_normal = {blob: blob, filename: filename_real};\n } else {\n capture_normal = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_normal !== false) {\n this.captured_callback(capture_normal.blob, capture_normal.filename);\n }\n\n // ***** 通常実行 (Canvas にキャプチャ以外のデータを重ねて描画する必要があるケース) *****\n\n } else {\n\n const promises: Promise[] = [];\n\n // Canvas に映像がキャプチャされた ImageBitmap を描画\n this.canvas_context.drawImage(image_bitmap, 0, 0, this.canvas.width, this.canvas.height);\n\n // 文字スーパーを描画 (表示されている場合)\n // 文字スーパー自体が稀だし、文字スーパーなしでキャプチャ撮りたいユースケースはない…はず\n if (is_superimpose_showing === true) {\n this.canvas_context.drawImage(superimpose_canvas, 0, 0, this.canvas.width, this.canvas.height);\n }\n\n // コメント付きキャプチャ: 追加でニコニコ実況のコメントを描画\n let comments_image: HTMLImageElement | null = null;\n if (with_comments === true) {\n comments_image = await this.createCommentsImage();\n await this.drawComments(comments_image);\n }\n\n // ***** 映像のみのキャプチャを保存 *****\n\n // 字幕表示時のキャプチャの保存モード: 映像のみ or 両方\n // 保存モードが「字幕キャプチャのみ」になっているが字幕が表示されていない場合も実行する\n if (['VideoOnly', 'Both'].includes(Utils.getSettingsItem('capture_caption_mode')) || is_caption_showing === false) {\n\n promises.push((async () => {\n\n // ファイル名\n // 保存モードが「字幕キャプチャのみ」のとき (=字幕キャプチャのみをキャプチャする設定にしていたが、字幕がそもそもないとき) は、\n // 便宜上字幕ありキャプチャと同じファイル名で保存する\n const filename_real =\n (Utils.getSettingsItem('capture_caption_mode') === 'CompositingCaption') ? filename_caption : filename;\n\n // Blob にエクスポートして保存\n const blob = await export_and_save(\n this.canvas, filename_real, channel.program_present, caption_text, false, with_comments);\n if (blob !== false) {\n capture_normal = {blob: blob, filename: filename_real};\n } else {\n capture_normal = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_normal !== false) {\n this.captured_callback(capture_normal.blob, capture_normal.filename);\n }\n\n })());\n }\n\n // ***** 字幕付きのキャプチャを保存 *****\n\n // 字幕表示時のキャプチャの保存モード: 字幕キャプチャのみ or 両方\n // 字幕が表示されているときのみ実行(字幕が表示されていないのにやっても意味がない)\n if (['CompositingCaption', 'Both'].includes(Utils.getSettingsItem('capture_caption_mode')) && is_caption_showing === true) {\n\n promises.push((async () => {\n\n // コメント付きキャプチャ: 映像と文字スーパーの描画をやり直す\n // すでに字幕なしキャプチャを生成する過程でコメントを描画してしまっているため、映像描画からやり直す必要がある\n if (with_comments === true) {\n this.canvas_context.drawImage(image_bitmap, 0, 0, this.canvas.width, this.canvas.height);\n if (is_superimpose_showing === true) {\n this.canvas_context.drawImage(superimpose_canvas, 0, 0, this.canvas.width, this.canvas.height);\n }\n }\n image_bitmap.close(); // 今後使うことはないので明示的に閉じる\n\n // 字幕を重ねて描画\n this.canvas_context.drawImage(caption_canvas, 0, 0, this.canvas.width, this.canvas.height);\n\n // コメント付きキャプチャ: 追加でニコニコ実況のコメントを描画\n if (with_comments === true) {\n await this.drawComments(comments_image);\n }\n\n // Blob にエクスポートして保存\n const blob = await export_and_save(\n this.canvas, filename_caption, channel.program_present, caption_text, true, with_comments,\n );\n if (blob !== false) {\n capture_caption = {blob: blob, filename: filename_caption};\n } else {\n capture_caption = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_caption !== false) {\n // 字幕表示時のキャプチャの保存モードが「両方 (Both)」のときのみ、映像のみのキャプチャの生成が終わるまで待ってから実行\n // 必ずキャプチャリストへの追加が [映像のみ] → [字幕付き] の順序で行われるようにする\n if (Utils.getSettingsItem('capture_caption_mode') === 'Both') {\n while (capture_normal === null) {\n // Blob (成功) か false (失敗) が capture_normal に入るまでループ\n await Utils.sleep(0.01);\n }\n }\n this.captured_callback(capture_caption.blob, capture_caption.filename);\n }\n\n })());\n }\n\n // すべてのキャプチャ処理が終わるまで待つ\n await Promise.all(promises);\n }\n\n console.log('[PlayerCaptureHandler] Total:', Utils.mathFloor(Utils.time() - total_time, 3), 'sec');\n\n // キャプチャボタンのハイライトを削除する\n this.removeHighlight(with_comments);\n\n // Twitter タブのキャプチャリストに送る処理が最優先なので、コールバックを実行しきった後に時間のかかるクリップボードへのコピーを行う\n for (const capture of [capture_normal, capture_caption]) {\n\n // クリップボードへのコピーが有効なら、キャプチャの Blob をクリップボードにコピー\n // PNG 以外は受け付けないそうなので、JPEG を PNG に変換してからコピーしている\n if (Utils.getSettingsItem('capture_copy_to_clipboard') && capture !== null && capture !== false) {\n try {\n await copyBlobToClipboard(await convertBlobToPng(capture.blob));\n } catch (error) {\n this.player.notice('クリップボードへのキャプチャのコピーに失敗しました…');\n console.error(error);\n }\n }\n }\n }\n\n\n /**\n * キャプチャボタンをハイライトする\n * @param with_comments コメント付きキャプチャボタンをハイライトするか\n */\n private addHighlight(with_comments: boolean = false): void {\n if (with_comments) {\n this.comment_capture_button.classList.add('dplayer-capturing');\n } else {\n this.capture_button.classList.add('dplayer-capturing');\n }\n }\n\n\n /**\n * キャプチャボタンのハイライトを外す\n * @param with_comments コメント付きキャプチャボタンのハイライトを外すか\n */\n private removeHighlight(with_comments: boolean = false): void {\n if (with_comments) {\n this.comment_capture_button.classList.remove('dplayer-capturing');\n } else {\n this.capture_button.classList.remove('dplayer-capturing');\n }\n }\n\n\n /**\n * DPlayer から取得したコメント HTML を SVG 画像の HTMLImageElement に変換する\n * ZenzaWatch のコードを参考にしている\n * ref: https://github.com/segabito/ZenzaWatch/blob/master/packages/lib/src/dom/VideoCaptureUtil.js\n * ref: https://web.archive.org/web/2/https://developer.mozilla.org/ja/docs/Web/HTML/Canvas/Drawing_DOM_objects_into_a_canvas\n * @param html DPlayer から取得したコメント HTML\n * @param width SVG 画像の幅\n * @param height SVG 画像の高さ\n * @returns SVG 画像の HTMLImageElement\n */\n private async commentsHTMLtoSVGImage(html: string, width: number, height: number): Promise {\n\n // SVG の foreignObject を使い、HTML をそのまま SVG に埋め込む\n // SVG なので、CSS はインラインでないと適用されない…\n // DPlayer の danmaku.scss の内容のうち、描画に必要なプロパティのみを列挙 (追加変更したものもある)\n // ref: https://github.com/tsukumijima/DPlayer/blob/master/src/css/danmaku.scss\n const svg = (`\n \n \n
\n \n ${html}\n
\n
\n
\n `).trim();\n\n // Data URL 化して Image オブジェクトにする\n // わざわざ Blob にするよりこっちのほうが楽\n const image = new Image();\n image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n\n // Image は onload を使わなくても await Image.decode() でロードできる\n await image.decode();\n return image;\n }\n\n\n /**\n * DPlayer から表示中のコメントを取得し、SVG 画像の HTMLImageElement を作成する\n * @returns 表示されているコメントが描画された HTMLImageElement\n */\n private async createCommentsImage(): Promise {\n\n // コメントが表示されている要素の HTML を取得する\n let comments_html = (this.player.template.danmaku as HTMLDivElement).outerHTML;\n\n // HTML を取得するだけではスクロール中コメントの表示位置が特定できないため、HTML を修正する\n for (const comment of this.player_container.querySelectorAll('.dplayer-danmaku-move')) { // コメントの数だけ置換\n // スクロール中のコメントの表示座標を計算\n const position = comment.getBoundingClientRect().left - this.player.video.getBoundingClientRect().left;\n comments_html = comments_html.replace(/transform: translateX\\(.*?\\);/, `left: ${position}px;`)\n .replaceAll('border: 2px solid #E64F97;', '');\n }\n\n // HTML を画像として取得\n // SVG のサイズはコメントが表示されている要素に合わせる (そうしないとプレイヤー側と一致しない)\n // SVG はベクター画像なので、リサイズしても画質が変わらないはず\n return await this.commentsHTMLtoSVGImage(\n comments_html,\n (this.player.template.danmaku as HTMLDivElement).offsetWidth,\n (this.player.template.danmaku as HTMLDivElement).offsetHeight,\n );\n }\n\n\n /**\n * 現在表示されているニコニコ実況のコメントを Canvas に描画する\n */\n private async drawComments(comments_image: HTMLImageElement): Promise {\n\n // コメント描画領域がコントロールの表示によりリサイズされている (=16:9でない) 場合も考慮して、コメント要素の offsetWidth から高さを求める\n // 映像の横解像度 (ex: 1920) がコメント描画領域の幅 (ex: 1280) の何倍かの割合 (ex: 1.5 (150%))\n const draw_scale_ratio = this.canvas.width / (this.player.template.danmaku as HTMLDivElement).offsetWidth;\n\n // コメント描画領域の高さを映像の横解像度に合わせて(コメント描画領域のアスペクト比を維持したまま)拡大した値\n // 映像の縦解像度が 1080 のとき、コントロールがコメント領域と被っていない or 表示されていないなら、この値は 1080 に近くなる\n const draw_height = (this.player.template.danmaku as HTMLDivElement).offsetHeight * draw_scale_ratio;\n\n this.canvas_context.drawImage(comments_image, 0, 0, this.canvas.width, draw_height);\n }\n\n\n /**\n * Canvas もしくは OffscreenCanvas に描画されている画像を Blob に変換する\n * JPEG 画像の品質は 99% にした方が若干 Blob 変換までの速度が速い (?)\n * @param canvas Canvas もしくは OffscreenCanvas\n * @returns Blob 化した画像\n */\n private async exportToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promise {\n if (canvas instanceof OffscreenCanvas) {\n return await canvas.convertToBlob({type: 'image/jpeg', quality: 0.99});\n } else {\n return new Promise((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob !== null) {\n resolve(blob);\n } else {\n reject(new Error('Failed to convert canvas to blob'));\n }\n }, 'image/jpeg', 0.99);\n });\n }\n }\n\n\n /**\n * キャプチャ画像に番組情報と撮影時刻、字幕やコメントが合成されているかどうかのメタデータ (EXIF) をセットする\n * @param blob キャプチャ画像の Blob オブジェクト\n * @param program EXIF にセットする番組情報オブジェクト\n * @param caption_text 字幕のテキスト (キャプチャしたときに字幕が表示されていなければ null)\n * @param is_caption_composited 字幕が合成されているか\n * @param is_comment_composited コメントが合成されているか\n * @returns EXIF が追加されたキャプチャ画像の Blob オブジェクト\n */\n private async setEXIFDataToCapture(\n blob: Blob,\n program: IProgram,\n caption_text: string | null,\n is_caption_composited: boolean,\n is_comment_composited: boolean,\n ): Promise {\n\n // 番組開始時刻換算のキャプチャ時刻 (秒)\n const captured_playback_position = dayjs().diff(dayjs(program.start_time), 'second', true);\n\n // EXIF の XPComment 領域に入れるメタデータの JSON オブジェクト\n // 撮影時刻とチャンネル・番組を一意に特定できる情報を入れる\n const json: ICaptureExifData = {\n captured_at: dayjs().format('YYYY-MM-DDTHH:mm:ss+09:00'), // ISO8601 フォーマットのキャプチャ時刻\n captured_playback_position: captured_playback_position, // 番組開始時刻換算のキャプチャ時刻 (秒)\n network_id: program.network_id, // 番組が放送されたチャンネルのネットワーク ID\n service_id: program.service_id, // 番組が放送されたチャンネルのサービス ID\n event_id: program.event_id, // 番組のイベント ID\n title: program.title, // 番組タイトル\n description: program.description, // 番組概要\n start_time: program.start_time, // 番組開始時刻 (ISO8601 フォーマット)\n end_time: program.end_time, // 番組終了時刻 (ISO8601 フォーマット)\n duration: program.duration, // 番組長 (秒)\n caption_text: caption_text, // 字幕のテキスト (キャプチャした瞬間に字幕が表示されていなかったときは null)\n is_caption_composited: is_caption_composited, // 字幕が合成されているか\n is_comment_composited: is_comment_composited, // コメントが合成されているか\n }\n\n // 保存する EXIF メタデータを構築\n // ref: 「カメラアプリで体感するWeb App」4.2\n const datetime = dayjs().format('YYYY:MM:DD HH:mm:ss'); // すべてコロンで区切るのがポイント\n const exif: piexif.IExif = {\n '0th': {\n // 必須らしいプロパティ\n // とりあえずデフォルト値 (?) を設定しておく\n [piexif.TagValues.ImageIFD.XResolution]: [72, 1],\n [piexif.TagValues.ImageIFD.YResolution]: [72, 1],\n [piexif.TagValues.ImageIFD.ResolutionUnit]: 2,\n [piexif.TagValues.ImageIFD.YCbCrPositioning]: 1,\n // 撮影時刻\n [piexif.TagValues.ImageIFD.DateTime]: datetime,\n // ソフトウェア名\n [piexif.TagValues.ImageIFD.Software]: `KonomiTV version ${Utils.version}`,\n // Microsoft 拡張のコメント領域(エクスプローラーで出てくるコメント欄と同じもの)\n // ref: https://stackoverflow.com/a/66186660/17124142\n [piexif.TagValues.ImageIFD.XPComment]: [...Buffer.from(JSON.stringify(json), 'ucs2')],\n },\n 'Exif': {\n // 必須らしいプロパティ\n // とりあえずデフォルト値 (?) を設定しておく\n [piexif.TagValues.ExifIFD.ExifVersion]: '0230',\n [piexif.TagValues.ExifIFD.ComponentsConfiguration]: '\\x01\\x02\\x03\\x00',\n [piexif.TagValues.ExifIFD.FlashpixVersion]: '0100',\n [piexif.TagValues.ExifIFD.ColorSpace]: 1,\n // 撮影時刻\n [piexif.TagValues.ExifIFD.DateTimeOriginal]: datetime,\n [piexif.TagValues.ExifIFD.DateTimeDigitized]: datetime,\n },\n };\n const exif_string = piexif.dump(exif); // バイナリ文字列に変換した EXIF データ\n\n // piexifjs はバイナリ文字列か DataURL しか受け付けないので、Blob をバイナリ文字列に変換\n const blob_string: string = await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsBinaryString(blob); // バイナリ文字列で読み込む\n });\n\n // 画像に EXIF を挿入\n // 戻り値は EXIF が追加された画像のバイナリ文字列 (なぜ未だにバイナリ文字列で実装してるんだ…)\n const blob_string_new = piexif.insert(exif_string, blob_string);\n\n // 画像のバイナリ文字列を ArrayBuffer に変換\n // ref: 「カメラアプリで体感するWeb App」4.2\n const buffer = new Uint8Array(blob_string_new.length);\n for (let index = 0; index < buffer.length; index++) {\n buffer[index] = blob_string_new.charCodeAt(index) & 0xff;\n }\n\n // 新しい Blob を返す\n return new Blob([buffer], {type: blob.type});\n }\n\n\n /**\n * KonomiTV サーバーにキャプチャ画像をアップロードする関数\n * @param blob キャプチャ画像の Blob\n * @param filename サーバーに保存するときのファイル名\n */\n private async uploadCaptureToServer(blob: Blob, filename: string): Promise {\n\n // キャプチャ画像の File オブジェクト (= Blob) を FormData に入れる\n // multipart/form-data で送るために必要\n // ref: https://r17n.page/2020/02/04/nodejs-axios-file-upload-api/\n const form_data = new FormData();\n form_data.append('image', blob, filename);\n\n // キャプチャ画像アップロード API にリクエスト\n try {\n await Vue.axios.post('/captures', form_data, {headers: {'Content-Type': 'multipart/form-data'}});\n } catch (error) {\n console.error(error);\n this.player.notice('キャプチャのアップロードに失敗しました。');\n }\n }\n}\n","\n/**\n * プレイヤー周りのユーティリティ\n */\nexport class PlayerUtils {\n\n /**\n * プレイヤーの背景画像をランダムで取得し、その URL を返す\n * @returns ランダムで設定されたプレイヤーの背景画像の URL\n */\n static generatePlayerBackgroundURL(): string {\n const background_count = 12; // 12種類から選択\n const random = (Math.floor(Math.random() * background_count) + 1);\n return `/assets/images/player-backgrounds/${random.toString().padStart(2, '0')}.jpg`;\n }\n\n\n /**\n * 現在のブラウザで H.265 / HEVC 映像が再生できるかどうかを取得する\n * @returns 再生できるなら true、できないなら false\n */\n static isHEVCVideoSupported(): boolean {\n // hvc1.1.1.L123.B0 の部分は呪文 (HEVC であることと、そのプロファイルを示す値らしい)\n return document.createElement('video').canPlayType('video/mp4;codecs=hvc1.1.1.L123.B0') === 'probably';\n }\n}\n","\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/ja';\nimport isBetween from 'dayjs/plugin/isBetween';\nimport isSameOrAfter from 'dayjs/plugin/isSameOrAfter';\nimport isSameOrBefore from 'dayjs/plugin/isSameOrBefore'\n\nimport { IProgram } from '@/interface';\nimport Utils from './Utils';\n\n/**\n * 番組情報周りのユーティリティ\n */\nexport class ProgramUtils {\n\n /**\n * 番組情報中の[字]や[解]などの記号をいい感じに装飾する\n * @param program 番組情報のオブジェクト\n * @param key 番組情報のオブジェクトから取り出すプロパティのキー\n * @returns 装飾した文字列\n */\n static decorateProgramInfo(program: IProgram | null, key: string): string {\n\n // program が空でないかつ、program[key] が存在する\n if (program !== null && program[key] !== null) {\n\n // 番組情報に含まれる HTML の特殊文字で表示がバグらないように、事前に HTML エスケープしておく\n const text = Utils.escapeHTML(program[key]);\n\n // 本来 ARIB 外字である記号の一覧\n // ref: https://ja.wikipedia.org/wiki/%E7%95%AA%E7%B5%84%E8%A1%A8\n // ref: https://github.com/xtne6f/EDCB/blob/work-plus-s/EpgDataCap3/EpgDataCap3/ARIB8CharDecode.cpp#L1319\n const mark = '新|終|再|交|映|手|声|多|副|字|文|CC|OP|二|S|B|SS|無|無料' +\n 'C|S1|S2|S3|MV|双|デ|D|N|W|P|H|HV|SD|天|解|料|前|後初|生|販|吹|PPV|' +\n '演|移|他|収|・|英|韓|中|字/日|字/日英|3D|2K|4K|8K|5.1|7.1|22.2|60P|120P|d|HC|HDR|SHV|UHD|VOD|配|初';\n\n // 正規表現を作成\n const pattern1 = new RegExp(`\\\\((二|字|再)\\\\)`, 'g'); // 通常の括弧で囲まれている記号\n const pattern2 = new RegExp(`\\\\[(${mark})\\\\]`, 'g');\n\n // 正規表現で置換した結果を返す\n return text.replace(pattern1, '$1')\n .replace(pattern2, '$1');\n\n // 番組情報がない時間帯\n } else {\n\n dayjs.extend(isSameOrAfter);\n dayjs.extend(isSameOrBefore);\n dayjs.extend(isBetween);\n\n // 23時~翌7時 (0:00 ~ 06:59 or 23:00 ~ 23:59) の間なら放送を休止している可能性が高いので、放送休止と表示する\n const now = dayjs();\n const pause_time_start = dayjs().hour(0).minute(0).second(0);\n const pause_time_end = dayjs().hour(6).minute(59).second(59);\n const pause_time_start_23 = dayjs().hour(23).minute(0).second(0);\n const pause_time_end_23 = dayjs().hour(23).minute(59).second(59);\n if ((now.isSameOrAfter(pause_time_start) && now.isSameOrBefore(pause_time_end)) ||\n (now.isSameOrAfter(pause_time_start_23) && now.isSameOrBefore(pause_time_end_23))) {\n if (key === 'title') {\n return '放送休止'; // タイトル\n } else {\n return 'この時間は放送を休止しています。'; // 番組概要\n }\n\n // それ以外の時間帯では、「番組情報がありません」と表示する\n // 急な番組変更の影響で、一時的にその時間帯に対応する番組情報が消えることがある\n // 特に Mirakurun バックエンドでは高頻度で収集した EIT[p/f] が比較的すぐ反映されるため、この現象が起こりやすい\n // 日中に放送休止(停波)になることはまずあり得ないので、番組情報が取得できてないだけで視聴できるかも?というニュアンスを与える\n } else {\n if (key === 'title') {\n return '番組情報がありません'; // タイトル\n } else {\n return 'この時間の番組情報を取得できませんでした。'; // 番組概要\n }\n }\n }\n }\n\n\n /**\n * オブジェクトからプロパティを取得し、もしプロパティが存在しなければ代替値を返す\n * @param items 対象のオブジェクト\n * @param key オブジェクトから取り出すプロパティのキー\n * @param default_value 取得できなかった際の代替値\n * @returns オブジェクト取得した値 or 代替値\n */\n static getAttribute(items: {[key: string]: any}, key: string, default_value: any): any {\n\n // items が空でないかつ、items[key] が存在する\n if (items !== null && items[key] !== undefined && items[key] !== null) {\n\n // items[key] の内容を返す\n return items[key];\n\n // 指定された代替値を返す\n } else {\n return default_value;\n }\n }\n\n\n /**\n * 番組の進捗状況を取得する\n * @param program 番組情報\n * @returns 番組の進捗状況(%単位)\n */\n static getProgramProgress(program: IProgram): number {\n\n // program が空でない\n if (program !== null) {\n\n // 番組開始時刻から何秒進んだか\n const progress = dayjs(dayjs()).diff(program.start_time, 'second');\n\n // %単位の割合を算出して返す\n return progress / program.duration * 100;\n\n // 放送休止中\n } else {\n return 0;\n }\n }\n\n\n /**\n * 番組の放送時刻を取得する\n * @param program 番組情報\n * @param is_short 時刻のみ返すかどうか\n * @returns 番組の放送時刻\n */\n static getProgramTime(program: IProgram, is_short: boolean = false): string {\n\n // program が空でなく、かつ番組時刻が初期値でない\n if (program !== null && program.start_time !== '2000-01-01T00:00:00+09:00') {\n\n // dayjs で日付を扱いやすく\n dayjs.locale('ja'); // ロケールを日本に設定\n const start_time = dayjs(program.start_time);\n const end_time = dayjs(program.end_time);\n const duration = program.duration / 60; // 分換算\n\n // 時刻のみ返す\n if (is_short === true) { // 時刻のみ\n return `${start_time.format('HH:mm')} ~ ${end_time.format('HH:mm')}`;\n // 通常\n } else {\n return `${start_time.format('YYYY/MM/DD (dd) HH:mm')} ~ ${end_time.format('HH:mm')} (${duration}分)`;\n }\n\n // 放送休止中\n } else {\n\n // 時刻のみ返す\n if (is_short === true) {\n return '--:-- ~ --:--';\n // 通常\n } else {\n return '----/--/-- (-) --:-- ~ --:-- (--分)';\n }\n }\n }\n}\n","\n// 共通ユーティリティをデフォルトとしてインポート\nimport Utils from '@/utils/Utils';\nexport default Utils;\n\n// Utils フォルダ配下のユーティリティを一括でインポートできるように\nexport * from '@/utils/ChannelUtils';\nexport * from '@/utils/PlayerCaptureHandler';\nexport * from '@/utils/PlayerUtils';\nexport * from '@/utils/ProgramUtils';\n","\nimport Utils from '@/utils';\nimport axios from 'axios'\n\n// ref: https://note.com/quoizunda/n/nb62e13e73499\n\n// Axios のインスタンスを作成\nconst axios_instance = axios.create();\n\n// HTTP リクエスト前に割り込んで行われる処理\naxios_instance.interceptors.request.use(config => {\n\n // API のベース URL を設定\n // BaseURL が明示的に指定されているときは設定しない\n if (config.baseURL === undefined) {\n config.baseURL = Utils.api_base_url;\n }\n\n // アクセストークンが取得できたら(=ログインされていれば)\n // 取得したアクセストークンを Authorization ヘッダーに Bearer トークンとしてセット\n // これを忘れると(当然ながら)ログインしていない扱いになる\n const access_token = Utils.getAccessToken();\n if (access_token !== null) {\n config.headers['Authorization'] = `Bearer ${access_token}`;\n }\n\n // KonomiTV クライアントのバージョンを設定\n // 今のところ使わないが、将来的にクライアントとサーバーを分離することを見据えて念のため\n config.headers['X-KonomiTV-Version'] = Utils.version;\n\n return config;\n})\n\n// ここで返したインスタンスを VueAxios (Vue.axios) に設定する\nexport default axios_instance;\n","\nimport Vue from 'vue';\nimport Vuetify from 'vuetify/lib/framework';\nimport { VSnackbar, VBtn, VIcon } from 'vuetify/lib';\n\nVue.use(Vuetify);\n\n// vuetify-message-snackbar を使うのに必要\nVue.component('v-snackbar', VSnackbar);\nVue.component('v-btn', VBtn);\nVue.component('v-icon', VIcon);\n\nexport default new Vuetify({\n theme: {\n dark: true,\n themes: {\n dark: {\n primary: '#E64F97',\n secondary: '#E33157',\n twitter: {\n base: '#4F82E6',\n lighten1: '#799FEC',\n lighten2: '#41A5F1',\n },\n gray: '#66514C',\n black: '#110A09',\n background: {\n base: '#1E1310',\n lighten1: '#2F221F',\n lighten2: '#433532',\n lighten3: '#4c3c38',\n },\n text: {\n base: '#FFEAEA',\n darken1: '#D9C7C7',\n darken2: '#8E7F7E',\n darken3: '#786968',\n }\n }\n },\n options: {\n customProperties: true,\n },\n },\n});\n","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"channels-container channels-container--home\",class:{'channels-container--loading': _vm.is_loading}},[_c('v-tabs-fix',{staticClass:\"channels-tab\",attrs:{\"centered\":\"\"},model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type,]){return _c('v-tab',{key:channels_type,staticClass:\"channels-tab__item\"},[_vm._v(_vm._s(channels_type))])}),1),_c('v-tabs-items-fix',{staticClass:\"channels-list\",model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type, channels]){return _c('v-tab-item-fix',{key:channels_type,staticClass:\"channels-tabitem\"},[_c('div',{staticClass:\"channels\",class:`channels--tab-${channels_type} channels--length-${channels.length}`},[_vm._l((channels),function(channel){return _c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:channel.id,staticClass:\"channel\",attrs:{\"to\":`/tv/watch/${channel.channel_id}`}},[_c('div',{staticClass:\"channel__broadcaster\"},[_c('img',{staticClass:\"channel__broadcaster-icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${channel.channel_id}/logo`}}),_c('div',{staticClass:\"channel__broadcaster-content\"},[_c('span',{staticClass:\"channel__broadcaster-name\"},[_vm._v(\"Ch: \"+_vm._s(channel.channel_number)+\" \"+_vm._s(channel.channel_name))]),_c('div',{staticClass:\"channel__broadcaster-status\"},[_c('div',{staticClass:\"channel__broadcaster-status-force\",class:`channel__broadcaster-status-force--${_vm.ChannelUtils.getChannelForceType(channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"12px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"勢い:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(channel, 'channel_force', '--')))]),_c('span',{staticStyle:{\"margin-left\":\"3px\"}},[_vm._v(\" コメ/分\")])],1),_c('div',{staticClass:\"channel__broadcaster-status-viewers ml-4\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:eye\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"視聴数:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(channel.viewers))])],1)])]),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip\",value:(_vm.isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'),expression:\"isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'\"}],staticClass:\"channel__broadcaster-pin\",class:{'channel__broadcaster-pin--pinned': _vm.isPinnedChannel(channel.channel_id)},on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.isPinnedChannel(channel.channel_id) ? _vm.removePinnedChannel(channel.channel_id) : _vm.addPinnedChannel(channel.channel_id)},\"mousedown\":function($event){$event.preventDefault();$event.stopPropagation();/* 親要素の波紋が広がらないように */}}},[_c('Icon',{attrs:{\"icon\":\"fluent:pin-20-filled\",\"width\":\"24px\"}})],1)]),_c('div',{staticClass:\"channel__program-present\"},[_c('div',{staticClass:\"channel__program-present-title-wrapper\"},[_c('span',{staticClass:\"channel__program-present-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'title'))}}),_c('span',{staticClass:\"channel__program-present-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_present)))])]),_c('span',{staticClass:\"channel__program-present-description\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'description'))}})]),_c('v-spacer'),_c('div',{staticClass:\"channel__program-following\"},[_c('div',{staticClass:\"channel__program-following-title\"},[_c('span',{staticClass:\"channel__program-following-title-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"channel__program-following-title-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}}),_c('span',{staticClass:\"channel__program-following-title-text\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_following, 'title'))}})],1),_c('span',{staticClass:\"channel__program-following-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_following)))])]),_c('div',{staticClass:\"channel__progressbar\"},[_c('div',{staticClass:\"channel__progressbar-progress\",style:(`width:${_vm.ProgramUtils.getProgramProgress(channel.program_present)}%;`)})])],1)}),(channels_type === 'ピン留め' && channels.length === 0)?_c('div',{staticClass:\"pinned-container d-flex justify-center align-center w-100\"},[_c('div',{staticClass:\"d-flex justify-center align-center flex-column\"},[_c('h2',[_vm._v(\"ピン留めされているチャンネルがありません。\")]),_c('div',{staticClass:\"mt-4 text--text text--darken-1\"},[_vm._v(\"各チャンネルの \"),_c('Icon',{staticStyle:{\"position\":\"relative\",\"bottom\":\"-5px\"},attrs:{\"icon\":\"fluent:pin-20-filled\",\"width\":\"22px\"}}),_vm._v(\" アイコンから、よくみるチャンネルをこのタブにピン留めできます。\")],1),_c('div',{staticClass:\"mt-2 text--text text--darken-1\"},[_vm._v(\"チャンネルをピン留めすると、このタブが最初に表示されます。\")])])]):_vm._e()],2)])}),1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('header',{staticClass:\"header\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"konomitv-logo ml-3 ml-md-6\",attrs:{\"to\":\"/tv/\"}},[_c('img',{staticClass:\"konomitv-logo__image\",attrs:{\"src\":\"/assets/images/logo.svg\",\"height\":\"21\"}})]),_c('v-spacer')],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./Header.vue?vue&type=template&id=506af489&scoped=true&\"\nvar script = {}\nimport style0 from \"./Header.vue?vue&type=style&index=0&id=506af489&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"506af489\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"navigation-container elevation-8\"},[_c('nav',{staticClass:\"navigation\"},[_c('div',{staticClass:\"navigation-scroll\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/tv/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:tv-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"テレビをみる\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/videos/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:movies-and-tv-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"ビデオをみる\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/timetable/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:calendar-ltr-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"番組表\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/captures/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:image-multiple-24-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"キャプチャ\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/watchlists/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"ic:round-playlist-play\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"ウォッチリスト\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/histories/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:history-16-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"視聴履歴\")])],1),_c('v-spacer'),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/settings/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:settings-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"設定\")])],1),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.top\",value:(_vm.is_update_available ? `アップデートがあります (version ${_vm.latest_version})` : ''),expression:\"is_update_available ? `アップデートがあります (version ${latest_version})` : ''\",modifiers:{\"top\":true}}],staticClass:\"navigation__link\",class:{'navigation__link--version': _vm.Utils.version.includes('-dev')},attrs:{\"active-class\":\"navigation__link--active\",\"href\":\"https://github.com/tsukumijima/KonomiTV\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",class:{'navigation__link-icon--highlight': _vm.is_update_available},attrs:{\"icon\":\"fluent:info-16-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"version \"+_vm._s(_vm.Utils.version))])],1)],1)])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport { IVersionInformation } from '@/interface';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Navigation',\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // 最新のバージョン\n latest_version: '' as string,\n\n // アップデートが利用可能か\n is_update_available: false as boolean,\n }\n },\n async created() {\n try {\n\n // バージョン情報を取得\n const version_info: IVersionInformation = (await Vue.axios.get(`/version`)).data;\n this.latest_version = version_info.latest_version;\n\n // もし現在のサーバーバージョン (-dev を除く) と最新のサーバーバージョンが異なるなら、アップデートが利用できる旨を表示する\n // 現在のサーバーバージョンが -dev 付きで、かつ最新のサーバーバージョンが -dev なし の場合 (リリース版がリリースされたとき) も同様に表示する\n // つまり開発版だと同じバージョンのリリース版がリリースされたときにしかアップデート通知が表示されない事になるが、ひとまずこれで…\n if ((version_info.version.includes('-dev') === false && version_info.version !== version_info.latest_version) ||\n (version_info.version.includes('-dev') === true && version_info.version.replace('-dev', '') === version_info.latest_version)) {\n this.is_update_available = true;\n }\n\n } catch (error) {\n throw new Error(error); // エラー内容をコンソールに表示して終了\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Navigation.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Navigation.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Navigation.vue?vue&type=template&id=3c027344&scoped=true&\"\nimport script from \"./Navigation.vue?vue&type=script&lang=ts&\"\nexport * from \"./Navigation.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Navigation.vue?vue&type=style&index=0&id=3c027344&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3c027344\",\n null\n \n)\n\nexport default component.exports","\n\nimport Vue from 'vue';\n\nimport { ChannelTypePretty, IChannel } from '@/interface';\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'TV-Home',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n\n // タブの状態管理\n tab: null as number | null,\n\n // ローディング中かどうか\n is_loading: true,\n\n // インターバル ID\n // ページ遷移時に setInterval(), setTimeout() の実行を止めるのに使う\n // setInterval(), setTimeout() の返り値を登録する\n interval_ids: [] as number[],\n\n // チャンネル情報リスト\n channels_list: new Map() as Map,\n\n // ピン留めしているチャンネルの ID (ex: gr011) が入るリスト\n pinned_channel_ids: [] as string[],\n }\n },\n // 開始時に実行\n created() {\n\n // チャンネル情報を取得\n this.update();\n\n // 00秒までの残り秒数\n // 現在 16:01:34 なら 26 (秒) になる\n const residue_second = 60 - (Math.floor(new Date().getTime() / 1000) % 60);\n\n // 00秒になるまで待ってから\n // 番組は基本1分単位で組まれているため、20秒や45秒など中途半端な秒数で更新してしまうと反映が遅れてしまう\n this.interval_ids.push(window.setTimeout(() => {\n\n // チャンネル情報を更新\n this.update();\n\n // チャンネル情報を定期的に更新\n this.interval_ids.push(window.setInterval(() => {\n this.update();\n }, 30 * 1000)); // 30秒おき\n\n }, residue_second * 1000));\n },\n // 終了前に実行\n beforeDestroy() {\n\n // clearInterval() ですべての setInterval(), setTimeout() の実行を止める\n // clearInterval() と clearTimeout() は中身共通なので問題ない\n for (const interval_id of this.interval_ids) {\n window.clearInterval(interval_id);\n }\n },\n methods: {\n\n // チャンネル情報一覧を取得し、画面を更新する\n async update() {\n\n // チャンネル情報一覧 API にアクセス\n let channels_response;\n try {\n channels_response = await Vue.axios.get('/channels');\n } catch (error) {\n console.error(error); // エラー内容を表示\n return;\n }\n\n // is_display が true のチャンネルのみに絞り込むフィルタ関数\n // 放送していないサブチャンネルを表示から除外する\n const filter = (channel: IChannel) => {\n return channel.is_display;\n }\n\n // チャンネルリストを再構築\n // 1つでもチャンネルが存在するチャンネルタイプのみ表示するように\n // たとえば SKY (スカパー!プレミアムサービス) のタブは SKY に属すチャンネルが1つもない(=受信できない)なら表示されない\n this.channels_list = new Map();\n if (channels_response.data.GR.length > 0) this.channels_list.set('地デジ', channels_response.data.GR.filter(filter));\n if (channels_response.data.BS.length > 0) this.channels_list.set('BS', channels_response.data.BS.filter(filter));\n if (channels_response.data.CS.length > 0) this.channels_list.set('CS', channels_response.data.CS.filter(filter));\n if (channels_response.data.CATV.length > 0) this.channels_list.set('CATV', channels_response.data.CATV.filter(filter));\n if (channels_response.data.SKY.length > 0) this.channels_list.set('SKY', channels_response.data.SKY.filter(filter));\n if (channels_response.data.STARDIGIO.length > 0) this.channels_list.set('StarDigio', channels_response.data.STARDIGIO.filter(filter));\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList(this.is_loading ? true : false);\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n\n // チャンネルをピン留めする\n addPinnedChannel(channel_id: string) {\n\n // 現在ピン留めされているチャンネルを取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めするチャンネルの ID を追加\n this.pinned_channel_ids.push(channel_id);\n\n // 設定を保存\n Utils.setSettingsItem('pinned_channel_ids', this.pinned_channel_ids);\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList();\n },\n\n // チャンネルをピン留めから外す\n removePinnedChannel(channel_id: string) {\n\n // 現在ピン留めされているチャンネルを取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めを外すチャンネルの ID を削除\n this.pinned_channel_ids.splice(this.pinned_channel_ids.indexOf(channel_id), 1);\n\n // 設定を保存\n Utils.setSettingsItem('pinned_channel_ids', this.pinned_channel_ids);\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList();\n },\n\n // ピン留めされているチャンネルのリストを更新する\n updatePinnedChannelList(is_update_tab: boolean = true) {\n\n // ピン留めされているチャンネルの ID を取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めされているチャンネル情報のリスト\n const pinned_channels = [] as IChannel[];\n\n // チャンネル ID が一致したチャンネルの情報を保存する\n for (const pinned_channel_id of this.pinned_channel_ids) {\n const pinned_channel_type = ChannelUtils.getChannelType(pinned_channel_id, true) as ChannelTypePretty;\n const pinned_channel = this.channels_list.get(pinned_channel_type).find((channel) => {\n return channel.channel_id === pinned_channel_id; // チャンネル ID がピン留めされているチャンネルのものと同じ\n });\n // チャンネル情報を取得できているときだけ\n // サブチャンネルをピン留めしたが、マルチ編成が終了して現在は放送していない場合などに備える (BS142 など)\n // 現在放送していないチャンネルは this.channels_list に入れた段階で弾いているため、チャンネル情報を取得できない\n if (pinned_channel !== undefined) {\n pinned_channels.push(pinned_channel);\n }\n }\n\n if (!this.channels_list.has('ピン留め')) {\n // タブの一番左にピン留めタブを表示する\n this.channels_list = new Map([['ピン留め', pinned_channels], ...this.channels_list]);\n } else {\n // 既に存在するピン留めタブにチャンネル情報を設定する\n this.channels_list.set('ピン留め', pinned_channels);\n }\n\n // pinned_channels が空の場合は、タブを地デジタブに変更\n // ピン留めができる事を示唆するためにピン留めタブ自体は残す\n if (pinned_channels.length === 0 && is_update_tab === true) {\n this.tab = 1;\n }\n },\n\n // チャンネルがピン留めされているか\n isPinnedChannel(channel_id: string): boolean {\n\n // 引数のチャンネルがピン留めリストに存在するかを返す\n return this.pinned_channel_ids.includes(channel_id);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Home.vue?vue&type=template&id=189c71d3&scoped=true&\"\nimport script from \"./Home.vue?vue&type=script&lang=ts&\"\nexport * from \"./Home.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Home.vue?vue&type=style&index=0&id=189c71d3&prod&lang=scss&\"\nimport style1 from \"./Home.vue?vue&type=style&index=1&id=189c71d3&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"189c71d3\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('main',{staticClass:\"watch-container\",class:{\n 'watch-container--control-display': _vm.is_control_display,\n 'watch-container--panel-display': _vm.is_panel_display,\n 'watch-container--fullscreen': _vm.is_fullscreen,\n }},[_c('nav',{staticClass:\"watch-navigation\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"watch-navigation__icon\",attrs:{\"to\":\"/tv/\"}},[_c('img',{staticClass:\"watch-navigation__icon-image\",attrs:{\"src\":\"/assets/images/icon.svg\",\"width\":\"23px\"}})]),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('テレビをみる'),expression:\"'テレビをみる'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/tv/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:tv-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('ビデオをみる'),expression:\"'ビデオをみる'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/videos/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:movies-and-tv-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('番組表'),expression:\"'番組表'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/timetable/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:calendar-ltr-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('キャプチャ'),expression:\"'キャプチャ'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/captures/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:image-multiple-24-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('ウォッチリスト'),expression:\"'ウォッチリスト'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/watchlists/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"ic:round-playlist-play\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('視聴履歴'),expression:\"'視聴履歴'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/histories/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:history-16-regular\",\"width\":\"26px\"}})],1),_c('v-spacer'),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('設定'),expression:\"'設定'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/settings/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:settings-20-regular\",\"width\":\"26px\"}})],1),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:(`version ${_vm.Utils.version}`),expression:\"`version ${Utils.version}`\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"href\":\"https://github.com/tsukumijima/KonomiTV\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:info-16-regular\",\"width\":\"26px\"}})],1)],1),_c('div',{staticClass:\"watch-content\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event, true)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event, true)},\"click\":function($event){return _vm.controlDisplayTimer($event, true)}}},[_c('header',{staticClass:\"watch-header\"},[_c('img',{staticClass:\"watch-header__broadcaster\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('span',{staticClass:\"watch-header__program-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'title'))}}),_c('span',{staticClass:\"watch-header__program-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_present, true)))]),_c('v-spacer'),_c('span',{staticClass:\"watch-header__now\"},[_vm._v(_vm._s(_vm.time))])],1),_c('div',{staticClass:\"watch-player\",class:{\n 'watch-player--loading': _vm.is_loading,\n 'watch-player--virtual-keyboard-display': _vm.is_virtual_keyboard_display && _vm.Utils.hasActiveElementClass('dplayer-comment-input'),\n }},[_c('div',{staticClass:\"watch-player__background\",class:{'watch-player__background--display': _vm.is_background_display},style:({backgroundImage: `url(${_vm.background_url})`})},[_c('img',{staticClass:\"watch-player__background-logo\",attrs:{\"src\":\"/assets/images/logo.svg\"}})]),_c('v-progress-circular',{staticClass:\"watch-player__buffering\",class:{'watch-player__buffering--display': _vm.is_video_buffering && (_vm.is_loading || (_vm.player !== null && !_vm.player.video.paused))},attrs:{\"indeterminate\":\"\",\"size\":\"60\",\"width\":\"6\"}}),_c('div',{staticClass:\"watch-player__dplayer\"}),_c('div',{staticClass:\"watch-player__button\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.top\",value:('前のチャンネル'),expression:\"'前のチャンネル'\",modifiers:{\"top\":true}}],staticClass:\"switch-button switch-button-up\",attrs:{\"to\":`/tv/watch/${_vm.channel_previous.channel_id}`}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:ios-arrow-left-24-filled\",\"width\":\"32px\",\"rotate\":\"1\"}})],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"switch-button switch-button-panel switch-button-panel--open\",on:{\"click\":function($event){_vm.is_panel_display = !_vm.is_panel_display}}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:navigation-16-filled\",\"width\":\"32px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.bottom\",value:('次のチャンネル'),expression:\"'次のチャンネル'\",modifiers:{\"bottom\":true}}],staticClass:\"switch-button switch-button-down\",attrs:{\"to\":`/tv/watch/${_vm.channel_next.channel_id}`}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:ios-arrow-right-24-filled\",\"width\":\"33px\",\"rotate\":\"1\"}})],1)],1)],1)]),_c('div',{staticClass:\"watch-panel\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('div',{staticClass:\"watch-panel__header\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-close-button\",on:{\"click\":function($event){_vm.is_panel_display = false}}},[_c('Icon',{staticClass:\"panel-close-button__icon\",attrs:{\"icon\":\"akar-icons:chevron-right\",\"width\":\"25px\"}}),_c('span',{staticClass:\"panel-close-button__text\"},[_vm._v(\"閉じる\")])],1),_c('v-spacer'),_c('div',{staticClass:\"panel-broadcaster\"},[_c('img',{staticClass:\"panel-broadcaster__icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('div',{staticClass:\"panel-broadcaster__number\"},[_vm._v(_vm._s(_vm.channel.channel_number))]),_c('div',{staticClass:\"panel-broadcaster__name\"},[_vm._v(_vm._s(_vm.channel.channel_name))])])],1),_c('div',{staticClass:\"watch-panel__content-container\"},[_c('Program',{staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Program'},attrs:{\"channel\":_vm.channel}}),_c('Channel',{staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Channel'},attrs:{\"channels_list\":_vm.channels_list}}),_c('Comment',{ref:\"Comment\",staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Comment'},attrs:{\"channel\":_vm.channel,\"player\":_vm.player}}),_c('Twitter',{ref:\"Twitter\",staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Twitter'},attrs:{\"channel\":_vm.channel,\"player\":_vm.player,\"is_virtual_keyboard_display\":_vm.is_virtual_keyboard_display},on:{\"panel_folding_requested\":function($event){_vm.is_panel_display = false}}})],1),_c('div',{staticClass:\"watch-panel__navigation\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Program'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Program'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-solid:info-circle\",\"width\":\"33px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"番組情報\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Channel'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Channel'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-solid:broadcast-tower\",\"width\":\"34px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"チャンネル\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Comment'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Comment'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"29px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"コメント\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Twitter'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Twitter'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"34px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"Twitter\")])],1)])])]),_c('v-dialog',{attrs:{\"max-width\":\"1000\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.shortcut_key_modal),callback:function ($$v) {_vm.shortcut_key_modal=$$v},expression:\"shortcut_key_modal\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"px-5 pt-4 pb-3 d-flex align-center font-weight-bold\"},[_c('Icon',{attrs:{\"icon\":\"fluent:keyboard-20-filled\",\"height\":\"28px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"キーボードショートカット\")]),_c('v-spacer'),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"d-flex align-center rounded-circle cursor-pointer px-2 py-2\",on:{\"click\":function($event){_vm.shortcut_key_modal = false}}},[_c('Icon',{attrs:{\"icon\":\"fluent:dismiss-12-filled\",\"width\":\"23px\",\"height\":\"23px\"}})],1)],1),_c('div',{staticClass:\"px-5 pb-4\"},[_c('v-row',_vm._l((_vm.shortcut_key_list),function(shortcut_key_column,shortcut_key_column_name){return _c('v-col',{key:shortcut_key_column_name,attrs:{\"cols\":\"6\"}},_vm._l((shortcut_key_column),function(shortcut_keys){return _c('div',{key:shortcut_keys.name,staticClass:\"mt-3\"},[_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold\"},[_c('Icon',{attrs:{\"icon\":shortcut_keys.icon,\"height\":shortcut_keys.icon_height}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(_vm._s(shortcut_keys.name))])],1),_vm._l((shortcut_keys.shortcuts),function(shortcut){return _c('div',{key:shortcut.name,staticClass:\"mt-3\"},[_c('div',{staticClass:\"text-subtitle-2 mt-2 d-flex align-center font-weight-medium\"},[_c('span',{staticClass:\"mr-2\",domProps:{\"innerHTML\":_vm._s(shortcut.name)}}),_c('div',{staticClass:\"ml-auto d-flex align-center flex-shrink-0\"},_vm._l((shortcut.keys),function(key,index){return _c('div',{key:key.name,staticClass:\"ml-auto d-flex align-center\"},[_c('span',{staticClass:\"shortcut-key\"},[_vm._l((key.name.split(';')),function(key_name){return _c('Icon',{directives:[{name:\"show\",rawName:\"v-show\",value:(key.icon === true),expression:\"key.icon === true\"}],key:key_name,attrs:{\"icon\":key_name,\"height\":\"18px\"}})}),(key.icon === false)?_c('span',{domProps:{\"innerHTML\":_vm._s(key.name)}}):_vm._e()],2),(index < (shortcut.keys.length - 1))?_c('span',{staticClass:\"shortcut-key-plus\"},[_vm._v(\"+\")]):_vm._e()])}),0)])])})],2)}),0)}),1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n// 番組情報を表すインターフェイス\nexport interface IProgram {\n id: string;\n network_id: number;\n service_id: number;\n event_id: number;\n channel_id: string;\n title: string;\n description: string;\n detail: {[key: string]: string};\n start_time: string;\n end_time: string;\n duration: number;\n is_free: boolean;\n genre: {major: string; middle: string}[];\n video_type: string;\n video_codec: string;\n video_resolution: string;\n primary_audio_type: string;\n primary_audio_language: string;\n primary_audio_sampling_rate: string;\n secondary_audio_type: string | null;\n secondary_audio_language: string | null;\n secondary_audio_sampling_rate: string | null;\n}\n\n// 番組情報を表すインターフェイスのデフォルト値\nexport const IProgramDefault: IProgram = {\n id: 'NID0-SID0',\n network_id: 0,\n service_id: 0,\n event_id: 0,\n channel_id: 'gr000',\n title: '取得中…',\n description: '取得中…',\n detail: {},\n start_time: '2000-01-01T00:00:00+09:00',\n end_time: '2000-01-01T00:00:00+09:00',\n duration: 0,\n is_free: true,\n genre: [],\n video_type: '映像1080i(1125i)、アスペクト比16:9 パンベクトルなし',\n video_codec: 'mpeg2',\n video_resolution: '1080i',\n primary_audio_type: '2/0モード(ステレオ)',\n primary_audio_language: '日本語',\n primary_audio_sampling_rate: '48kHz',\n secondary_audio_type: null,\n secondary_audio_language: null,\n secondary_audio_sampling_rate: null,\n}\n\n// チャンネルタイプの型\nexport type ChannelType = 'GR' | 'BS' | 'CS' | 'CATV' | 'SKY' | 'STARDIGIO';\n\n// チャンネルタイプの型 (実際のチャンネルリストに表示される表現)\nexport type ChannelTypePretty = 'ピン留め' | '地デジ' | 'BS' | 'CS' | 'CATV' | 'SKY' | 'StarDigio';\n\n// チャンネル情報を表すインターフェイス\nexport interface IChannel {\n id: string;\n network_id: number;\n service_id: number;\n transport_stream_id: number | null;\n remocon_id: number | null;\n channel_id: string;\n channel_number: string;\n channel_name: string;\n channel_type: ChannelType;\n channel_force: number | null;\n channel_comment: number | null;\n is_subchannel: boolean;\n is_radiochannel: boolean;\n is_display: boolean;\n viewers: number;\n program_present: IProgram;\n program_following: IProgram;\n}\n\n// チャンネル情報を表すインターフェイスのデフォルト値\nexport const IChannelDefault: IChannel = {\n id: 'NID0-SID0',\n network_id: 0,\n service_id: 0,\n transport_stream_id: null,\n remocon_id: null,\n channel_id: 'gr000',\n channel_number: '---',\n channel_name: '取得中…',\n channel_type: 'GR',\n channel_force: null,\n channel_comment: null,\n is_subchannel: false,\n is_radiochannel: false,\n is_display: true,\n viewers: 0,\n program_present: IProgramDefault,\n program_following: IProgramDefault,\n}\n\n// ユーザーアカウントに紐づく Twitter アカウントの情報を表すインターフェイス\nexport interface ITwitterAccount {\n id: number;\n name: string;\n screen_name: string;\n icon_url: string;\n created_at: string;\n updated_at: string;\n}\n\n// ユーザーアカウントの情報を表すインターフェイス\nexport interface IUser {\n id: number;\n name: string;\n is_admin: boolean;\n niconico_user_id: number | null;\n niconico_user_name: string | null;\n niconico_user_premium: boolean | null;\n twitter_accounts: ITwitterAccount[];\n created_at: string;\n updated_at: string;\n}\n\n// バージョン情報を表すインターフェイス\nexport interface IVersionInformation {\n version: string;\n latest_version: string;\n backend: 'EDCB' | 'Mirakurun';\n environment: 'Windows' | 'Linux' | 'Linux-Docker';\n}\n\n// DPlayer のコメントデータの型\n// KonomiTV で使うプロパティのみ定義している\n// ref: https://github.com/tsukumijima/DPlayer/blob/master/src/js/danmaku.js#L86-L96\nexport interface IDPlayerDanmaku {\n author: string;\n time: number;\n text: string;\n color: string;\n type: 'top' | 'right' | 'bottom';\n size: 'big' | 'medium' | 'small';\n}\n\n// コメントを送信する際に DPlayer から受け取るオプションの型\n// KonomiTV で使うプロパティのみ定義している\n// ref: https://github.com/tsukumijima/DPlayer/blob/master/src/js/danmaku.js#L98-L121\nexport interface IDPlayerDanmakuSendOptions {\n data: IDPlayerDanmaku;\n success: () => void;\n error: (message: string | undefined) => void;\n}\n\n// キャプチャに書き込む EXIF メタデータの型\nexport interface ICaptureExifData {\n captured_at: string;\n captured_playback_position: number;\n network_id: number;\n service_id: number;\n event_id: number;\n title: string;\n description: string;\n start_time: string;\n end_time: string;\n duration: number;\n caption_text: string | null;\n is_caption_composited: boolean;\n is_comment_composited: boolean;\n}\n\n// ミュート済みのコメントのキーワードが入るリスト\nexport interface IMutedCommentKeywords {\n match: 'partial' | 'forward' | 'backward' | 'exact' | 'regex';\n pattern: string;\n}\n","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"channels-container channels-container--watch\"},[_c('v-tabs-fix',{staticClass:\"channels-tab\",attrs:{\"centered\":\"\",\"show-arrows\":\"\"},model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type,]){return _c('v-tab',{key:channels_type,staticClass:\"channels-tab__item\"},[_vm._v(\" \"+_vm._s(channels_type)+\" \")])}),1),_c('div',{staticClass:\"channels-list-container\"},[_c('v-tabs-items-fix',{staticClass:\"channels-list\",model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type, channels]){return _c('v-tab-item-fix',{key:channels_type,staticClass:\"channels\"},_vm._l((channels),function(channel){return _c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:channel.id,staticClass:\"channel\",attrs:{\"to\":`/tv/watch/${channel.channel_id}`}},[_c('div',{staticClass:\"channel__broadcaster\"},[_c('img',{staticClass:\"channel__broadcaster-icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${channel.channel_id}/logo`}}),_c('div',{staticClass:\"channel__broadcaster-content\"},[_c('span',{staticClass:\"channel__broadcaster-name\"},[_vm._v(\"Ch: \"+_vm._s(channel.channel_number)+\" \"+_vm._s(channel.channel_name))]),_c('div',{staticClass:\"channel__broadcaster-force\",class:`channel__broadcaster-force--${_vm.ChannelUtils.getChannelForceType(channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"11px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(channel, 'channel_force', '-')))])],1)])]),_c('div',{staticClass:\"channel__program-present\"},[_c('span',{staticClass:\"channel__program-present-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'title'))}}),_c('span',{staticClass:\"channel__program-present-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_present)))])]),_c('div',{staticClass:\"channel__program-following\"},[_c('div',{staticClass:\"channel__program-following-title\"},[_c('span',{staticClass:\"channel__program-following-title-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"channel__program-following-title-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}}),_c('span',{staticClass:\"channel__program-following-title-text\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_following, 'title'))}})],1),_c('span',{staticClass:\"channel__program-following-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_following)))])]),_c('div',{staticClass:\"channel__progressbar\"},[_c('div',{staticClass:\"channel__progressbar-progress\",style:(`width:${_vm.ProgramUtils.getProgramProgress(channel.program_present)}%;`)})])])}),1)}),1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { ChannelTypePretty, IChannel } from '@/interface';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Panel-ChannelTab',\n props: {\n // チャンネル情報リスト\n channels_list: {\n type: Map as PropType>,\n required: true,\n }\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n\n // タブの状態管理\n tab: null,\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Channel.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Channel.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Channel.vue?vue&type=template&id=4380062e&scoped=true&\"\nimport script from \"./Channel.vue?vue&type=script&lang=ts&\"\nexport * from \"./Channel.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Channel.vue?vue&type=style&index=0&id=4380062e&prod&lang=scss&\"\nimport style1 from \"./Channel.vue?vue&type=style&index=1&id=4380062e&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4380062e\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"comment-container\"},[_c('section',{staticClass:\"comment-header\"},[_c('h2',{staticClass:\"comment-header__title\"},[_c('Icon',{staticClass:\"comment-header__title-icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"height\":\"18.5px\"}}),_c('span',{staticClass:\"comment-header__title-text\"},[_vm._v(\"コメント\")])],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"comment-header__button ml-auto\",on:{\"click\":function($event){_vm.comment_mute_settings_modal = !_vm.comment_mute_settings_modal}}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"11px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ミュート設定\")])],1)]),_c('section',{ref:\"comment_list_wrapper\",staticClass:\"comment-list-wrapper\"},[_c('div',{staticClass:\"comment-list-dropdown\",class:{'comment-list-dropdown--display': _vm.is_comment_list_dropdown_display},style:({'--comment-list-dropdown-top': `${_vm.comment_list_dropdown_top}px`})},[_c('v-list',{staticStyle:{\"background\":\"var(--v-background-lighten1)\"}},[_c('v-list-item',{staticStyle:{\"min-height\":\"30px\"},attrs:{\"dense\":\"\"},on:{\"click\":function($event){_vm.addMutedKeywords(_vm.comment_list_dropdown_comment.text); _vm.is_comment_list_dropdown_display = false}}},[_c('v-list-item-title',{staticClass:\"d-flex align-center\"},[_c('Icon',{attrs:{\"icon\":\"fluent:comment-dismiss-20-filled\",\"width\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"このコメントをミュート\")])],1)],1),_c('v-list-item',{staticStyle:{\"min-height\":\"30px\"},attrs:{\"dense\":\"\"},on:{\"click\":function($event){_vm.addMutedNiconicoUserIDs(_vm.comment_list_dropdown_comment.user_id); _vm.is_comment_list_dropdown_display = false}}},[_c('v-list-item-title',{staticClass:\"d-flex align-center\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-prohibited-20-filled\",\"width\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"このコメントの投稿者をミュート\")])],1)],1)],1)],1),_c('div',{staticClass:\"comment-list-cover\",class:{'comment-list-cover--display': _vm.is_comment_list_dropdown_display},on:{\"click\":function($event){_vm.is_comment_list_dropdown_display = false}}}),_c('DynamicScroller',{staticClass:\"comment-list\",attrs:{\"direction\":'vertical',\"items\":_vm.comment_list,\"min-item-size\":34},scopedSlots:_vm._u([{key:\"default\",fn:function({item, active}){return [_c('DynamicScrollerItem',{attrs:{\"item\":item,\"active\":active,\"size-dependencies\":[item.text]}},[_c('div',{staticClass:\"comment\",class:{'comment--my-post': item.my_post}},[_c('span',{staticClass:\"comment__text\"},[_vm._v(_vm._s(item.text))]),_c('span',{staticClass:\"comment__time\"},[_vm._v(_vm._s(item.time))]),_c('v-btn',{staticClass:\"comment__icon\",attrs:{\"icon\":\"\"},on:{\"click\":function($event){return _vm.displayCommentListDropdown($event, item)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:more-vertical-20-filled\",\"width\":\"20px\"}})],1)],1)])]}}])}),(_vm.initialize_failed_message === null && _vm.comment_list.length === 0)?_c('div',{staticClass:\"comment-announce\"},[_c('div',{staticClass:\"comment-announce__heading\"},[_vm._v(\"まだコメントがありません。\")]),_vm._m(0)]):_vm._e(),(_vm.initialize_failed_message !== null && _vm.comment_list.length === 0)?_c('div',{staticClass:\"comment-announce\"},[_c('div',{staticClass:\"comment-announce__heading\"},[_vm._v(\"コメントがありません。\")]),_c('div',{staticClass:\"comment-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(_vm._s(_vm.initialize_failed_message))])])]):_vm._e()],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"comment-scroll-button elevation-5\",class:{'comment-scroll-button--display': _vm.is_manual_scroll},on:{\"click\":function($event){_vm.is_manual_scroll = false; _vm.scrollCommentList(true);}}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-down-12-filled\",\"height\":\"29px\"}})],1),_c('CommentMuteSettings',{model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}})],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"comment-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(\"このチャンネルに対応するニコニコ実況のコメントが、リアルタイムで表示されます。\")])])\n}]\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('v-dialog',{attrs:{\"max-width\":\"740\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"px-5 pt-5 pb-3 d-flex align-center font-weight-bold\",staticStyle:{\"height\":\"60px\"}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"26px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"コメントのミュート設定\")]),_c('v-spacer'),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"d-flex align-center rounded-circle cursor-pointer px-2 py-2\",on:{\"click\":function($event){_vm.comment_mute_settings_modal = false}}},[_c('Icon',{attrs:{\"icon\":\"fluent:dismiss-12-filled\",\"width\":\"23px\",\"height\":\"23px\"}})],1)],1),_c('div',{staticClass:\"px-5 pb-5\"},[_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:comment-dismiss-20-filled\",\"width\":\"24px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"ミュート済みのキーワード\")]),_c('v-btn',{staticClass:\"ml-auto\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.muted_comment_keywords.push({id: Date.now(), match: 'partial', pattern: ''})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"height\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)],1),_c('div',{staticClass:\"muted-comment-items\"},_vm._l((_vm.muted_comment_keywords),function(muted_comment_keyword){return _c('div',{key:muted_comment_keyword.id,staticClass:\"muted-comment-item\"},[_c('v-text-field',{staticClass:\"muted-comment-item__input\",attrs:{\"type\":\"search\",\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"ミュートするキーワードを入力\"},model:{value:(muted_comment_keyword.pattern),callback:function ($$v) {_vm.$set(muted_comment_keyword, \"pattern\", $$v)},expression:\"muted_comment_keyword.pattern\"}}),_c('v-select',{staticClass:\"muted-comment-item__match-type\",attrs:{\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"items\":_vm.muted_comment_keyword_match_type},model:{value:(muted_comment_keyword.match),callback:function ($$v) {_vm.$set(muted_comment_keyword, \"match\", $$v)},expression:\"muted_comment_keyword.match\"}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"muted-comment-item__delete-button\",on:{\"click\":function($event){_vm.muted_comment_keywords.splice(_vm.muted_comment_keywords.indexOf(muted_comment_keyword), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"20px\"}})],1)],1)}),0),_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-prohibited-20-filled\",\"width\":\"24px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"ミュート済みのニコニコユーザー ID\")]),_c('v-btn',{staticClass:\"ml-auto\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.muted_niconico_user_ids.push({id: Date.now(), user_id: ''})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"height\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)],1),_c('div',{staticClass:\"muted-comment-items\"},_vm._l((_vm.muted_niconico_user_ids),function(muted_niconico_user_id){return _c('div',{key:muted_niconico_user_id.id,staticClass:\"muted-comment-item\"},[_c('v-text-field',{staticClass:\"muted-comment-item__input\",attrs:{\"type\":\"search\",\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"ミュートするニコニコユーザー ID を入力\"},model:{value:(muted_niconico_user_id.user_id),callback:function ($$v) {_vm.$set(muted_niconico_user_id, \"user_id\", $$v)},expression:\"muted_niconico_user_id.user_id\"}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"muted-comment-item__delete-button\",on:{\"click\":function($event){_vm.muted_niconico_user_ids.splice(_vm.muted_niconico_user_ids.indexOf(muted_niconico_user_id), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"20px\"}})],1)],1)}),0),_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"24px\",\"height\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"クイック設定\")])],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_vulgar_comments\"}},[_vm._v(\" 露骨な表現を含むコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_vulgar_comments\"}},[_vm._v(\" 性的な単語などの露骨・下品な表現を含むコメントを、一括でミュートするかを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_vulgar_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_vulgar_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_vulgar_comments\", $$v)},expression:\"settings.mute_vulgar_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_abusive_discriminatory_prejudiced_comments\"}},[_vm._v(\" 罵倒や誹謗中傷、差別的な表現、政治的に偏った表現を含むコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_abusive_discriminatory_prejudiced_comments\"}},[_vm._v(\" 『死ね』『殺す』などの罵倒や誹謗中傷、特定の国や人々への差別的な表現、政治的に偏った表現を含むコメントを、一括でミュートするかを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_abusive_discriminatory_prejudiced_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_abusive_discriminatory_prejudiced_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_abusive_discriminatory_prejudiced_comments\", $$v)},expression:\"settings.mute_abusive_discriminatory_prejudiced_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_big_size_comments\"}},[_vm._v(\" 文字サイズが大きいコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_big_size_comments\"}},[_vm._v(\" 通常より大きい文字サイズで表示されるコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" 文字サイズが大きいコメントには迷惑なコメントが多いです。基本的にはオンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_big_size_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_big_size_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_big_size_comments\", $$v)},expression:\"settings.mute_big_size_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_fixed_comments\"}},[_vm._v(\" 映像の上下に固定表示されるコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_fixed_comments\"}},[_vm._v(\" 映像の上下に固定された状態で表示されるコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" 固定表示されるコメントが煩わしいと感じる方は、オンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_fixed_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_fixed_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_fixed_comments\", $$v)},expression:\"settings.mute_fixed_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_colored_comments\"}},[_vm._v(\" 色付きのコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_colored_comments\"}},[_vm._v(\" 白以外の色で表示される色付きのコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" この設定をオンにしておくと、目立つ色のコメントを一掃できます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_colored_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_colored_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_colored_comments\", $$v)},expression:\"settings.mute_colored_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_consecutive_same_characters_comments\"}},[_vm._v(\" 8文字以上同じ文字が連続しているコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_consecutive_same_characters_comments\"}},[_vm._v(\" 『wwwwwwwwwww』『あばばばばばばばばば』など、8文字以上同じ文字が連続しているコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" しばしばあるテンプレコメントが煩わしいと感じる方は、オンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_consecutive_same_characters_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_consecutive_same_characters_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_consecutive_same_characters_comments\", $$v)},expression:\"settings.mute_consecutive_same_characters_comments\"}})],1)])],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { IMutedCommentKeywords } from '@/interface';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'CommentMuteSettings',\n // カスタム v-model を実装する\n // ref: https://jp.vuejs.org/v2/guide/components-custom-events.html\n model: {\n prop: 'showing', // v-model で渡された値が \"showing\" props に入る\n event: 'change' // \"change\" イベントで親コンポーネントに反映\n },\n props: {\n // コメントのミュート設定のモーダルを表示するか\n showing: {\n type: Boolean as PropType,\n required: true,\n }\n },\n data() {\n return {\n\n // インターバルのタイマー ID\n interval_timer_id: 0,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n\n // ミュート済みのキーワードが入るリスト\n muted_comment_keywords: (Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[]).map((keyword, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {\n id: Date.now() + index,\n match: keyword.match as ('partial' | 'forward' | 'backward' | 'exact' | 'regex'),\n pattern: keyword.pattern as string,\n };\n }),\n\n // ミュート済みのキーワードのマッチタイプ\n muted_comment_keyword_match_type: [\n {text: '部分一致', value: 'partial'},\n {text: '前方一致', value: 'forward'},\n {text: '後方一致', value: 'backward'},\n {text: '完全一致', value: 'exact'},\n {text: '正規表現', value: 'regex'},\n ],\n\n // ミュート済みのニコニコユーザー ID が入るリスト\n muted_niconico_user_ids: (Utils.getSettingsItem('muted_niconico_user_ids') as string[]).map((user_id, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {\n id: Date.now() + index,\n user_id: user_id,\n };\n }),\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {}\n const setting_keys = [\n 'mute_vulgar_comments',\n 'mute_abusive_discriminatory_prejudiced_comments',\n 'mute_big_size_comments',\n 'mute_fixed_comments',\n 'mute_colored_comments',\n 'mute_consecutive_same_characters_comments',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n created() {\n // 1秒に1回、muted_comment_keywords と muted_niconico_user_ids の変更内容を同期する\n // コメントリストからのミュート設定の変更を反映するために必要\n this.interval_timer_id = window.setInterval(() => {\n const new_muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n if (JSON.stringify(this.muted_comment_keywords) !== JSON.stringify(new_muted_comment_keywords)) {\n this.muted_comment_keywords = (new_muted_comment_keywords).map((keyword, index) => {\n return {\n id: Date.now() + index,\n match: keyword.match as ('partial' | 'forward' | 'backward' | 'exact' | 'regex'),\n pattern: keyword.pattern as string,\n };\n });\n }\n const new_muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n if (JSON.stringify(this.muted_niconico_user_ids) !== JSON.stringify(new_muted_niconico_user_ids)) {\n this.muted_niconico_user_ids = (new_muted_niconico_user_ids).map((user_id, index) => {\n return {\n id: Date.now() + index,\n user_id: user_id,\n };\n });\n }\n }, 1000);\n },\n beforeDestroy() {\n // インスタンスの破棄前にタイマーを解除する\n window.clearInterval(this.interval_timer_id);\n },\n watch: {\n\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n },\n\n // ミュート済みのキーワードが変更されたら随時 LocalStorage に保存する\n muted_comment_keywords: {\n deep: true,\n handler() {\n Utils.setSettingsItem('muted_comment_keywords', this.muted_comment_keywords.map((muted_comment_keyword) => {\n delete muted_comment_keyword.id;\n return muted_comment_keyword;\n }));\n }\n },\n\n // ミュート済みのニコニコユーザー ID が変更されたら随時 LocalStorage に保存する\n muted_niconico_user_ids: {\n deep: true,\n handler() {\n Utils.setSettingsItem('muted_niconico_user_ids', this.muted_niconico_user_ids.map((muted_niconico_user_id) => {\n return muted_niconico_user_id.user_id;\n }));\n }\n },\n\n // showing (親コンポーネント側) の変更を監視し、変更されたら comment_mute_settings_modal に反映する\n showing() {\n this.comment_mute_settings_modal = this.showing as boolean;\n },\n\n // comment_mute_settings_modal (子コンポーネント側) の変更を監視し、変更されたら this.$emit() で親コンポーネントに伝える\n comment_mute_settings_modal() {\n this.$emit('change', this.comment_mute_settings_modal);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CommentMuteSettings.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CommentMuteSettings.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./CommentMuteSettings.vue?vue&type=template&id=5d831536&scoped=true&\"\nimport script from \"./CommentMuteSettings.vue?vue&type=script&lang=ts&\"\nexport * from \"./CommentMuteSettings.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./CommentMuteSettings.vue?vue&type=style&index=0&id=5d831536&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"5d831536\",\n null\n \n)\n\nexport default component.exports","\n\nimport { AxiosResponse } from 'axios';\nimport { Buffer } from 'buffer';\nimport dayjs from 'dayjs';\nimport Vue, { PropType } from 'vue';\n\nimport { IChannel, IDPlayerDanmakuSendOptions, IMutedCommentKeywords, IUser } from '@/interface';\nimport CommentMuteSettings from '@/components/Settings/CommentMuteSettings.vue';\nimport Utils from '@/utils';\n\n// このコンポーネント内でのコメントのインターフェイス\ninterface IComment {\n id: number;\n text: string;\n time: string;\n user_id: string;\n my_post: boolean;\n}\n\n// 「露骨な表現を含むコメントをミュートする」のフィルタ正規表現\nconst mute_vulgar_comments_pattern = new RegExp(Buffer.from('cHJwcnzvvZDvvZLvvZDvvZJ8U0VYfFPjgIdYfFPil69YfFPil4tYfFPil49YfO+8s++8pe+8uHzvvLPjgIfvvLh877yz4pev77y4fO+8s+KXi++8uHzvvLPil4/vvLh844Ki44OA44Or44OIfOOCouODiuOCpXzjgqLjg4rjg6t844Kk44Kr6IetfOOCpOOBj3zjgYbjgpPjgZN844Km44Oz44KzfOOBhuOCk+OBoXzjgqbjg7Pjg4F844Ko44Kt44ObfOOBiOOBo+OBoXzjgqjjg4Pjg4F844GI44Gj44KNfOOCqOODg+ODrXzjgYjjgo1844Ko44OtfOW3peWPo3zjgYrjgZXjgo/jgorjgb7jgpN844GK44GX44Gj44GTfOOCquOCt+ODg+OCs3zjgqrjg4PjgrXjg7N844GK44Gj44Gx44GEfOOCquODg+ODkeOCpHzjgqrjg4rjg4vjg7x844Kq44OK44ObfOOBiuOBseOBhHzjgqrjg5HjgqR844GKcHzjgYrvvZB844Kq44OV44OR44KzfOOCrOOCpOOCuOODs3zjgq3jg7Pjgr/jg55844GP44Gx44GCfOOBj+OBseOBgXzjgq/jg6p844Kv44Oz44OLfOOBkeOBpHzjgrHjg4R844GU44GP44GU44GP44GU44GP44GU44GPfOOCs+ODs+ODieODvOODoHzjgrbjg7zjg6Hjg7N844K344KzfOOBl+OBk+OBl+OBk3zjgrfjgrPjgrfjgrN844GZ44GR44GZ44GRfOOBm+OBhOOBiOOBjXzjgZnjgYXjgYXjgYXjgYXjgYV844GZ44GG44GG44GG44GG44GGfOOCu+OCr+ODreOCuXzjgrvjg4Pjgq/jgrl844K744OV44OsfOOBoeOBo+OBseOBhHzjgaHjgaPjg5HjgqR844OB44OD44OR44KkfOOBoeOCk+OBk3zjgaHjgIfjgZN844Gh4pev44GTfOOBoeKXi+OBk3zjgaHil4/jgZN844OB44Oz44KzfOODgeOAh+OCs3zjg4Hil6/jgrN844OB4peL44KzfOODgeKXj+OCs3zjgaHjgpPjgb1844Gh44CH44G9fOOBoeKXr+OBvXzjgaHil4vjgb1844Gh4peP44G9fOODgeODs+ODnXzjg4HjgIfjg51844OB4pev44OdfOODgeKXi+ODnXzjg4Hil4/jg51844Gh44KT44Gh44KTfOODgeODs+ODgeODs3zjgabjgYPjgpPjgabjgYPjgpN844OG44Kj44Oz44OG44Kj44OzfOODhuOCo+ODs+ODnXzjg4fjgqvjgYR844OH44Oq44OY44OrfOiEseOBknzjgbHjgYTjgoLjgb9844OR44OR5rS7fOOBteOBhuODu3zjgbXjgYbigKZ844G144GFfO++jO+9qXzjgbXjgY/jgonjgb/jgYvjgZF844G144GP44KJ44KT44GnfOOBuuOBo+OBn3zjgbrjgo3jgbrjgo1844Oa44Ot44Oa44OtfO++je++n+++m+++je++n+++m3zjg5Xjgqfjg6l844G844Gj44GNfOODneODq+ODjnzjgbzjgo3jgpN844Oc44Ot44OzfO++ju++nu++m+++nXzjgb3jgo3jgop844Od44Ot44OqfO++ju++n+++m+++mHzjg57jg7PjgY3jgaR844Oe44Oz44Kt44OEfOOBvuOCk+OBk3zjgb7jgIfjgZN844G+4pev44GTfOOBvuKXi+OBk3zjgb7il4/jgZN844Oe44Oz44KzfOODnuOAh+OCs3zjg57il6/jgrN844Oe4peL44KzfOODnuKXj+OCs3zjgb7jgpPjgZXjgpN844KC44Gj44GT44KKfOODouODg+OCs+ODqnzjgoLjgb/jgoLjgb9844Oi44Of44Oi44OffOODpOOBo+OBpnzjg6Tjgol844KE44KJ44Gb44KNfOODpOOCinzjg6Tjgot844Ok44KMfOODpOOCjXzjg6njg5bjg5t844Ov44Os44OhfOWWmHzpmbDmoLh86Zmw6IyOfOmZsOWUh3zmt6vlpKJ86Zmw5q+bfOeUo+OCgeOCi3zlpbPjga7lrZDjga7ml6V85rGa44Gj44GV44KTfOWkluS6unzlp6Z86aiO5LmX5L2NfOmHkeeOiXzmnIjntYx85b6M6IOM5L2NfOWtkOS9nOOCinzlsITnsr585L+h6ICFfOeyvua2snzpgI/jgZF85oCn5LqkfOeyvuWtkHzmraPluLjkvY185oCn5b60fOaAp+eahHznlJ/nkIZ85a+45q2i44KBfOe0oOadkHzmirHjgYR85oqx44GLfOaKseOBjXzmirHjgY985oqx44GRfOaKseOBk3zkubPpppZ85oGl5Z6ifOS4reOBoOOBl3zkuK3lh7rjgZd85bC/fOaKnOOBhHzmipzjgZHjgarjgYR85oqc44GR44KLfOaKnOOBkeOCjHzohqjjgol85YuD6LW3fOaPieOBvnzmj4njgb985o+J44KAfOaPieOCgXzmvKvmuZZ844CH772efOKXr++9nnzil4vvvZ584peP772efOOAh+ODg+OCr+OCuXzil6/jg4Pjgq/jgrl84peL44OD44Kv44K5fOKXj+ODg+OCr+OCuQ==', 'base64').toString());\n\n// 「罵倒や差別的な表現を含むコメントをミュートする」のフィルタ正規表現\nconst mute_abusive_discriminatory_prejudiced_comments_pattern = new RegExp(Buffer.from('44CCfOOCouOCueODmnzjgqLjg7Pjg4F844Kk44Kr44KMfOOCpOODqeOBpOOBj3zjgqbjgrh844Km44O844OofOOCpuODqHzjgqbjg6jjgq9844GN44KC44GEfOOCreODouOCpHzjgq3jg6LjgYR844KtL+ODoC/jg4F844Ks44Kk44K4fO+9tu++nu+9su+9vO++nnzjgqzjgq1844Kr44K5fOOCreODg+OCunzjgY3jgaHjgYzjgYR844Kt44OB44Ks44KkfOOCreODoOODgXzjgZTjgb9844K044OffOODgeODp+ODs3zljYPjg6fjg7N844Gk44KT44G8fOODhOODs+ODnHzjg4vjgqt844ON44OI44Km44OofOODi+ODgHzvvobvvoDvvp5844OR44O844OofOODkeODqHzjg5Hjg6jjgq9844G244Gj44GVfOODluODg+OCtXzjgbbjgZXjgYR844OW44K144KkfOOBvuOBrOOBkXzjg6Hjgq/jg6l844OQ44KrfOODoOOCq+OBpOOBj3zmhbDlronlqaZ85a6z5YWQfOWkluWtl3zlp6blm7186Z+T5Zu9fOWfuuWcsOWklnzmsJfmjIHjgaHmgqp85q66fOmgg+OBmXzlnKjml6V85q2744GtfOawj+OBrXzvvoDvvot85q255YyVfOatueODknzpmpzlrrN85pyd6a6ufOeymOedgHzlj43ml6V86aas6bm/fOeZuumBlHzmnLR85LiN5b+rfOmWk+aKnOOBkXzpnZblm70=', 'base64').toString());\n\nexport default Vue.extend({\n name: 'Panel-CommentTab',\n components: {\n CommentMuteSettings,\n },\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n },\n // プレイヤーのインスタンス\n player: {\n type: null as PropType, // 代入当初は null になるため苦肉の策\n required: true,\n }\n },\n data() {\n return {\n\n // 手動スクロール状態かどうか\n is_manual_scroll: false,\n\n // 自動スクロール中かどうか\n // 自動スクロール中の場合、scroll イベントが発火しても無視する\n is_auto_scrolling: false,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // コメントリストの配列\n comment_list: [] as IComment[],\n\n // コメントリストの要素\n comment_list_element: null as HTMLElement | null,\n\n // コメントリストのドロップダウン関連\n is_comment_list_dropdown_display: false as boolean,\n comment_list_dropdown_top: 0 as number,\n comment_list_dropdown_comment: null as IComment | null,\n\n // 視聴セッションの WebSocket のインスタンス\n watch_session: null as WebSocket | null,\n\n // コメントセッションの WebSocket のインスタンス\n comment_session: null as WebSocket | null,\n\n // 視聴セッション・コメントセッションの初期化に失敗した際のエラーメッセージ\n // 視聴中チャンネルのニコニコ実況がないときなどに発生する\n initialize_failed_message: null as string | null,\n\n // vpos を計算する基準となる時刻のタイムスタンプ\n vpos_base_timestamp: 0,\n\n // 座席維持用のタイマーのインターバル ID\n keep_seat_interval_id: 0,\n\n // ResizeObserver のインスタンス\n resize_observer: null as ResizeObserver | null,\n\n // ResizeObserver の監視対象の要素\n resize_observer_element: null as HTMLElement | null,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n }\n },\n // 終了前に実行\n beforeDestroy() {\n\n // destroy() を実行\n this.destroy();\n\n // ResizeObserver を終了\n if (this.resize_observer !== null) {\n this.resize_observer.unobserve(this.resize_observer_element);\n }\n },\n watch: {\n\n // チャンネル情報が変更されたとき\n // created() だとチャンネル情報の取得前に実行してしまう\n // this が変わってしまうのでアロー関数は使えない\n async channel(new_channel: IChannel, old_channel: IChannel) {\n\n // 前のチャンネル情報と次のチャンネル情報で channel_id が変わってたら\n if (new_channel.channel_id !== old_channel.channel_id) {\n\n // 0.5秒だけ待ってから\n // 連続してチャンネルを切り替えた時などに毎回コメントサーバーに接続しないように猶予を設ける\n // ただし、最初 (channel_id が gr000 の初期値になっている) だけは待たずに実行する\n if (old_channel.channel_id !== 'gr000') {\n await Utils.sleep(0.5);\n // 0.5 秒待った結果、channel_id が既に変更されているので終了\n if (this.channel.channel_id !== new_channel.channel_id) {\n return;\n }\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // コメントリストの要素を取得\n if (this.comment_list_element === null) {\n this.comment_list_element = this.$el.querySelector('.comment-list');\n }\n\n // 現在コメントリストがユーザーイベントでスクロールされているかどうか\n let is_user_scrolling = false;\n\n // mousedown → mouseup 中: スクロールバーをマウスでドラッグ\n // 残念ながらスクロールバーのドラッグ中は mousemove のイベントが発火しないため、直接 is_user_scrolling を設定する\n this.comment_list_element.onmousedown = (event: MouseEvent) => {\n // コメントリストの要素の左上を起点としたカーソルのX座標を求める\n const x = event.clientX - this.comment_list_element.getBoundingClientRect().left;\n // 座標が clientWidth 以上であれば、スクロールバー上で mousedown されたものとする\n if (x > this.comment_list_element.clientWidth) is_user_scrolling = true;\n }\n this.comment_list_element.onmouseup = (event: MouseEvent) => {\n // コメントリストの要素の左上を起点としたカーソルのX座標を求める\n const x = event.clientX - this.comment_list_element.getBoundingClientRect().left;\n // 座標が clientWidth 以上であれば、スクロールバー上で mouseup されたものとする\n if (x > this.comment_list_element.clientWidth) is_user_scrolling = false;\n }\n\n // ユーザーによるスクロールイベントで is_user_scrolling を true にする\n // 0.1 秒後に false にする(継続してイベントが発火すれば再び true になる)\n const on_user_scrolling = () => {\n is_user_scrolling = true;\n window.setTimeout(() => is_user_scrolling = false, 100);\n }\n\n // 現在コメントリストがドラッグされているかどうか\n let is_dragging = false;\n // touchstart → touchend 中: スクロールバーをタップでドラッグ\n this.comment_list_element.ontouchstart = () => is_dragging = true;\n this.comment_list_element.ontouchend = () => is_dragging = false;\n // touchmove + is_dragging 中: コメントリストをタップでドラッグしてスクロール\n this.comment_list_element.ontouchmove = () => is_dragging === true ? on_user_scrolling(): '';\n\n // wheel 中: マウスホイールの回転\n this.comment_list_element.onwheel = on_user_scrolling;\n\n // コメントリストがスクロールされた際、自動スクロール中でない&ユーザーイベントで操作されていれば、手動スクロールモードに設定\n // 手動スクロールモードでは自動スクロールを行わず、ユーザーがコメントリストをスクロールできるようにする\n this.comment_list_element.onscroll = async () => {\n\n // scroll イベントは自動スクロールでも発火してしまうので、ユーザーイベントによるスクロールかを確認しないといけない\n // 自動スクロール中かどうかは is_auto_scrolling が true のときで判定できるはずだが、\n // コメントが多くなると is_auto_scrolling が false なのに scroll イベントが遅れて発火してしまうことがある\n if (this.is_auto_scrolling === false && is_user_scrolling === true) {\n\n // 手動スクロールを有効化\n this.is_manual_scroll = true;\n\n // イベント発火時点では scrollTop の値が完全に下にスクロールされていない場合があるため、0.1秒だけ待つ\n await Utils.sleep(0.1);\n\n // 一番下までスクロールされていたら自動スクロールに戻す\n if ((this.comment_list_element.scrollTop + this.comment_list_element.offsetHeight) >\n (this.comment_list_element.scrollHeight - 10)) { // 一番下から 10px 以内\n this.is_manual_scroll = false; // 手動スクロールを無効化\n }\n }\n }\n\n // リサイズ時のイベントを初期化\n await this.initReserveObserver();\n\n // ユーザーアカウントの情報を取得\n try {\n this.user = (await Vue.axios.get('/users/me')).data;\n } catch (error) {\n this.user = null;\n }\n\n try {\n\n // 視聴セッションを初期化\n const comment_session_info = await this.initWatchSession();\n\n // vpos の基準時刻のタイムスタンプを取得\n // vpos は番組開始時間からの累計秒(10ミリ秒単位)\n this.vpos_base_timestamp = dayjs(comment_session_info['vpos_base_time']).unix() * 100;\n\n // コメントセッションを初期化\n await this.initCommentSession(comment_session_info);\n\n } catch (error) {\n\n // 初期化に失敗した場合のエラーメッセージを保存しておく\n // 初期化に失敗したのにコメントを送信しようとした際に表示するもの\n this.initialize_failed_message = error.message;\n console.error(error.toString());\n }\n }\n }\n },\n methods: {\n\n // 視聴セッションを初期化\n async initWatchSession(): Promise<{[key: string]: string | null}> {\n\n // セッション情報を取得\n let watch_session_info: AxiosResponse;\n try {\n watch_session_info = await Vue.axios.get(`/channels/${this.channel.channel_id}/jikkyo`);\n } catch (error) {\n throw new Error(error); // エラー内容をコンソールに表示して終了\n }\n\n // セッション情報を取得できなかった\n if (watch_session_info.data.is_success === false) {\n\n // 一部を除くエラーメッセージはプレイヤーにも通知する\n if ((watch_session_info.data.detail !== 'このチャンネルはニコニコ実況に対応していません。') &&\n (watch_session_info.data.detail !== '現在放送中のニコニコ実況がありません。')) {\n this.player.notice(watch_session_info.data.detail);\n }\n\n throw new Error(watch_session_info.data.detail); // エラー内容をコンソールに表示して終了\n }\n\n // イベント内で値を返すため、Promise で包む\n return new Promise((resolve) => {\n\n // 視聴セッション WebSocket を開く\n this.watch_session = new WebSocket(watch_session_info.data.audience_token);\n\n // 視聴セッション WebSocket を開いたとき\n this.watch_session.addEventListener('open', () => {\n\n // 視聴セッションをリクエスト\n // 公式ドキュメントいわく、stream フィールドは Optional らしい\n // サーバー負荷軽減のため、映像が不要な場合は必ず省略してくださいとのこと\n this.watch_session.send(JSON.stringify({\n 'type': 'startWatching',\n 'data': {\n 'reconnect': false,\n },\n }));\n });\n\n // 視聴セッション WebSocket からメッセージを受信したとき\n this.watch_session.addEventListener('message', async (event) => {\n\n // 受信したメッセージ\n const message = JSON.parse(event.data);\n\n switch (message.type) {\n\n // 部屋情報(実際には統合されていて、全てアリーナ扱いになっている)\n case 'room': {\n\n // コメントサーバーへの接続情報の入ったオブジェクトを返す\n return resolve({\n // コメントサーバーへの接続情報\n 'message_server': message.data.messageServer.uri,\n // コメントサーバー上のスレッド ID\n 'thread_id': message.data.threadId,\n // vpos を計算する基準となる時刻 (ISO8601形式)\n 'vpos_base_time': message.data.vposBaseTime,\n // メッセージサーバーから受信するコメント (chat メッセージ) に yourpost フラグを付けるためのキー\n 'your_post_key': (message.data.yourPostKey ? message.data.yourPostKey : null),\n });\n }\n\n // 座席情報\n case 'seat': {\n\n // keepIntervalSec の秒数ごとに keepSeat を送信して座席を維持する\n this.keep_seat_interval_id = window.setInterval(() => {\n // セッションがまだ開いていれば\n if (this.watch_session.readyState === 1) {\n // 座席を維持\n this.watch_session.send(JSON.stringify({\n 'type': 'keepSeat',\n }));\n // setInterval を解除\n } else {\n window.clearInterval(this.keep_seat_interval_id);\n }\n }, message.data.keepIntervalSec * 1000);\n break;\n }\n\n // ping-pong\n case 'ping': {\n\n // pong を返してセッションを維持する\n // 送り返さなかった場合、勝手にセッションが閉じられてしまう\n this.watch_session.send(JSON.stringify({\n 'type': 'pong',\n }));\n break;\n }\n\n // エラー情報\n case 'error': {\n\n // エラー情報\n let error:string;\n switch (message.data.code) {\n\n case 'CONNECT_ERROR':\n error = 'コメントサーバーに接続できません。';\n break;\n case 'CONTENT_NOT_READY':\n error = 'ニコニコ実況が配信できない状態です。';\n break;\n case 'NO_THREAD_AVAILABLE':\n error = 'コメントスレッドを取得できません。';\n break;\n case 'NO_ROOM_AVAILABLE':\n error = 'コメント部屋を取得できません。';\n break;\n case 'NO_PERMISSION':\n error = 'API にアクセスする権限がありません。';\n break;\n case 'NOT_ON_AIR':\n error = 'ニコニコ実況が放送中ではありません。';\n break;\n case 'BROADCAST_NOT_FOUND':\n error = 'ニコニコ実況の配信情報を取得できません。';\n break;\n case 'INTERNAL_SERVERERROR':\n error = 'ニコニコ実況でサーバーエラーが発生しています。';\n break;\n default:\n error = `ニコニコ実況でエラーが発生しています。(${message.data.code})`;\n break;\n }\n\n // エラー情報を表示\n console.log(`error occurred. code: ${message.data.code}`);\n if (this.player.danmaku.showing) {\n this.player.notice(error);\n }\n\n break;\n }\n\n // 再接続を求められた\n case 'reconnect': {\n\n // waitTimeSec に記載の秒数だけ待ってから再接続する\n await Utils.sleep(message.data.waitTimeSec);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n // 公式ドキュメントには reconnect で送られてくる audienceToken で再接続しろと書いてあるんだけど、\n // 確実性的な面で実装が面倒なので当面このままにしておく\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n\n break;\n }\n\n // 視聴セッションが閉じられた(4時のリセットなど)\n case 'disconnect': {\n\n // 実際に接続が閉じられる前に disconnect イベントが送られてきたので、onclose イベントを削除する\n // onclose イベントが発火するのは不意に切断されたときなど最終手段\n if (this.watch_session) this.watch_session.onclose = null;\n\n // 接続切断の理由\n let disconnect_reason;\n switch (message.data.reason) {\n\n case 'TAKEOVER':\n disconnect_reason = 'ニコニコ実況の番組から追い出されました。';\n break;\n case 'NO_PERMISSION':\n disconnect_reason = 'ニコニコ実況の番組の座席を取得できませんでした。';\n break;\n case 'END_PROGRAM':\n disconnect_reason = 'ニコニコ実況がリセットされたか、コミュニティの番組が終了しました。';\n break;\n case 'PING_TIMEOUT':\n disconnect_reason = 'コメントサーバーとの接続生存確認に失敗しました。';\n break;\n case 'TOO_MANY_CONNECTIONS':\n disconnect_reason = 'ニコニコ実況の同一ユーザからの接続数上限を越えています。';\n break;\n case 'TOO_MANY_WATCHINGS':\n disconnect_reason = 'ニコニコ実況の同一ユーザからの視聴番組数上限を越えています。';\n break;\n case 'CROWDED':\n disconnect_reason = 'ニコニコ実況の番組が満席です。';\n break;\n case 'MAINTENANCE_IN':\n disconnect_reason = 'ニコニコ実況はメンテナンス中です。';\n break;\n case 'SERVICE_TEMPORARILY_UNAVAILABLE':\n disconnect_reason = 'ニコニコ実況で一時的にサーバーエラーが発生しています。';\n break;\n default:\n disconnect_reason = `ニコニコ実況との接続が切断されました。(${message.data.reason})`;\n break;\n }\n\n // 接続切断の理由を表示\n console.log(`disconnected. reason: ${message.data.reason}`);\n if (this.player.danmaku.showing) {\n this.player.notice(disconnect_reason);\n }\n\n // 5 秒ほど待ってから再接続する\n await Utils.sleep(5);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n\n break;\n }\n }\n });\n\n\n // 視聴セッションの接続が閉じられたとき(ネットワークが切断された場合など)\n // イベントを無効化しやすいように敢えて onclose で実装する\n this.watch_session.onclose = async (event) => {\n\n // 接続切断の理由を表示\n console.log(`disconnected. code: ${event.code}`);\n if (this.player.danmaku.showing) {\n this.player.notice(`ニコニコ実況との接続が切断されました。(code: ${event.code})`);\n }\n\n // 10 秒ほど待ってから再接続する\n // ニコ生側から切断された場合と異なりネットワークが切断された可能性が高いので、間を多めに取る\n await Utils.sleep(10);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n };\n });\n },\n\n // コメントセッションを初期化\n async initCommentSession(comment_session_info: {[key: string]: string | null}) {\n\n // タブが非表示状態のときにコメントを格納する配列\n // タブが表示状態になったらコメントリストにのみ表示する(遅れているのでプレイヤーには表示しない)\n let comment_list_buffer: IComment[] = [];\n\n // 最初に送信されてくるコメントを受信し終えたかどうかのフラグ\n let is_received_initial_comment = false;\n\n // コメントセッション WebSocket を開く\n this.comment_session = new WebSocket(comment_session_info.message_server);\n\n // コメントセッション WebSocket を開いたとき\n this.comment_session.addEventListener('open', () => {\n\n // コメント送信をリクエスト\n // このコマンドを送らないとコメントが送信されてこない\n this.comment_session.send(JSON.stringify([\n { 'ping': {'content': 'rs:0'} },\n { 'ping': {'content': 'ps:0'} },\n {\n 'thread': {\n 'version': '20061206', // 設定必須\n 'thread': comment_session_info.thread_id, // スレッド ID\n 'threadkey': comment_session_info.your_post_key, // スレッドキー\n 'user_id': '', // ユーザー ID(設定不要らしい)\n 'res_from': -50, // 最初にコメントを 50 個送信する\n }\n },\n { 'ping': {'content': 'pf:0'} },\n { 'ping': {'content': 'rf:0'} },\n ]));\n });\n\n // コメントセッション WebSocket からメッセージを受信したとき\n this.comment_session.addEventListener('message', async (event_raw) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n\n // thread メッセージのみ\n if (event.thread !== undefined) {\n\n // 接続成功のコールバックを DPlayer に通知\n if (event.thread.resultcode === 0) {\n\n // 接続失敗のコールバックを DPlayer に通知\n } else {\n const message = 'コメントサーバーに接続できませんでした。';\n console.error('Error: ' + message);\n }\n }\n\n // ping メッセージのみ\n // rf:0 が送られてきたら初回コメントの受信は完了\n if (event.ping !== undefined && event.ping.content === 'rf:0') {\n\n // 最初に送信されてくるコメントを受信し終えたフラグを立てる\n is_received_initial_comment = true;\n\n // コメントリストを一番下にスクロール\n // 初回コメントは量が多いので、一括でスクロールする\n this.scrollCommentList();\n }\n\n // コメントデータを取得\n const comment = event.chat;\n\n // コメントがない or 広告用など特殊な場合は弾く\n if (comment === undefined ||\n comment.content === undefined ||\n comment.content.match(/\\/[a-z]+ /)) {\n return;\n }\n\n // 自分のコメントも表示しない\n if (comment.yourpost && comment.yourpost === 1) {\n return;\n }\n\n // ミュート対象のコメントかどうかを判定し、もしそうならここで弾く\n if (this.isMutedComment(comment.content as string, comment.user_id as string)) {\n console.log('Muted comment: ' + comment.content);\n return;\n }\n\n // 色・位置・サイズ\n let color = '#FFEAEA'; // コメント色のデフォルト\n let position: 'top' | 'right' | 'bottom' = 'right'; // コメント位置のデフォルト\n let size: 'big' | 'medium' | 'small' = 'medium'; // コメントサイズのデフォルト\n if (comment.mail !== undefined && comment.mail !== null) {\n\n // コマンドをスペースで区切って配列にしたもの (184 は事前に除外)\n const commands = comment.mail.replace('184', '').split(' ');\n\n for (const command of commands) { // コマンドごとに\n // コメント色指定コマンドがあれば取得\n if (this.getCommentColor(command) !== null) {\n color = this.getCommentColor(command);\n }\n // コメント位置指定コマンドがあれば取得\n if (this.getCommentPosition(command) !== null) {\n position = this.getCommentPosition(command);\n }\n // コメントサイズ指定コマンドがあれば取得\n // コメントサイズのコマンドは DPlayer とニコニコで共通なので、変換の必要はない\n if (command === 'big' || command === 'medium' || command === 'small') {\n size = command;\n }\n }\n }\n\n // 「映像の上下に固定表示されるコメントをミュートする」がオンの場合\n // コメントの位置が top (上固定) もしくは bottom (下固定) のときは弾く\n if (Utils.getSettingsItem('mute_fixed_comments') === true && (position === 'top' || position === 'bottom')) {\n console.log('Muted comment (Fixed): ' + comment.content);\n return;\n }\n\n // 「色付きのコメントをミュートする」がオンの場合\n // コメントの色が #FFEAEA (デフォルト) 以外のときは弾く\n if (Utils.getSettingsItem('mute_colored_comments') === true && color !== '#FFEAEA') {\n console.log('Muted comment (Colored): ' + comment.content);\n return;\n }\n\n // 「文字サイズが大きいコメントをミュートする」がオンの場合\n // コメントのサイズが big のときは弾く\n if (Utils.getSettingsItem('mute_big_size_comments') === true && size === 'big') {\n console.log('Muted comment (Big): ' + comment.content);\n return;\n }\n\n // 配信に発生する遅延分待ってから\n // 最初にドカッと送信されてくる初回コメントは少し前に投稿されたコメント群なので、遅らせずに表示させる\n if (is_received_initial_comment) {\n const comment_delay_time = Utils.getSettingsItem('comment_delay_time');\n await Utils.sleep(comment_delay_time);\n }\n\n // コメントリストのコメントが 500 件を超えたら古いものから順に削除する\n // 仮想スクロールとはいえ、さすがに 500 件を超えると重くなりそう\n // 手動スクロール時は実行しない\n if (this.comment_list.length >= 500 && this.is_manual_scroll === false) {\n while (this.comment_list.length >= 500) {\n this.comment_list.shift();\n }\n }\n\n // コメントリストへ追加するオブジェクト\n // コメント投稿時刻はフォーマットしてから\n const comment_dict: IComment = {\n id: comment.no,\n text: comment.content,\n time: dayjs(comment.date * 1000).format('HH:mm:ss'),\n user_id: comment.user_id,\n my_post: false,\n };\n\n // タブが非表示状態のときは、バッファにコメントを追加するだけで終了する\n // ここで追加すると、タブが表示状態になったときに一斉に描画されて大変なことになる\n if (document.visibilityState === 'hidden') {\n comment_list_buffer.push(comment_dict);\n return;\n }\n\n // コメントリストに追加\n this.comment_list.push(comment_dict);\n\n // // コメントリストを一番下にスクロール\n // 最初に受信したコメントは上の処理で一括でスクロールさせる\n if (is_received_initial_comment) {\n this.scrollCommentList();\n }\n\n // コメント描画 (再生時のみ)\n // 最初に受信したコメントはリアルタイムなコメントではないため、描画しないように\n if (is_received_initial_comment) {\n if (!this.player.video.paused){\n this.player.danmaku.draw({\n text: comment.content,\n color: color,\n type: position,\n size: size,\n });\n }\n }\n });\n\n // タブの表示/非表示の状態が切り替わったときのイベント\n // 表示状態になったときにバッファにあるコメントをコメントリストに表示する\n document.onvisibilitychange = () => {\n if (document.visibilityState === 'visible') {\n this.comment_list.push(...comment_list_buffer); // コメントリストに一括で追加\n comment_list_buffer = []; // バッファをクリア\n this.scrollCommentList(); // コメントリストをスクロール\n }\n };\n },\n\n // コメントを送信する\n async sendComment(options: IDPlayerDanmakuSendOptions) {\n\n // 初期化に失敗しているときは実行せず、保存しておいたエラーメッセージを表示する\n if (this.initialize_failed_message !== null) {\n options.error(this.initialize_failed_message);\n return;\n }\n\n // 未ログイン時\n if (this.user === null) {\n options.error('コメントするには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // ニコニコアカウント未連携時\n if (this.user.niconico_user_id === null) {\n options.error('コメントするには、ニコニコアカウントと連携してください。');\n return;\n }\n\n // 一般会員ではコメント位置の指定 (ue, shita) が無視されるので、事前にエラーにしておく\n if (this.user.niconico_user_premium === false && (options.data.type === 'top' || options.data.type === 'bottom')) {\n options.error('コメントを上下に固定するには、ニコニコアカウントのプレミアム会員登録が必要です。');\n return;\n }\n\n // 一般会員ではコメントサイズ大きめの指定 (big) が無視されるので、事前にエラーにしておく\n if (this.user.niconico_user_premium === false && options.data.size === 'big') {\n options.error('コメントサイズを大きめに設定するには、ニコニコアカウントのプレミアム会員登録が必要です。');\n return;\n }\n\n // DPlayer 上のコメント色(カラーコード)とニコニコの色コマンド定義のマッピング\n const color_table = {\n '#FFEAEA': 'white',\n '#F02840': 'red',\n '#FD7E80': 'pink',\n '#FDA708': 'orange',\n '#FFE133': 'yellow',\n '#64DD17': 'green',\n '#00D4F5': 'cyan',\n '#4763FF': 'blue',\n };\n\n // DPlayer 上のコメント位置を表す数値とニコニコの位置コマンド定義のマッピング\n const position_table = {\n 'top': 'ue',\n 'right': 'naka',\n 'bottom': 'shita',\n };\n\n // vpos を計算 (10ミリ秒単位)\n // 番組開始時間からの累計秒らしいけど、なぜ指定しないといけないのかは不明\n const vpos = Math.floor(new Date().getTime() / 10) - this.vpos_base_timestamp;\n\n // コメントを送信\n this.watch_session.send(JSON.stringify({\n 'type': 'postComment',\n 'data': {\n 'text': options.data.text, // コメント本文\n 'color': color_table[options.data.color.toUpperCase()], // コメントの色\n 'position': position_table[options.data.type], // コメント位置\n 'size': options.data.size, // コメントサイズ (DPlayer とニコニコで表現が共通)\n 'vpos': vpos, // 番組開始時間からの累計秒(10ミリ秒単位)\n 'isAnonymous': true, // 匿名コメント (184)\n }\n }));\n\n // 自分のコメントをコメントリストに追加\n this.comment_list.push({\n id: new Date().getTime(),\n text: options.data.text,\n time: dayjs().format('HH:mm:ss'),\n user_id: `${this.user.niconico_user_id}`,\n my_post: true, // コメントリスト上でハイライトする\n });\n\n // コメント送信のレスポンスを取得\n // 簡単にイベントリスナーを削除できるため、あえて onmessage で実装している\n this.watch_session.onmessage = (event) => {\n\n // 受信したメッセージ\n const message = JSON.parse(event.data);\n\n switch (message.type) {\n\n // postCommentResult\n // これが送られてくる → コメント送信に成功している\n case 'postCommentResult': {\n\n // コメント成功のコールバックを DPlayer に通知\n options.success();\n\n // イベントリスナーを削除\n this.watch_session.onmessage = null;\n break;\n }\n\n // error\n // コメント送信直後に error が送られてきた → コメント送信に失敗している\n case 'error': {\n\n // エラーメッセージ\n let error = `コメントの送信に失敗しました。(${message.data.code})`;\n switch (message.data.code) {\n case 'COMMENT_POST_NOT_ALLOWED': {\n error = 'コメントが許可されていません。';\n break;\n }\n case 'INVALID_MESSAGE': {\n error = 'コメント内容が無効です。';\n break;\n }\n }\n\n // コメント失敗のコールバックを DPlayer に通知\n options.error(error);\n\n // イベントリスナーを解除\n this.watch_session.onmessage = null;\n break;\n }\n }\n };\n },\n\n // リサイズ時のイベントを初期化\n async initReserveObserver() {\n\n // 監視対象の要素\n this.resize_observer_element = document.querySelector('.watch-player');\n\n // タイムアウト ID\n // 一時的に無効にした transition を有効化する際に利用する\n let animation_timeout_id = null;\n\n // プレイヤーの要素がリサイズされた際に発火するイベント\n const on_resize = () => {\n\n // 映像の要素\n const video_element = document.querySelector('.dplayer-video-wrap-aspect');\n\n // コメント描画領域の要素\n const comment_area_element = document.querySelector('.dplayer-danmaku');\n\n // プレイヤー全体と映像の高さの差(レターボックス)から、コメント描画領域の高さを狭める必要があるかを判定する\n // 2で割っているのは単体の差を測るため\n if (this.resize_observer_element === null || this.resize_observer_element.clientHeight === null) return;\n if (video_element === null || video_element.clientHeight === null) return;\n const letter_box_height = (this.resize_observer_element.clientHeight - video_element.clientHeight) / 2;\n\n // 70px or 54px (高さが 450px 以下) 以下ならヘッダー(番組名などの表示)と被るので対応する\n const threshold = window.matchMedia('(max-height: 450px)').matches ? 50 : 66;\n if (letter_box_height < threshold) {\n\n // コメント描画領域に必要な上下マージン\n const comment_area_vertical_margin = (threshold - letter_box_height) * 2;\n\n // 狭めるコメント描画領域の幅\n // 映像の要素の幅をそのまま利用する\n const comment_area_width = video_element.clientWidth;\n\n // 狭めるコメント描画領域の高さ\n const comment_area_height = video_element.clientHeight - comment_area_vertical_margin;\n\n // 狭めるコメント描画領域のアスペクト比を求める\n // https://tech.arc-one.jp/asepct-ratio/\n const gcd = (x: number, y: number) => { // 最大公約数を求める関数\n if (y === 0) return x;\n return gcd(y, x % y);\n }\n // 幅と高さの最大公約数を求める\n const gcd_result = gcd(comment_area_width, comment_area_height);\n // 幅と高さをそれぞれ最大公約数で割ってアスペクト比を算出\n const comment_area_height_aspect = `${comment_area_width / gcd_result} / ${comment_area_height / gcd_result}`;\n\n // 一時的に transition を無効化する\n // アスペクト比の設定は連続して行われるが、その際に transition が適用されるとワンテンポ遅れたアニメーションになってしまう\n comment_area_element.style.transition = 'none';\n\n // コメント描画領域に算出したアスペクト比を設定する\n comment_area_element.style.setProperty('--comment-area-aspect-ratio', comment_area_height_aspect);\n\n // コメント描画領域に必要な上下マージンを設定する\n comment_area_element.style.setProperty('--comment-area-vertical-margin', `${comment_area_vertical_margin}px`);\n\n // 以前セットされた setTimeout() を止める\n window.clearTimeout(animation_timeout_id);\n\n // 0.2秒後に実行する\n // 0.2秒より前にもう一度リサイズイベントが来た場合はタイマーがクリアされるため実行されない\n window.setTimeout(() => {\n\n // 再び transition を有効化する\n comment_area_element.style.transition = '';\n\n }, 0.2 * 1000);\n\n } else {\n\n // コメント描画領域に設定したアスペクト比・上下マージンを削除する\n comment_area_element.style.removeProperty('--comment-area-aspect-ratio');\n comment_area_element.style.removeProperty('--comment-area-vertical-margin');\n }\n }\n\n // 要素の監視を開始\n this.resize_observer = new ResizeObserver(on_resize);\n this.resize_observer.observe(this.resize_observer_element);\n\n // 0.6 秒待ってから初回実行\n // チャンネル切り替え後、再初期化されたプレイヤーに適用するため(早いと再初期化前のプレイヤーに適用されてしまう)\n window.setTimeout(on_resize, 0.6 * 1000);\n },\n\n // コメントリストを一番下までスクロールする\n async scrollCommentList(smooth: boolean = false) {\n\n // 手動スクロールモードの時は実行しない\n if (this.is_manual_scroll === true) return;\n\n // 自動スクロール中のフラグを立てる\n this.is_auto_scrolling = true;\n\n // 0.01 秒待って実行し、念押しで2回実行しないと完全に最下部までスクロールされない…(ブラウザの描画バグ?)\n // this.$nextTick() は効かなかった\n for (let index = 0; index < 3; index++) {\n await Utils.sleep(0.01);\n if (smooth === true) { // スムーズスクロール\n this.comment_list_element.scrollTo({top: this.comment_list_element.scrollHeight, left: 0, behavior: 'smooth'});\n } else {\n this.comment_list_element.scrollTo(0, this.comment_list_element.scrollHeight);\n }\n }\n\n // 0.1 秒待つ(重要)\n await Utils.sleep(0.1);\n\n // 自動スクロール中のフラグを降ろす\n this.is_auto_scrolling = false;\n },\n\n /**\n * ニコニコの色指定を 16 進数カラーコードに置換する\n * @param color ニコニコの色指定\n * @return 16 進数カラーコード\n */\n getCommentColor(color: string): string {\n const color_table = {\n 'white': '#FFEAEA',\n 'red': '#F02840',\n 'pink': '#FD7E80',\n 'orange': '#FDA708',\n 'yellow': '#FFE133',\n 'green': '#64DD17',\n 'cyan': '#00D4F5',\n 'blue': '#4763FF',\n 'purple': '#D500F9',\n 'black': '#1E1310',\n 'white2': '#CCCC99',\n 'niconicowhite': '#CCCC99',\n 'red2': '#CC0033',\n 'truered': '#CC0033',\n 'pink2': '#FF33CC',\n 'orange2': '#FF6600',\n 'passionorange': '#FF6600',\n 'yellow2': '#999900',\n 'madyellow': '#999900',\n 'green2': '#00CC66',\n 'elementalgreen': '#00CC66',\n 'cyan2': '#00CCCC',\n 'blue2': '#3399FF',\n 'marineblue': '#3399FF',\n 'purple2': '#6633CC',\n 'nobleviolet': '#6633CC',\n 'black2': '#666666',\n };\n if (color_table[color] !== undefined) {\n return color_table[color];\n } else {\n return null;\n }\n },\n\n /**\n * ニコニコの位置指定を DPlayer の位置指定に置換する\n * @param position ニコニコの位置指定\n * @return DPlayer の位置指定\n */\n getCommentPosition(position: string): 'top' | 'right' | 'bottom' {\n switch (position) {\n case 'ue':\n return 'top';\n case 'naka':\n return 'right';\n case 'shita':\n return 'bottom';\n default:\n return null;\n }\n },\n\n /**\n * ミュート対象のコメントかどうかを判断する\n * @param comment コメント\n * @param user_id コメントを投稿したユーザーの ID\n * @return ミュート対象のコメントなら true を返す\n */\n isMutedComment(comment: string, user_id: string): boolean {\n\n // キーワードミュート処理\n const muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n for (const muted_comment_keyword of muted_comment_keywords) {\n if (muted_comment_keyword.pattern === '') continue; // キーワードが空文字のときは無視\n switch (muted_comment_keyword.match) {\n // 部分一致\n case 'partial':\n if (comment.includes(muted_comment_keyword.pattern)) return true;\n break;\n // 前方一致\n case 'forward':\n if (comment.startsWith(muted_comment_keyword.pattern)) return true;\n break;\n // 後方一致\n case 'backward':\n if (comment.endsWith(muted_comment_keyword.pattern)) return true;\n break;\n // 完全一致\n case 'exact':\n if (comment === muted_comment_keyword.pattern) return true;\n break;\n // 正規表現\n case 'regex':\n if (new RegExp(muted_comment_keyword.pattern).test(comment)) return true;\n break;\n }\n }\n\n // 「露骨な表現を含むコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_vulgar_comments') === true) {\n if (mute_vulgar_comments_pattern.test(comment)) return true;\n }\n\n // 「罵倒や差別的な表現を含むコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_abusive_discriminatory_prejudiced_comments') === true) {\n if (mute_abusive_discriminatory_prejudiced_comments_pattern.test(comment)) return true;\n }\n\n // 「8文字以上同じ文字が連続しているコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_consecutive_same_characters_comments') === true) {\n if (/(.)\\1{7,}/.test(comment)) return true;\n }\n\n // 「NHK→計1447ID/内プレ425ID/総33372米 ◆ Eテレ → 計73ID/内プレ19ID/総941米」のような\n // 迷惑コメントを一括で弾く (あえてミュートしたくないユースケースが思い浮かばないのでデフォルトで弾く)\n if (/最高\\d+米\\/|計\\d+ID|総\\d+米/.test(comment)) return true;\n\n // ユーザー ID ミュート処理\n const muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n for (const muted_niconico_user_id of muted_niconico_user_ids) {\n if (user_id === muted_niconico_user_id) return true;\n }\n\n // いずれのミュート処理にも引っかからなかった (ミュート対象ではない)\n return false;\n },\n\n // ミュート済みキーワードリストに追加する (完全一致)\n addMutedKeywords(comment: string) {\n const muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n muted_comment_keywords.push({\n match: 'exact',\n pattern: comment,\n });\n Utils.setSettingsItem('muted_comment_keywords', muted_comment_keywords);\n },\n\n // ミュート済みニコニコユーザー ID リストに追加する\n addMutedNiconicoUserIDs(user_id: string) {\n const muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n muted_niconico_user_ids.push(user_id);\n Utils.setSettingsItem('muted_niconico_user_ids', muted_niconico_user_ids);\n },\n\n // ドロップダウンメニューを表示する\n displayCommentListDropdown(event: Event, comment: IComment) {\n this.is_comment_list_dropdown_display = true;\n this.comment_list_dropdown_top = (event.currentTarget as HTMLElement).getBoundingClientRect().top -\n (this.$refs.comment_list_wrapper as HTMLDivElement).getBoundingClientRect().top;\n this.comment_list_dropdown_comment = comment;\n },\n\n // 破棄する\n destroy() {\n\n // 初期化失敗時のメッセージをクリア\n this.initialize_failed_message = null;\n\n // コメントリストをクリア\n this.comment_list = [];\n\n // タブの表示/非表示の状態が切り替わったときのイベントを削除\n document.onvisibilitychange = null;\n\n // 視聴セッションを閉じる\n if (this.watch_session !== null) {\n this.watch_session.onclose = null; // WebSocket が閉じられた際のイベントを削除\n this.watch_session.close(); // WebSocket を閉じる\n this.watch_session = null; // null に戻す\n }\n\n // コメントセッションを閉じる\n if (this.comment_session !== null) {\n this.comment_session.onclose = null; // WebSocket が閉じられた際のイベントを削除\n this.comment_session.close(); // WebSocket を閉じる\n this.comment_session = null; // null に戻す\n }\n\n // 座席保持用のタイマーをクリア\n window.clearInterval(this.keep_seat_interval_id);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Comment.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Comment.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Comment.vue?vue&type=template&id=3eadf094&scoped=true&\"\nimport script from \"./Comment.vue?vue&type=script&lang=ts&\"\nexport * from \"./Comment.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Comment.vue?vue&type=style&index=0&id=3eadf094&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3eadf094\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"program-container\"},[_c('section',{staticClass:\"program-broadcaster\"},[_c('img',{staticClass:\"program-broadcaster__icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('div',{staticClass:\"program-broadcaster__number\"},[_vm._v(\"Ch: \"+_vm._s(_vm.channel.channel_number))]),_c('div',{staticClass:\"program-broadcaster__name\"},[_vm._v(_vm._s(_vm.channel.channel_name))])]),_c('section',{staticClass:\"program-info\"},[_c('h1',{staticClass:\"program-info__title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'title'))}}),_c('div',{staticClass:\"program-info__time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_present)))]),_c('div',{staticClass:\"program-info__description\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'description'))}}),_c('div',{staticClass:\"program-info__genre-container\"},_vm._l((_vm.ProgramUtils.getAttribute(_vm.channel.program_present, 'genre', [])),function(genre,genre_index){return _c('div',{key:genre_index,staticClass:\"program-info__genre\"},[_vm._v(\" \"+_vm._s(genre.major)+\" / \"+_vm._s(genre.middle)+\" \")])}),0),_c('div',{staticClass:\"program-info__next\"},[_c('span',{staticClass:\"program-info__next-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"program-info__next-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}})],1),_c('span',{staticClass:\"program-info__next-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_following, 'title'))}}),_c('div',{staticClass:\"program-info__next-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_following)))]),_c('div',{staticClass:\"program-info__status\"},[_c('div',{staticClass:\"program-info__status-force\",class:`program-info__status-force--${_vm.ChannelUtils.getChannelForceType(_vm.channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"勢い:\")]),_c('span',{staticClass:\"ml-2\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(_vm.channel, 'channel_force', '--'))+\" コメ/分\")])],1),_c('div',{staticClass:\"program-info__status-viewers ml-5\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:eye\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"視聴数:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.channel.viewers))])],1)])]),_c('section',{staticClass:\"program-detail-container\"},_vm._l((_vm.ProgramUtils.getAttribute(_vm.channel.program_present, 'detail', {})),function(detail_text,detail_heading){return _c('div',{key:detail_heading,staticClass:\"program-detail\"},[_c('h2',{staticClass:\"program-detail__heading\"},[_vm._v(_vm._s(detail_heading))]),_c('div',{staticClass:\"program-detail__text\",domProps:{\"innerHTML\":_vm._s(_vm.Utils.URLtoLink(detail_text))}})])}),0)])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { IChannel } from '@/interface';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Panel-ProgramTab',\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n }\n },\n data() {\n return {\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Program.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Program.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Program.vue?vue&type=template&id=3c7f1e0c&scoped=true&\"\nimport script from \"./Program.vue?vue&type=script&lang=ts&\"\nexport * from \"./Program.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Program.vue?vue&type=style&index=0&id=3c7f1e0c&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3c7f1e0c\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"twitter-container\"},[_c('v-dialog',{attrs:{\"content-class\":\"zoom-capture-modal-container\",\"max-width\":\"980\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.zoom_capture_modal),callback:function ($$v) {_vm.zoom_capture_modal=$$v},expression:\"zoom_capture_modal\"}},[_c('div',{staticClass:\"zoom-capture-modal\"},[_c('img',{staticClass:\"zoom-capture-modal__image\",attrs:{\"src\":_vm.zoom_capture ? _vm.zoom_capture.image_url: ''}}),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"zoom-capture-modal__download\",attrs:{\"href\":_vm.zoom_capture ? _vm.zoom_capture.image_url : '',\"download\":_vm.zoom_capture ? _vm.zoom_capture.filename : ''}},[_c('Icon',{attrs:{\"icon\":\"fa6-solid:download\",\"width\":\"45px\"}})],1)])]),_c('div',{staticClass:\"tab-container\"},[_c('div',{staticClass:\"tab-content\",class:{'tab-content--active': _vm.twitter_active_tab === 'Capture'}},[_c('div',{staticClass:\"captures\"},_vm._l((_vm.captures),function(capture){return _c('div',{key:capture.image_url,staticClass:\"capture\",class:{\n 'capture--selected': capture.selected,\n 'capture--focused': capture.focused,\n 'capture--disabled': !capture.selected && _vm.tweet_captures.length >= 4,\n },on:{\"click\":function($event){return _vm.clickCapture(capture)}}},[_c('img',{staticClass:\"capture__image\",attrs:{\"src\":capture.image_url}}),_c('div',{staticClass:\"capture__disabled-cover\"}),_c('div',{staticClass:\"capture__selected-number\"},[_vm._v(_vm._s(_vm.tweet_captures.findIndex(blob => blob === capture.blob) + 1))]),_c('Icon',{staticClass:\"capture__selected-checkmark\",attrs:{\"icon\":\"fluent:checkmark-circle-16-filled\"}}),_c('div',{staticClass:\"capture__selected-border\"}),_c('div',{staticClass:\"capture__focused-border\"}),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"capture__zoom\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.zoom_capture_modal = true; _vm.zoom_capture = capture},\"mousedown\":function($event){$event.preventDefault();$event.stopPropagation();/* 親要素の波紋が広がらないように */}}},[_c('Icon',{attrs:{\"icon\":\"fluent:zoom-in-16-regular\",\"width\":\"32px\"}})],1)],1)}),0),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.captures.length === 0),expression:\"captures.length === 0\"}],staticClass:\"capture-announce\"},[_c('div',{staticClass:\"capture-announce__heading\"},[_vm._v(\"まだキャプチャがありません。\")]),_vm._m(0)])])]),_c('div',{staticClass:\"tab-button-container\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Search'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Search'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:search-16-filled\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"ツイート検索\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Timeline'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Timeline'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:home-16-regular\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"タイムライン\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Capture'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Capture'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:image-copy-20-regular\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"キャプチャ\")])],1)]),_c('div',{staticClass:\"tweet-form\",class:{\n 'tweet-form--focused': _vm.is_tweet_hashtag_form_focused || _vm.is_tweet_text_form_focused,\n 'tweet-form--virtual-keyboard-display': _vm.is_virtual_keyboard_display &&\n (_vm.Utils.hasActiveElementClass('tweet-form__hashtag-form') || _vm.Utils.hasActiveElementClass('tweet-form__textarea')) &&\n (() => {_vm.is_hashtag_list_display = false; return true;})(),\n }},[_c('div',{staticClass:\"tweet-form__hashtag\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.tweet_hashtag),expression:\"tweet_hashtag\"}],staticClass:\"tweet-form__hashtag-form\",attrs:{\"type\":\"search\",\"placeholder\":\"#ハッシュタグ\"},domProps:{\"value\":(_vm.tweet_hashtag)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.tweet_hashtag=$event.target.value},function($event){return _vm.updateTweetLetterCount()}],\"focus\":function($event){_vm.is_tweet_hashtag_form_focused = true},\"blur\":function($event){_vm.is_tweet_hashtag_form_focused = false},\"change\":function($event){_vm.tweet_hashtag = _vm.formatHashtag(_vm.tweet_hashtag)}}}),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tweet-form__hashtag-list-button\",on:{\"click\":function($event){_vm.is_hashtag_list_display = !_vm.is_hashtag_list_display}}},[_c('Icon',{attrs:{\"icon\":\"fluent:clipboard-text-ltr-32-regular\",\"height\":\"22px\"}})],1)]),_c('textarea',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.tweet_text),expression:\"tweet_text\"}],ref:\"tweet_text\",staticClass:\"tweet-form__textarea\",attrs:{\"placeholder\":\"ツイート\"},domProps:{\"value\":(_vm.tweet_text)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.tweet_text=$event.target.value},function($event){return _vm.updateTweetLetterCount()}],\"paste\":function($event){return _vm.pasteClipboardData($event)},\"focus\":function($event){_vm.is_tweet_text_form_focused = true},\"blur\":function($event){_vm.is_tweet_text_form_focused = false}}}),_c('div',{staticClass:\"tweet-form__control\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"account-button\",class:{'account-button--no-login': !_vm.is_logged_in_twitter},on:{\"click\":function($event){return _vm.clickAccountButton()}}},[_c('img',{staticClass:\"account-button__icon\",attrs:{\"src\":_vm.is_logged_in_twitter ? _vm.selected_twitter_account.icon_url : '/assets/images/account-icon-default.png'}}),_c('span',{staticClass:\"account-button__screen-name\"},[_vm._v(\" \"+_vm._s(_vm.is_logged_in_twitter ? `@${_vm.selected_twitter_account.screen_name}` : '連携されていません')+\" \")]),_c('Icon',{staticClass:\"account-button__menu\",attrs:{\"icon\":\"fluent:more-circle-20-regular\",\"width\":\"22px\"}})],1),_c('div',{staticClass:\"limit-meter\"},[_c('div',{staticClass:\"limit-meter__content\",class:{\n 'limit-meter__content--yellow': _vm.tweet_letter_count <= 20,\n 'limit-meter__content--red': _vm.tweet_letter_count <= 0,\n }},[_c('Icon',{staticStyle:{\"margin-right\":\"-2px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"12px\"}}),_c('span',[_vm._v(_vm._s(_vm.tweet_letter_count))])],1),_c('div',{staticClass:\"limit-meter__content\"},[_c('Icon',{attrs:{\"icon\":\"fluent:image-16-filled\",\"width\":\"14px\"}}),_c('span',[_vm._v(_vm._s(_vm.tweet_captures.length)+\"/4\")])],1)]),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tweet-button\",attrs:{\"disabled\":!_vm.is_logged_in_twitter || _vm.tweet_letter_count < 0 ||\n (_vm.tweet_letter_count === 140 && _vm.tweet_captures.length === 0)},on:{\"click\":function($event){return _vm.sendTweet()}}},[_c('Icon',{attrs:{\"icon\":\"fa-brands:twitter\",\"height\":\"16px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ツイート\")])],1)])]),_c('div',{staticClass:\"twitter-account-list\",class:{'twitter-account-list--display': _vm.is_twitter_account_list_display}},_vm._l((_vm.user.twitter_accounts),function(twitter_account){return _c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:twitter_account.id,staticClass:\"twitter-account\",on:{\"click\":function($event){return _vm.updateSelectedTwitterAccount(twitter_account)}}},[_c('img',{staticClass:\"twitter-account__icon\",attrs:{\"src\":twitter_account.icon_url}}),_c('div',{staticClass:\"twitter-account__info\"},[_c('div',{staticClass:\"twitter-account__name\"},[_vm._v(_vm._s(twitter_account.name))]),_c('div',{staticClass:\"twitter-account__screen-name\"},[_vm._v(\"@\"+_vm._s(twitter_account.screen_name))])]),_c('Icon',{directives:[{name:\"show\",rawName:\"v-show\",value:(twitter_account.id === _vm.selected_twitter_account_id),expression:\"twitter_account.id === selected_twitter_account_id\"}],staticClass:\"twitter-account__check\",attrs:{\"icon\":\"fluent:checkmark-16-filled\",\"width\":\"24px\"}})],1)}),0),_c('div',{staticClass:\"hashtag-list\",class:{\n 'hashtag-list--display': _vm.is_hashtag_list_display,\n 'hashtag-list--virtual-keyboard-display': _vm.is_virtual_keyboard_display && _vm.Utils.hasActiveElementClass('hashtag__input'),\n }},[_c('div',{staticClass:\"hashtag-heading\"},[_c('div',{staticClass:\"hashtag-heading__text\"},[_c('Icon',{attrs:{\"icon\":\"charm:hash\",\"width\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ハッシュタグリスト\")])],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag-heading__add-button\",on:{\"click\":function($event){_vm.saved_twitter_hashtags.push({id: Date.now(), text: '#ここにハッシュタグを入力', editing: false})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"width\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)]),_c('draggable',{staticClass:\"hashtag-container\",attrs:{\"handle\":\".hashtag__sort-handle\"},model:{value:(_vm.saved_twitter_hashtags),callback:function ($$v) {_vm.saved_twitter_hashtags=$$v},expression:\"saved_twitter_hashtags\"}},_vm._l((_vm.saved_twitter_hashtags),function(hashtag){return _c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\",value:(!hashtag.editing),expression:\"!hashtag.editing\"}],key:hashtag.id,staticClass:\"hashtag\",class:{'hashtag--editing': hashtag.editing},on:{\"click\":function($event){_vm.tweet_hashtag = hashtag.text; _vm.tweet_hashtag = _vm.formatHashtag(_vm.tweet_hashtag);\n _vm.updateTweetLetterCount(); _vm.window.setTimeout(() => _vm.is_hashtag_list_display = false, 150)}}},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(hashtag.text),expression:\"hashtag.text\"}],staticClass:\"hashtag__input\",attrs:{\"type\":\"search\",\"disabled\":!hashtag.editing},domProps:{\"value\":(hashtag.text)},on:{\"click\":function($event){$event.stopPropagation();},\"input\":function($event){if($event.target.composing)return;_vm.$set(hashtag, \"text\", $event.target.value)}}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag__edit-button\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();hashtag.editing = !hashtag.editing; hashtag.text = _vm.formatHashtag(hashtag.text, true)}}},[_c('Icon',{attrs:{\"icon\":hashtag.editing ? 'fluent:checkmark-16-filled': 'fluent:edit-16-filled',\"width\":\"17px\"}})],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag__delete-button\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.saved_twitter_hashtags.splice(_vm.saved_twitter_hashtags.indexOf(hashtag), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"17px\"}})],1),_c('div',{staticClass:\"hashtag__sort-handle\"},[_c('Icon',{attrs:{\"icon\":\"material-symbols:drag-handle-rounded\",\"width\":\"17px\"}})],1)])}),0)],1)],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"capture-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(\"プレイヤーのキャプチャボタンやショートカットキーでキャプチャを撮ると、ここに表示されます。\")]),_c('p',{staticClass:\"mt-2 mb-0\"},[_vm._v(\"表示されたキャプチャを選択してからツイートすると、キャプチャを付けてツイートできます。\")])])\n}]\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue, { PropType } from 'vue';\nimport draggable from 'vuedraggable'\n\nimport { IChannel, ITwitterAccount, IUser } from '@/interface';\nimport Utils from '@/utils';\n\n// このコンポーネント内でのキャプチャのインターフェイス\ninterface ITweetCapture {\n blob: Blob;\n filename: string;\n image_url: string;\n selected: boolean;\n focused: boolean;\n}\n\n// このコンポーネント内でのハッシュタグのインターフェイス\ninterface IHashtag {\n id: number;\n text: string;\n editing: boolean;\n}\n\nexport default Vue.extend({\n name: 'Panel-TwitterTab',\n components: {\n draggable,\n },\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n },\n // プレイヤーのインスタンス\n player: {\n type: null as PropType, // 代入当初は null になるため苦肉の策\n required: true,\n },\n // 仮想キーボードが表示されているかどうか\n is_virtual_keyboard_display: {\n type: Boolean as PropType,\n required: true,\n },\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // window.setTimeout() にアクセスできるように\n window: window,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // Twitter アカウントを1つでも連携しているかどうか\n is_logged_in_twitter: false,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // 現在ツイート対象として選択されている Twitter アカウント\n selected_twitter_account: null as ITwitterAccount | null,\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID\n selected_twitter_account_id: Utils.getSettingsItem('selected_twitter_account_id') as number | null,\n\n // 連携している Twitter アカウントリストを表示しているか\n is_twitter_account_list_display: false,\n\n // 保存している Twitter のハッシュタグが入るリスト\n saved_twitter_hashtags: (Utils.getSettingsItem('saved_twitter_hashtags') as string[]).map((hashtag, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {id: Date.now() + index, text: hashtag, editing: false} as IHashtag;\n }),\n\n // ハッシュタグリストを表示しているか\n is_hashtag_list_display: false,\n\n // 既定で表示される Twitter タブ内のタブ\n twitter_active_tab: Utils.getSettingsItem('twitter_active_tab') as ('Search' | 'Timeline' | 'Capture'),\n\n // キャプチャを拡大表示するモーダルの表示状態\n zoom_capture_modal: false,\n\n // 現在モーダルで拡大表示中のキャプチャのオブジェクト\n zoom_capture: null as ITweetCapture | null,\n\n // キャプチャリスト\n captures: [] as ITweetCapture[],\n\n // キャプチャリストの要素\n captures_element: null as HTMLDivElement | null,\n\n // ツイートハッシュタグフォームにフォーカスしているか\n is_tweet_hashtag_form_focused: false,\n\n // ツイート本文フォームにフォーカスしているか\n is_tweet_text_form_focused: false,\n\n // ツイートのハッシュタグ\n tweet_hashtag: '',\n\n // ツイート本文\n tweet_text: '',\n\n // ツイートに添付するキャプチャの Blob のリスト\n tweet_captures: [] as Blob[],\n\n // 文字数カウント\n tweet_letter_count: 140,\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n\n // 連携している Twitter アカウントがあれば true に設定\n if (this.user.twitter_accounts.length > 0) {\n this.is_logged_in_twitter = true;\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID が設定されていない or ID に紐づく Twitter アカウントがない\n // 連携している Twitter アカウントのうち、一番最初のものを自動選択する\n // ここで言う Twitter アカウントの ID は DB 上で連番で振られるもので、Twitter アカウントそのものの固有 ID ではない\n if (this.selected_twitter_account_id === null ||\n !this.user.twitter_accounts.some((twitter_account) => twitter_account.id === this.selected_twitter_account_id)) {\n this.selected_twitter_account_id = this.user.twitter_accounts[0].id;\n Utils.setSettingsItem('selected_twitter_account_id', this.selected_twitter_account_id);\n }\n\n // 現在ツイート対象として選択されている Twitter アカウントを取得・設定\n const twitter_account_index = this.user.twitter_accounts.findIndex((twitter_account) => {\n return twitter_account.id === this.selected_twitter_account_id; // Twitter アカウントの ID が選択されているものと一致する\n });\n this.selected_twitter_account = this.user.twitter_accounts[twitter_account_index];\n }\n }\n\n // 局タグ追加処理を走らせる (ハッシュタグフォームのフォーマット処理も同時に行われるが、元々空なので無意味)\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag);\n },\n beforeDestroy() {\n // 終了前にすべてのキャプチャの Blob URL を revoke してリソースを解放する\n for (const capture of this.captures) {\n URL.revokeObjectURL(capture.image_url);\n }\n },\n watch: {\n\n // チャンネル情報が変更されたとき\n // 前のチャンネル情報と次のチャンネル情報で channel_id が変わってたら局タグ追加処理を走らせる\n async channel(new_channel: IChannel, old_channel: IChannel) {\n if (new_channel.channel_id !== old_channel.channel_id) {\n const old_channel_hashtag = this.getChannelHashtag(old_channel.channel_name) ?? '';\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag.replaceAll(old_channel_hashtag, ''));\n }\n },\n\n // 保存しているハッシュタグが変更されたら随時 LocalStorage に保存する\n saved_twitter_hashtags: {\n deep: true,\n handler() {\n Utils.setSettingsItem('saved_twitter_hashtags', this.saved_twitter_hashtags.map(hashtag => hashtag.text));\n }\n }\n },\n methods: {\n\n // ユーザーアカウントの情報を取得する\n async syncAccountInfo() {\n try {\n this.user = (await Vue.axios.get('/users/me')).data;\n } catch (error) {\n // ログインされていないので未ログイン状態に設定\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n // 文字数カウントを変更するイベント\n updateTweetLetterCount() {\n\n // サロゲートペアを考慮し、スプレッド演算子で一度配列化してから数えている\n // ref: https://qiita.com/suin/items/3da4fb016728c024eaca\n this.tweet_letter_count = 140 - [...this.tweet_hashtag].length - [...this.tweet_text].length;\n },\n\n // アカウントボタンが押されたときのイベント\n clickAccountButton() {\n\n // Twitter アカウントが連携されていない場合は Twitter 設定画面に飛ばす\n if (!this.is_logged_in_twitter) {\n\n // 視聴ページ以外に遷移するため、フルスクリーンを解除しないと画面が崩れる\n if (document.fullscreenElement) {\n document.exitFullscreen();\n }\n\n this.$router.push({path: '/settings/twitter'});\n return;\n }\n\n // アカウントリストの表示/非表示を切り替え\n this.is_twitter_account_list_display = !this.is_twitter_account_list_display;\n\n // アカウントリストが表示されているなら、ハッシュタグリストを非表示にする\n if (this.is_twitter_account_list_display === true) {\n this.is_hashtag_list_display = false;\n }\n },\n\n // クリップボード内のデータがペーストされたときのイベント\n pasteClipboardData(event: ClipboardEvent) {\n\n // 一応配列になっているので回しているが、基本1回のペーストにつき DataTransferItem は1個しか入らない\n for (const clipboard_item of event.clipboardData.items) {\n\n // 画像のみを対象にする (DataTransferItem.type には MIME タイプが入る)\n if (clipboard_item.type.startsWith('image/')) {\n\n // クリップボード内の画像データを File オブジェクトとして取得し、キャプチャリストに追加\n const file = clipboard_item.getAsFile();\n this.addCaptureList(file, file.name);\n }\n }\n },\n\n // 選択されている Twitter アカウントを更新する\n updateSelectedTwitterAccount(twitter_account: ITwitterAccount) {\n this.selected_twitter_account_id = twitter_account.id;\n Utils.setSettingsItem('selected_twitter_account_id', this.selected_twitter_account_id);\n this.selected_twitter_account = twitter_account;\n\n // Twitter アカウントリストのオーバーレイを閉じる (少し待ってから閉じたほうが体感が良い)\n window.setTimeout(() => this.is_twitter_account_list_display = false, 150);\n },\n\n // キャプチャリスト内のキャプチャがクリックされたときのイベント\n clickCapture(capture: ITweetCapture) {\n\n // 選択されたキャプチャが3枚まで & まだ選択されていないならキャプチャをツイート対象に追加する\n if (this.tweet_captures.length < 4 && capture.selected === false) {\n capture.selected = true;\n this.tweet_captures.push(capture.blob);\n } else {\n // ツイート対象のキャプチャになっていたら取り除く\n const index = this.tweet_captures.findIndex(blob => blob === capture.blob);\n if (index > -1) {\n this.tweet_captures.splice(index, 1);\n }\n // キャプチャの選択を解除\n capture.selected = false;\n }\n },\n\n // 撮ったキャプチャを親コンポーネントから受け取り、キャプチャリストに追加する\n async addCaptureList(blob: Blob, filename: string) {\n\n if (this.captures_element === null) {\n this.captures_element = this.$el.querySelector('.tab-content');\n }\n\n // 撮ったキャプチャが50件を超えていたら、重くなるので古いものから削除する\n // 削除する前に Blob URL を revoke してリソースを解放するのがポイント\n if (this.captures.length > 50) {\n URL.revokeObjectURL(this.captures[0].image_url);\n this.captures.shift();\n }\n\n // キャプチャリストにキャプチャを追加\n const blob_url = URL.createObjectURL(blob);\n this.captures.push({\n blob: blob,\n filename: filename,\n image_url: blob_url,\n selected: false,\n focused: false,\n });\n\n // キャプチャリストを下にスクロール\n // this.$nextTick() のコールバックで DOM の更新を待つ\n this.$nextTick(() => {\n this.captures_element.scrollTo({\n top: this.captures_element.scrollHeight,\n behavior: 'smooth',\n });\n });\n },\n\n // 撮ったキャプチャに番組タイトルの透かしを描画する\n async drawProgramTitleOnCapture(capture: Blob): Promise {\n\n // キャプチャの Blob を createImageBitmap() で Canvas に描ける ImageBitmap に変換\n const image_bitmap = await createImageBitmap(capture);\n\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n const canvas = ('OffscreenCanvas' in window) ?\n new OffscreenCanvas(image_bitmap.width, image_bitmap.height) : document.createElement('canvas');\n\n // Canvas にキャプチャを描画\n const context = canvas.getContext('2d');\n context.drawImage(image_bitmap, 0, 0);\n image_bitmap.close();\n\n // 描画設定\n context.font = `bold 22px 'YakuHanJPs', 'Open Sans', 'Hiragino Sans', 'Noto Sans JP', sans-serif`; // フォント\n context.fillStyle = 'rgba(255, 255, 255, 70%)'; // 半透明の白\n context.shadowColor = 'rgba(0, 0, 0, 100%)' // 影の色\n context.shadowBlur = 4; // 影をぼかすしきい値\n context.shadowOffsetX = 0; // 影のX座標\n context.shadowOffsetY = 0; // 影のY座標\n\n // 番組タイトルの透かしを描画\n switch (Utils.getSettingsItem('tweet_capture_watermark_position')) {\n case 'TopLeft': {\n context.textAlign = 'left'; // 左寄せ\n context.textBaseline = 'top'; // ベースラインを上寄せ\n context.fillText(this.channel.program_present.title, 16, 12);\n break;\n }\n case 'TopRight': {\n context.textAlign = 'right'; // 右寄せ\n context.textBaseline = 'top'; // ベースラインを上寄せ\n context.fillText(this.channel.program_present.title, canvas.width - 16, 12);\n break;\n }\n case 'BottomLeft': {\n context.textAlign = 'left'; // 左寄せ\n context.textBaseline = 'bottom'; // ベースラインを下寄せ\n context.fillText(this.channel.program_present.title, 16, canvas.height - 12);\n break;\n }\n case 'BottomRight': {\n context.textAlign = 'right'; // 右寄せ\n context.textBaseline = 'bottom'; // ベースラインを下寄せ\n context.fillText(this.channel.program_present.title, canvas.width - 16, canvas.height - 12);\n break;\n }\n }\n\n // Blob にして返す\n if ('OffscreenCanvas' in window) {\n return await (canvas as OffscreenCanvas).convertToBlob({type: 'image/jpeg', quality: 1});\n } else {\n return new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(blob => resolve(blob), 'image/jpeg', 1));\n }\n },\n\n // チャンネル名から対応する局タグを取得する\n // とりあえず三大首都圏 + BS のみ対応\n getChannelHashtag(channel_name: string): string | null {\n // NHK\n if (channel_name.startsWith('NHK総合')) {\n return '#nhk';\n } else if (channel_name.startsWith('NHKEテレ')) {\n return '#etv';\n // 民放\n } else if (channel_name.startsWith('日テレ')) {\n return '#ntv';\n } else if (channel_name.startsWith('読売テレビ')) {\n return '#ytv';\n } else if (channel_name.startsWith('中京テレビ')) {\n return '#chukyotv';\n } else if (channel_name.startsWith('テレビ朝日')) {\n return '#tvasahi';\n } else if (channel_name.startsWith('ABCテレビ')) {\n return '#abc';\n } else if (channel_name.startsWith('メ~テレ')) {\n return '#nagoyatv';\n } else if (channel_name.startsWith('TBS') && !channel_name.includes('TBSチャンネル')) {\n return '#tbs';\n } else if (channel_name.startsWith('MBS')) {\n return '#mbs';\n } else if (channel_name.startsWith('CBC')) {\n return '#cbc';\n } else if (channel_name.startsWith('テレビ東京')) {\n return '#tvtokyo';\n } else if (channel_name.startsWith('テレビ大阪')) {\n return '#tvo';\n } else if (channel_name.startsWith('テレビ愛知')) {\n return '#tva';\n } else if (channel_name.startsWith('フジテレビ')) {\n return '#fujitv';\n } else if (channel_name.startsWith('関西テレビ')) {\n return '#kantele';\n } else if (channel_name.startsWith('東海テレビ')) {\n return '#tokaitv';\n // 独立局\n } else if (channel_name.startsWith('TOKYO MX')) {\n return '#tokyomx';\n } else if (channel_name.startsWith('tvk')) {\n return '#tvk';\n } else if (channel_name.startsWith('チバテレ')) {\n return '#chibatv';\n } else if (channel_name.startsWith('テレ玉')) {\n return '#teletama';\n } else if (channel_name.startsWith('サンテレビ')) {\n return '#suntv';\n } else if (channel_name.startsWith('KBS京都')) {\n return '#kbs';\n // BS・CS\n } else if (channel_name.startsWith('NHKBS1')) {\n return '#nhkbs1';\n } else if (channel_name.startsWith('NHKBSプレミアム')) {\n return '#nhkbsp';\n } else if (channel_name.startsWith('BS日テレ')) {\n return '#bsntv';\n } else if (channel_name.startsWith('BS朝日')) {\n return '#bsasahi';\n } else if (channel_name.startsWith('BS-TBS')) {\n return '#bstbs';\n } else if (channel_name.startsWith('BSテレ東')) {\n return '#bstvtokyo';\n } else if (channel_name.startsWith('BSフジ')) {\n return '#bsfuji';\n } else if (channel_name.startsWith('BS11イレブン')) {\n return '#bs11';\n } else if (channel_name.startsWith('BS12トゥエルビ')) {\n return '#bs12';\n } else if (channel_name.startsWith('AT-X')) {\n return '#at_x';\n }\n\n return null;\n },\n\n // ハッシュタグを整形(余計なスペースなどを削り、全角ハッシュを半角ハッシュへ、全角スペースを半角スペースに置換)\n formatHashtag(tweet_hashtag: string, from_hashtag_list: boolean = false): string {\n\n // ハッシュとスペースの表記ゆれを統一し、連続するハッシュやスペースを1つにする\n const tweet_hashtag_array = tweet_hashtag.trim()\n .replaceAll('♯', '#').replaceAll('#', '#').replace(/#{2,}/g, '#').replaceAll(' ', ' ').replaceAll(/ +/g,' ').split(' ')\n .filter(hashtag => hashtag !== '');\n\n // ハッシュタグがついてない場合にハッシュタグを付与\n for (let index in tweet_hashtag_array) {\n if (!tweet_hashtag_array[index].startsWith('#')) {\n tweet_hashtag_array[index] = `#${tweet_hashtag_array[index]}`;\n }\n }\n\n // 設定でオンになっている場合のみ、視聴中チャンネルの局タグを自動的に追加する (ハッシュタグリスト内のハッシュタグは除外)\n if (Utils.getSettingsItem('auto_add_watching_channel_hashtag') === true && from_hashtag_list === false) {\n const channel_hashtag = this.getChannelHashtag(this.channel.channel_name);\n if (channel_hashtag !== null) {\n if (tweet_hashtag_array.includes(channel_hashtag) === false) {\n tweet_hashtag_array.push(channel_hashtag);\n }\n }\n }\n\n return tweet_hashtag_array.join(' ');\n },\n\n // ツイートを送信する\n async sendTweet() {\n\n // ハッシュタグを整形\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag);\n const tweet_hashtag = this.tweet_hashtag;\n\n // 実際に送るツイート本文を作成\n let tweet_text = this.tweet_text;\n if (tweet_hashtag !== '') { // ハッシュタグが入力されているときのみ\n switch (Utils.getSettingsItem('tweet_hashtag_position')) {\n // ツイート本文の前に追加する\n case 'Prepend': {\n tweet_text = `${tweet_hashtag} ${this.tweet_text}`;\n break;\n }\n // ツイート本文の後に追加する\n case 'Append': {\n tweet_text = `${this.tweet_text} ${tweet_hashtag}`;\n break;\n }\n // ツイート本文の前に追加してから改行する\n case 'PrependWithLineBreak': {\n tweet_text = `${tweet_hashtag}\\n${this.tweet_text}`;\n break;\n }\n // ツイート本文の後に改行してから追加する\n case 'AppendWithLineBreak': {\n tweet_text = `${this.tweet_text}\\n${tweet_hashtag}`;\n break;\n }\n }\n }\n\n // multipart/form-data でツイート本文と画像(選択されている場合)を送る\n const form_data = new FormData();\n form_data.append('tweet', tweet_text);\n for (let tweet_capture of this.tweet_captures) {\n // キャプチャへの透かしの描画がオンの場合、キャプチャの Blob を透かし付きのものに差し替える\n if (Utils.getSettingsItem('tweet_capture_watermark_position') !== 'None') {\n tweet_capture = await this.drawProgramTitleOnCapture(tweet_capture);\n }\n form_data.append('images', tweet_capture);\n }\n\n // 連投防止のため、フォーム上のツイート本文・キャプチャの選択・キャプチャのフォーカスを消去\n // 送信した感を出す意味合いもある\n for (const capture of this.captures) {\n capture.selected = false;\n capture.focused = false;\n }\n this.tweet_captures = [];\n this.tweet_text = '';\n\n // パネルを閉じるように親コンポーネントに伝える\n if (Utils.getSettingsItem('fold_panel_after_sending_tweet') === true) {\n this.$emit('panel_folding_requested');\n (this.$refs.tweet_text as HTMLTextAreaElement).blur(); // フォーカスを外す\n }\n\n try {\n\n // ツイート送信 API にリクエスト\n const result = await Vue.axios.post(`/twitter/accounts/${this.selected_twitter_account.screen_name}/tweets`, form_data, {\n headers: {'Content-Type': 'multipart/form-data'},\n });\n\n // 成功 or 失敗に関わらず detail の内容をそのまま通知する\n if (result.data.is_success === true) {\n this.player.notice(result.data.detail);\n } else {\n this.player.notice('エラー: ' + result.data.detail);\n }\n\n } catch (error) {\n console.error(error);\n this.player.notice('エラー: ツイートの送信に失敗しました。');\n }\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Twitter.vue?vue&type=template&id=df5cea26&scoped=true&\"\nimport script from \"./Twitter.vue?vue&type=script&lang=ts&\"\nexport * from \"./Twitter.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Twitter.vue?vue&type=style&index=0&id=df5cea26&prod&lang=scss&\"\nimport style1 from \"./Twitter.vue?vue&type=style&index=1&id=df5cea26&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"df5cea26\",\n null\n \n)\n\nexport default component.exports","\n\nimport { AxiosResponse } from 'axios';\nimport dayjs from 'dayjs';\n// @ts-ignore JavaScript で書かれているので型定義がなく、作ろうとするとややこしくなるので黙殺\nimport DPlayer from 'dplayer';\nimport mpegts from 'mpegts.js';\nimport Vue from 'vue';\n\nimport { ChannelTypePretty, IChannel, IChannelDefault } from '@/interface';\nimport Channel from '@/components/Panel/Channel.vue';\nimport Comment from '@/components/Panel/Comment.vue';\nimport Program from '@/components/Panel/Program.vue';\nimport Twitter from '@/components/Panel/Twitter.vue';\nimport Utils, { ChannelUtils, PlayerCaptureHandler, PlayerUtils, ProgramUtils } from '@/utils';\n\n// 低遅延モードオン時の再生バッファ (秒単位)\n// これ以上小さくすると再生が詰まりやすくなる印象\nconst PLAYBACK_BUFFER_SEC_LOW_LATENCY = 1.5;\n\n// 低遅延モードオフ時の再生バッファ (秒単位)\n// 3秒程度の遅延を許容する\nconst PLAYBACK_BUFFER_SEC = 3.0;\n\nexport default Vue.extend({\n name: 'TV-Watch',\n components: {\n Channel,\n Comment,\n Program,\n Twitter,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ProgramUtils: ProgramUtils,\n\n // 現在時刻\n time: dayjs().format('YYYY/MM/DD HH:mm:ss'),\n\n // 表示されるパネルのタブ\n tv_panel_active_tab: Utils.getSettingsItem('tv_panel_active_tab'),\n\n // 背景の URL\n background_url: '',\n\n // プレイヤーのローディング状態\n // 既定でローディングとする\n is_loading: true,\n\n // プレイヤーが映像の再生をバッファリングしているか\n // 視聴開始時以外にも、ネットワークが遅くて再生が一時的に途切れたときなどで表示される\n // 既定でバッファリング中とする\n is_video_buffering: true,\n\n // プレイヤーの背景を表示するか\n // 既定で表示しない\n is_background_display: false,\n\n // コントロールを表示するか\n // 既定で表示する\n is_control_display: true,\n\n // パネルを表示するか\n // panel_display_state が 'AlwaysDisplay' なら常に表示し、'AlwaysFold' なら常に折りたたむ\n // 'RestorePreviousState' なら showed_panel_last_time の値を使い、前回の状態を復元する\n is_panel_display: (() => {\n switch (Utils.getSettingsItem('panel_display_state')) {\n case 'AlwaysDisplay':\n return true;\n case 'AlwaysFold':\n return false;\n case 'RestorePreviousState':\n return Utils.getSettingsItem('showed_panel_last_time');\n }\n })() as boolean,\n\n // フルスクリーン状態かどうか\n is_fullscreen: false,\n\n // IME 変換中かどうか\n is_ime_composing: false,\n\n // 仮想キーボードが表示されているか\n is_virtual_keyboard_display: false,\n\n // プレイヤーからのコメント送信から間もないかどうか\n is_comment_send_just_did: false,\n\n // インターバル ID\n // ページ遷移時に setInterval(), setTimeout() の実行を止めるのに使う\n // setInterval(), setTimeout() の返り値を登録する\n interval_ids: [] as number[],\n\n // コントロール表示切り替え用のインターバル ID\n // 混ぜるとダメなので独立させる\n control_interval_id: 0,\n\n // ***** チャンネル *****\n\n // チャンネル ID\n channel_id: this.$route.params.channel_id,\n\n // チャンネル情報\n // IChannelDefault に情報取得が完了するまでの間表示される初期値が定義されている\n channel: IChannelDefault,\n\n // 前のチャンネルのチャンネル情報\n channel_previous: IChannelDefault,\n\n // 次のチャンネルのチャンネル情報\n channel_next: IChannelDefault,\n\n // チャンネル情報リスト\n channels_list: new Map() as Map,\n\n // ***** プレイヤー *****\n\n // プレイヤー (DPlayer) のインスタンス\n player: null,\n\n // RomSound の AudioContext\n romsounds_context: null as AudioContext | null,\n\n // RomSound の AudioBuffer(音声データ)が入るリスト\n romsounds_buffers: [] as AudioBuffer[] | null,\n\n // イベントソースのインスタンス\n eventsource: null as EventSource | null,\n\n // フルスクリーン状態が切り替わったときのハンドラー\n fullscreen_handler: null as () => void | null,\n\n // キャプチャハンドラーのインスタンス\n capture_handler: null as PlayerCaptureHandler | null,\n\n // ***** キーボードショートカット *****\n\n // ショートカットキーのハンドラー\n shortcut_key_handler: null as (event: KeyboardEvent) => void | null,\n\n // ショートカットキーの最終押下時刻のタイムスタンプ\n shortcut_key_pressed_at: Date.now(),\n\n // キーボードショートカットの一覧のモーダルを表示するか\n shortcut_key_modal: false,\n\n // キーボードショートカットの一覧に表示するショートカットキーのリスト\n shortcut_key_list: {\n left_column: [\n {\n name: '全般',\n icon: 'fluent:home-20-filled',\n icon_height: '22px',\n shortcuts: [\n { name: '数字キー・テンキーに対応するリモコン番号 (1~12) の地デジチャンネルに切り替える', keys: [{name: '1~9, 0, -(=), ^(~)', icon: false}] },\n { name: '数字キー・テンキーに対応するリモコン番号 (1~12) の BS チャンネルに切り替える', keys: [{name: 'Shift', icon: false}, {name: '1~9, 0, -(=), ^(~)', icon: false}] },\n { name: '前のチャンネルに切り替える', keys: [{name: 'fluent:arrow-up-12-filled', icon: true}] },\n { name: '次のチャンネルに切り替える', keys: [{name: 'fluent:arrow-down-12-filled', icon: true}] },\n { name: 'キーボードショートカットの一覧を表示する', keys: [{name: '/(?)', icon: false}] },\n ]\n },\n {\n name: 'プレイヤー',\n icon: 'fluent:play-20-filled',\n icon_height: '20px',\n shortcuts: [\n { name: '再生 / 一時停止の切り替え', keys: [{name: 'Space', icon: false}] },\n { name: '再生 / 一時停止の切り替え (キャプチャタブ表示時)', keys: [{name: 'Shift', icon: false}, {name: 'Space', icon: false}] },\n { name: 'プレイヤーの音量を上げる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-up-12-filled', icon: true}] },\n { name: 'プレイヤーの音量を下げる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-down-12-filled', icon: true}] },\n { name: '停止して0.5秒早戻し', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-left-12-filled', icon: true}] },\n { name: '停止して0.5秒早送り', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-right-12-filled', icon: true}] },\n { name: 'フルスクリーンの切り替え', keys: [{name: 'F', icon: false}] },\n { name: 'ライブストリームの同期', keys: [{name: 'W', icon: false}] },\n { name: 'Picture-in-Picture の表示切り替え', keys: [{name: 'E', icon: false}] },\n { name: '字幕の表示切り替え', keys: [{name: 'S', icon: false}] },\n { name: 'コメントの表示切り替え', keys: [{name: 'D', icon: false}] },\n { name: '映像をキャプチャする', keys: [{name: 'C', icon: false}] },\n { name: '映像をコメントを付けてキャプチャする', keys: [{name: 'V', icon: false}] },\n { name: 'コメント入力フォームにフォーカスする', keys: [{name: 'M', icon: false}] },\n { name: 'コメント入力フォームを閉じる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'M', icon: false}] },\n ]\n },\n ],\n right_column: [\n {\n name: 'パネル',\n icon: 'fluent:panel-right-20-filled',\n icon_height: '24px',\n shortcuts: [\n { name: 'パネルの表示切り替え', keys: [{name: 'P', icon: false}] },\n { name: '番組情報タブを表示する', keys: [{name: 'K', icon: false}] },\n { name: 'チャンネルタブを表示する', keys: [{name: 'L', icon: false}] },\n { name: 'コメントタブを表示する', keys: [{name: ';(+)', icon: false}] },\n { name: 'Twitter タブを表示する', keys: [{name: ':(*)', icon: false}] },\n ]\n },\n {\n name: 'Twitter',\n icon: 'fa-brands:twitter',\n icon_height: '22px',\n shortcuts: [\n { name: 'ツイート検索タブを表示する', keys: [{name: '[ (「)', icon: false}] },\n { name: 'タイムラインタブを表示する', keys: [{name: '] (」)', icon: false}] },\n { name: 'キャプチャタブを表示する', keys: [{name: '\(¥)', icon: false}] },\n { name: 'ツイート入力フォームにフォーカスを当てる/フォーカスを外す', keys: [{name: 'Tab', icon: false}] },\n { name: 'キャプチャにフォーカスする', keys: [{name: 'キャプチャタブを表示', icon: false}, {name: 'fluent:arrow-up-12-filled;fluent:arrow-down-12-filled;fluent:arrow-left-12-filled;fluent:arrow-right-12-filled', icon: true}] },\n { name: 'キャプチャを拡大表示する/
キャプチャの拡大表示を閉じる', keys: [{name: 'キャプチャにフォーカス', icon: false}, {name: 'Enter', icon: false}] },\n { name: 'キャプチャを選択する/
キャプチャの選択を解除する', keys: [{name: 'キャプチャにフォーカス', icon: false}, {name: 'Space', icon: false}] },\n { name: 'クリップボード内の画像を
キャプチャとして取り込む', keys: [{name: 'ツイート入力
フォームにフォーカス', icon: false}, {name: Utils.CtrlOrCmd(), icon: false}, {name: 'V', icon: false}] },\n { name: 'ツイートを送信する', keys: [{name: 'Twitter タブを表示', icon: false}, {name: Utils.CtrlOrCmd(), icon: false}, {name: 'Enter', icon: false}] },\n ]\n },\n ]\n }\n }\n },\n // 開始時に実行\n async created() {\n\n // Virtual Keyboard API に対応している場合は、仮想キーボード周りの操作を自力で行うことをブラウザに伝える\n // この視聴画面のみ\n if ('virtualKeyboard' in navigator) {\n navigator.virtualKeyboard.overlaysContent = true;\n // 仮想キーボードが表示されたり閉じられたときのイベント\n navigator.virtualKeyboard.ongeometrychange = (event) => {\n if (event.target.boundingRect.width === 0 && event.target.boundingRect.height === 0) {\n this.is_virtual_keyboard_display = false;\n } else {\n this.is_virtual_keyboard_display = true;\n }\n }\n }\n\n // 再生セッションを初期化\n this.init();\n\n // RomSound を鳴らすための AudioContext を生成\n this.romsounds_context = new AudioContext();\n\n // 01 ~ 14 まですべての RomSound を読み込む\n for (let index = 1; index <= 14; index++) {\n\n // ArrayBuffer として RomSound を取得\n const url = `/assets/romsounds/${index.toString().padStart(2, '0')}.wav`;\n const audio_data = await Vue.axios.get(url, {\n baseURL: '', // BaseURL を明示的にクライアントのルートに設定\n responseType: 'arraybuffer',\n });\n\n // ArrayBuffer をデコードして AudioBuffer にし、すぐ呼び出せるように貯めておく\n // ref: https://ics.media/entry/200427/\n this.romsounds_buffers.push(await this.romsounds_context.decodeAudioData(audio_data.data));\n }\n },\n // 終了前に実行\n beforeDestroy() {\n\n // 仮想キーボード周りの操作をブラウザに戻す\n if ('virtualKeyboard' in navigator) {\n navigator.virtualKeyboard.overlaysContent = false;\n }\n\n // destroy() を実行\n // 別のページへ遷移するため、DPlayer のインスタンスを確実に破棄する\n // さもなければ、ブラウザがリロードされるまでバックグラウンドで永遠に再生され続けてしまう\n this.destroy(true);\n\n // AudioContext のリソースを解放\n this.romsounds_context.close();\n },\n // チャンネル切り替え時に実行\n // コンポーネント(インスタンス)は再利用される\n // ref: https://router.vuejs.org/ja/guide/advanced/navigation-guards.html#%E3%83%AB%E3%83%BC%E3%83%88%E5%8D%98%E4%BD%8D%E3%82%AB%E3%82%99%E3%83%BC%E3%83%88%E3%82%99\n beforeRouteUpdate(to, from, next) {\n\n // 前の再生セッションを破棄して終了する\n this.destroy();\n\n // チャンネル ID を次のチャンネルのものに切り替える\n this.channel_id = to.params.channel_id;\n\n // 既に取得済みのチャンネル情報で、前・現在・次のチャンネル情報を更新する\n [this.channel_previous, this.channel, this.channel_next]\n = ChannelUtils.getPreviousAndCurrentAndNextChannel(this.channels_list, this.channel_id);\n\n // ハッシュタグフォームのリセットがオンなら、ハッシュタグフォームを空にする\n if (Utils.getSettingsItem('reset_hashtag_when_program_switches') === true) {\n (this.$refs.Twitter as InstanceType).tweet_hashtag = '';\n }\n\n // 0.5秒だけ待ってから、新しい再生セッションを初期化する\n // 連続して押した時などに毎回再生処理を開始しないように猶予を設ける\n this.interval_ids.push(window.setTimeout(() => this.init(), 500));\n\n next();\n },\n watch: {\n // 前回視聴画面を開いた際にパネルが表示されていたかどうかを保存\n is_panel_display() {\n Utils.setSettingsItem('showed_panel_last_time', this.is_panel_display);\n }\n },\n methods: {\n\n // 再生セッションを初期化する\n init() {\n\n // ローディング中の背景画像をランダムで設定\n this.background_url = PlayerUtils.generatePlayerBackgroundURL();\n\n // コントロール表示タイマーを実行\n this.controlDisplayTimer();\n\n // チャンネル情報を取得\n this.update();\n\n // 現在時刻を1秒おきに更新\n this.interval_ids.push(window.setInterval(() => {\n this.time = dayjs().format('YYYY/MM/DD HH:mm:ss');\n }, 1 * 1000));\n\n // 00秒までの残り秒数\n // 現在 16:01:34 なら 26 (秒) になる\n const residue_second = 60 - (Math.floor(new Date().getTime() / 1000) % 60);\n\n // 00秒になるまで待ってから\n // 番組は基本1分単位で組まれているため、20秒や45秒など中途半端な秒数で更新してしまうと反映が遅れてしまう\n this.interval_ids.push(window.setTimeout(() => {\n\n // チャンネル情報を更新\n this.update();\n\n // チャンネル情報を定期的に更新\n this.interval_ids.push(window.setInterval(() => {\n this.update();\n }, 30 * 1000)); // 30秒おき\n\n }, residue_second * 1000));\n },\n\n // チャンネル情報一覧を取得し、画面を更新する\n async update() {\n\n // チャンネル ID が未定義なら実行しない(フェイルセーフ)\n if (this.$route.params.channel_id === undefined) {\n return;\n }\n\n // チャンネル情報 API にアクセス\n let channel_response: AxiosResponse;\n try {\n channel_response = await Vue.axios.get(`/channels/${this.channel_id}`);\n } catch (error) {\n\n // エラー内容を表示\n console.error(error);\n\n // ステータスコードが 422(チャンネルが存在しない)なら 404 ページにリダイレクト\n // 正確には 404 ページ自体がルートとして存在するわけじゃないけど、そもそも存在しないページなら 404 になるので\n if (error.response && error.response.status === 422 && error.response.data.detail === 'Specified channel_id was not found') {\n await this.$router.push({path: '/not-found/'});\n }\n\n // 処理を中断\n return;\n }\n\n // 取得したチャンネル情報と現在のチャンネル情報の NID-SID-EID の組み合わせが異なる場合\n // ハッシュタグフォームのリセットがオンなら、ハッシュタグフォームを空にする\n const channel_response_data = channel_response.data as IChannel;\n if ((this.channel.id !== channel_response_data.id) || // チャンネルが異なる\n (this.channel.program_present !== null && channel_response_data.program_present === null) || // 番組情報あり→番組情報なし\n (this.channel.program_present === null && channel_response_data.program_present !== null) || // 番組情報なし→番組情報あり\n (this.channel.program_present.id !== channel_response_data.program_present.id)) { // 番組が異なる\n if (Utils.getSettingsItem('reset_hashtag_when_program_switches') === true) {\n (this.$refs.Twitter as InstanceType).tweet_hashtag = '';\n }\n }\n\n // チャンネル情報を代入\n this.channel = channel_response_data;\n\n // プレイヤーがまだ初期化されていない or 他のチャンネルからの切り替えですでにプレイヤーが初期化されているけど破棄が可能\n // update() 自体は初期化時以外にも1分ごとに定期実行されるため、その際に毎回プレイヤーを再初期化しないようにする\n if (this.player === null || this.player.KonomiTVCanDestroy === true) {\n\n // プレイヤー (DPlayer) 周りのセットアップ\n this.initPlayer();\n\n // サーバーから送られてくるメッセージのイベントハンドラーを初期化\n this.initEventHandler();\n\n // キャプチャのイベントハンドラーを初期化\n this.initCaptureHandler();\n\n // ショートカットキーのイベントハンドラーを初期化\n // 事前に前のイベントハンドラーを削除しておかないと、重複してキー操作が実行されてしまう\n // 直前で実行しないと上下キーでのチャンネル操作が動かなくなる\n document.removeEventListener('keydown', this.shortcut_key_handler);\n this.initShortcutKeyHandler();\n }\n\n // 副音声がない番組でプレイヤー上で副音声に切り替えられないように\n // 音声多重放送でもデュアルモノでもない番組のみ\n if ((this.channel.program_present === null) ||\n ((this.channel.program_present.primary_audio_type !== '1/0+1/0モード(デュアルモノ)') &&\n (this.channel.program_present.secondary_audio_type === null))) {\n\n // クラスを付与\n this.player.template.audioItem[1].classList.add('dplayer-setting-audio-item--disabled');\n\n // 現在副音声が選択されている可能性を考慮し、明示的に主音声に切り替える\n if (this.player.plugins.mpegts) {\n window.setTimeout(() => { // プレイヤーの初期化が完了するまで少し待つ\n this.player.template.audioItem[0].classList.add('dplayer-setting-audio-current');\n this.player.template.audioItem[1].classList.remove('dplayer-setting-audio-current');\n this.player.template.audioValue.textContent = this.player.tran('Primary audio');\n try {\n this.player.plugins.mpegts.switchPrimaryAudio();\n } catch (error) {\n // pass\n }\n }, 300);\n }\n\n // 音声多重放送かデュアルモノなので、副音声への切り替えを有効化\n } else {\n\n // クラスを削除\n this.player.template.audioItem[1].classList.remove('dplayer-setting-audio-item--disabled');\n }\n\n // チャンネル情報一覧 API にアクセス\n // チャンネル情報 API と同時にアクセスするとむしろレスポンスが遅くなるので、返ってくるのを待ってから実行\n let channels_response: AxiosResponse;\n try {\n channels_response = await Vue.axios.get('/channels');\n } catch (error) {\n console.error(error); // エラー内容を表示\n return;\n }\n\n // is_display が true または現在表示中のチャンネルのみに絞り込むフィルタ関数\n // 放送していないサブチャンネルを表示から除外する\n const filter = (channel: IChannel) => {\n return channel.is_display || this.channel_id === channel.channel_id;\n }\n\n // チャンネルリストを再構築\n // 1つでもチャンネルが存在するチャンネルタイプのみ表示するように\n // たとえば SKY (スカパー!プレミアムサービス) のタブは SKY に属すチャンネルが1つもない(=受信できない)なら表示されない\n this.channels_list = new Map();\n this.channels_list.set('ピン留め', []); // ピン留めタブの準備\n if (channels_response.data.GR.length > 0) this.channels_list.set('地デジ', channels_response.data.GR.filter(filter));\n if (channels_response.data.BS.length > 0) this.channels_list.set('BS', channels_response.data.BS.filter(filter));\n if (channels_response.data.CS.length > 0) this.channels_list.set('CS', channels_response.data.CS.filter(filter));\n if (channels_response.data.CATV.length > 0) this.channels_list.set('CATV', channels_response.data.CATV.filter(filter));\n if (channels_response.data.SKY.length > 0) this.channels_list.set('SKY', channels_response.data.SKY.filter(filter));\n if (channels_response.data.STARDIGIO.length > 0) this.channels_list.set('StarDigio', channels_response.data.STARDIGIO.filter(filter));\n\n // ピン留めされているチャンネルの ID を取得\n const pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めされているチャンネル情報のリスト\n const pinned_channels = [] as IChannel[];\n\n // チャンネル ID が一致したチャンネルの情報を保存する\n for (const pinned_channel_id of pinned_channel_ids) {\n const pinned_channel_type = ChannelUtils.getChannelType(pinned_channel_id, true) as ChannelTypePretty;\n const pinned_channel = this.channels_list.get(pinned_channel_type).find((channel) => {\n return channel.channel_id === pinned_channel_id; // チャンネル ID がピン留めされているチャンネルのものと同じ\n });\n // チャンネル情報を取得できているときだけ\n // サブチャンネルをピン留めしたが、マルチ編成が終了して現在は放送していない場合などに備える (BS142 など)\n // 現在放送していないチャンネルは this.channels_list に入れた段階で弾いているため、チャンネル情報を取得できない\n if (pinned_channel !== undefined) {\n pinned_channels.push(pinned_channel);\n }\n }\n\n // pinned_channels に何か入っていたらピン留めタブを表示するし、そうでなければ表示しない\n if (pinned_channels.length > 0) {\n this.channels_list.set('ピン留め', pinned_channels);\n } else {\n this.channels_list.delete('ピン留め');\n }\n\n // 前と次のチャンネル ID を取得する\n [this.channel_previous, , this.channel_next] = ChannelUtils.getPreviousAndCurrentAndNextChannel(this.channels_list, this.channel_id);\n\n // MediaSession API を使い、メディア通知の表示をカスタマイズ\n if ('mediaSession' in navigator) {\n\n // アートワークとして表示するアイコン\n const artwork = [\n {src: '/assets/images/icons/icon-maskable-192px.png', sizes: '192x192', type: 'image/png'},\n {src: '/assets/images/icons/icon-maskable-512px.png', sizes: '512x512', type: 'image/png'},\n ];\n\n // メディア通知の表示をカスタマイズ\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel.program_present ? this.channel.program_present.title : '放送休止',\n artist: this.channel.channel_name,\n artwork: artwork,\n });\n\n // 再生状況のステータスを設定\n if ('setPositionState' in navigator.mediaSession) {\n navigator.mediaSession.setPositionState({\n duration: 0, // ライブなので0(長さなしを表すらしい)に設定\n playbackRate: 1, // ライブなので再生速度は常に1になる\n });\n }\n\n // メディア通知上のボタンが押されたときのイベント\n navigator.mediaSession.setActionHandler('play', () => { this.player.play() }); // 再生\n navigator.mediaSession.setActionHandler('pause', () => { this.player.pause() }); // 停止\n navigator.mediaSession.setActionHandler('previoustrack', async () => { // 前のチャンネルに切り替え\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel_previous.program_present ? this.channel_previous.program_present.title : '放送休止',\n artist: this.channel_previous.channel_name,\n artwork: artwork,\n });\n // ルーティングを前のチャンネルに置き換える\n await this.$router.push({path: `/tv/watch/${this.channel_previous.channel_id}`});\n });\n navigator.mediaSession.setActionHandler('nexttrack', async () => { // 次のチャンネルに切り替え\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel_next.program_present ? this.channel_next.program_present.title : '放送休止',\n artist: this.channel_next.channel_name,\n artwork: artwork,\n });\n // ルーティングを次のチャンネルに置き換える\n await this.$router.push({path: `/tv/watch/${this.channel_next.channel_id}`});\n });\n }\n },\n\n // マウスが動いたりタップされた時のイベント\n // 3秒間何も操作がなければコントロールを非表示にする\n controlDisplayTimer(event: Event | null = null, is_player_event: boolean = false) {\n\n // タッチデバイスかどうか\n // DPlayer の UA 判定コードと同一\n const is_touch_device = /iPhone|iPad|iPod|Windows|Macintosh|Android|Mobile/i.test(navigator.userAgent) && 'ontouchend' in document;\n\n // タッチデバイスで mousemove 、あるいはタッチデバイス以外で touchmove か click が発火した時は実行じない\n if (is_touch_device == true && event !== null && event.type === 'mousemove') return;\n if (is_touch_device == false && event !== null && (event.type === 'touchmove' || event.type === 'click')) return;\n\n // 以前セットされたタイマーを止める\n window.clearTimeout(this.control_interval_id);\n\n // setTimeout に渡すタイマー関数\n const timeout = () => {\n\n // コメント入力フォームが表示されているときは実行しない\n // タイマーを掛け直してから抜ける\n if (this.player !== null && this.player.template.controller.classList.contains('dplayer-controller-comment')) {\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n return;\n }\n\n // コントロールを非表示にする\n this.is_control_display = false;\n\n // プレイヤーのコントロールと設定パネルを非表示にする\n if (this.player !== null) {\n this.player.controller.hide();\n this.player.setting.hide();\n }\n }\n\n // タッチデバイスでプレイヤー画面がクリックされたとき\n if (is_touch_device === true && is_player_event === true) {\n\n // プレイヤーのコントロールの表示状態に合わせる\n if (this.player.controller.isShow()) {\n\n // コントロールを表示する\n this.is_control_display = true;\n\n // プレイヤーのコントロールを表示する\n this.player.controller.show();\n\n // 3秒間何も操作がなければコントロールを非表示にする\n // 3秒間の間一度でもマウスが動けばタイマーが解除されてやり直しになる\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n\n } else {\n\n // コントロールを非表示にする\n this.is_control_display = false;\n\n // プレイヤーのコントロールと設定パネルを非表示にする\n this.player.controller.hide();\n this.player.setting.hide();\n }\n\n // それ以外の画面がクリックされたとき\n } else {\n\n // コントロールを表示する\n this.is_control_display = true;\n\n // プレイヤーのコントロールを表示する\n if (this.player !== null) {\n this.player.controller.show();\n }\n\n // 3秒間何も操作がなければコントロールを非表示にする\n // 3秒間の間一度でもマウスが動けばタイマーが解除されてやり直しになる\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n }\n },\n\n // プレイヤーを初期化する\n initPlayer() {\n\n // mpegts.js を window 直下に入れる\n // こうしないと DPlayer が mpegts.js を認識できない\n (window as any).mpegts = mpegts;\n\n // すでに DPlayer が初期化されている場合は破棄する\n // チャンネル切り替え時などが該当する\n if (this.player !== null && this.player.KonomiTVCanDestroy === true) {\n try {\n this.player.destroy();\n } catch (error) {\n // mpegts.js をうまく破棄できない場合\n if (this.player.plugins.mpegts !== undefined) {\n this.player.plugins.mpegts.destroy();\n }\n }\n this.player = null;\n }\n\n // 低遅延モードであれば低遅延向けの再生バッファを、そうでなければ通常の再生バッファをセット (秒単位)\n const playback_buffer_sec = Utils.getSettingsItem('tv_low_latency_mode') ?\n PLAYBACK_BUFFER_SEC_LOW_LATENCY : PLAYBACK_BUFFER_SEC;\n\n // DPlayer を初期化\n this.player = new DPlayer({\n container: this.$el.querySelector('.watch-player__dplayer'),\n theme: '#E64F97', // テーマカラー\n lang: 'ja-jp', // 言語\n live: true, // ライブモード\n liveSyncMinBufferSize: playback_buffer_sec, // ライブモードで同期する際の最小バッファサイズ\n loop: false, // ループ再生 (ライブのため無効化)\n airplay: false, // AirPlay 機能 (うまく動かないため無効化)\n autoplay: true, // 自動再生\n hotkey: false, // ショートカットキー(こちらで制御するため無効化)\n screenshot: false, // スクリーンショット (こちらで制御するため無効化)\n volume: 1.0, // 音量の初期値\n // 映像\n video: {\n // デフォルトの品質\n // ラジオチャンネルでは常に 48KHz/192kbps に固定する\n defaultQuality: (this.channel.is_radiochannel) ? '48kHz/192kbps' : Utils.getSettingsItem('tv_streaming_quality'),\n // 品質リスト\n quality: (() => {\n const qualities = [];\n // ラジオチャンネル\n // API が受け付ける品質の値は通常のチャンネルと同じだが (手抜き…)、実際の品質は 48KHz/192kbps で固定される\n // ラジオチャンネルの場合は、1080p と渡しても 48kHz/192kbps 固定の音声だけの MPEG-TS が配信される\n if (this.channel.is_radiochannel) {\n qualities.push({\n name: '48kHz/192kbps',\n type: 'mpegts',\n url: `${Utils.api_base_url}/streams/live/${this.channel_id}/1080p/mpegts`,\n });\n // 通常のチャンネル\n } else {\n // ブラウザが H.265 / HEVC の再生に対応していて、かつ通信節約モードが有効なとき\n // API に渡す画質に -hevc のプレフィックスをつける\n let hevc_prefix = '';\n if (PlayerUtils.isHEVCVideoSupported() && Utils.getSettingsItem('tv_data_saver_mode') === true) {\n hevc_prefix = '-hevc';\n }\n for (const quality of ['1080p-60fps', '1080p', '810p', '720p', '540p', '480p', '360p', '240p']) {\n qualities.push({\n name: quality === '1080p-60fps' ? '1080p (60fps)' : quality,\n type: 'mpegts',\n url: `${Utils.api_base_url}/streams/live/${this.channel_id}/${quality}${hevc_prefix}/mpegts`,\n });\n }\n }\n return qualities;\n })(),\n },\n // コメント\n danmaku: {\n user: 'KonomiTV', // 便宜上 KonomiTV に固定\n speedRate: Utils.getSettingsItem('comment_speed_rate'), // コメントの流れる速度\n fontSize: Utils.getSettingsItem('comment_font_size'), // コメントのフォントサイズ\n },\n // コメント API バックエンド\n apiBackend: {\n // コメント受信時\n read: (options) => {\n // 成功したことにして通知を抑制\n options.success([{}]);\n },\n // コメント送信時\n send: async (options) => {\n // Comment コンポーネント内のコメント送信メソッドを呼び出す\n // ref: https://stackoverflow.com/a/65729556/17124142 ($refs への型設定)\n await (this.$refs.Comment as InstanceType).sendComment(options);\n },\n },\n // プラグイン\n pluginOptions: {\n // mpegts.js\n mpegts: {\n config: {\n // Web Worker を有効にする\n enableWorker: true,\n // HTMLMediaElement の内部バッファによるライブストリームの遅延を追跡する\n // liveBufferLatencyChasing と異なり、いきなり再生時間をスキップするのではなく、\n // 再生速度を少しだけ上げることで再生を途切れさせることなく遅延を追跡する\n liveSync: Utils.getSettingsItem('tv_low_latency_mode'),\n // 許容する HTMLMediaElement の内部バッファの最大値 (秒単位, 3秒)\n liveSyncMaxLatency: 3,\n // HTMLMediaElement の内部バッファ (遅延) が liveSyncMaxLatency を超えたとき、ターゲットとする遅延時間 (秒単位)\n liveSyncTargetLatency: playback_buffer_sec,\n // ライブストリームの遅延の追跡に利用する再生速度 (x1.1)\n // 遅延が 3 秒を超えたとき、遅延が playback_buffer_sec を下回るまで再生速度が x1.1 に設定される\n liveSyncPlaybackRate: 1.1,\n }\n },\n // aribb24.js\n aribb24: {\n // 描画フォント\n normalFont: `\"${Utils.getSettingsItem('caption_font')}\", sans-serif`,\n // 縁取りする色\n forceStrokeColor: Utils.getSettingsItem('always_border_caption_text') ? true : false,\n // 背景色\n forceBackgroundColor: Utils.getSettingsItem('specify_caption_background_color') ?\n Utils.getSettingsItem('caption_background_color') : null,\n // DRCS 文字を対応する Unicode 文字に置換\n drcsReplacement: true,\n // 高解像度の字幕 Canvas を取得できるように\n enableRawCanvas: true,\n // 縁取りに strokeText API を利用\n useStrokeText: true,\n // Unicode 領域の代わりに私用面の領域を利用 (Windows TV 系フォントのみ)\n usePUA: (() => {\n const font = Utils.getSettingsItem('caption_font') as string;\n const context = document.createElement('canvas').getContext('2d');\n context.font = `10px ${font}`;\n context.fillText('Test', 0, 0);\n if (font.startsWith('Windows TV')) {\n return true;\n } else {\n return false;\n }\n })(),\n // 文字スーパーの PRA (内蔵音再生コマンド) のコールバックを指定\n PRACallback: async (index: number) => {\n\n // 設定で文字スーパーが無効なら実行しない\n if (Utils.getSettingsItem('tv_show_superimpose') === false) return;\n\n // index に応じた内蔵音を鳴らす\n // ref: https://ics.media/entry/200427/\n // ref: https://www.ipentec.com/document/javascript-web-audio-api-change-volume\n\n // 自動再生ポリシーに引っかかったなどで AudioContext が一時停止されている場合、一度 resume() する必要がある\n // resume() するまでに何らかのユーザーのジェスチャーが行われているはず…\n // なくても動くこともあるみたいだけど、念のため\n if (this.romsounds_context.state === 'suspended') {\n await this.romsounds_context.resume();\n }\n\n // index で指定された音声データを読み込み\n const buffer_source_node = this.romsounds_context.createBufferSource();\n buffer_source_node.buffer = this.romsounds_buffers[index];\n\n // GainNode につなげる\n const gain_node = this.romsounds_context.createGain();\n buffer_source_node.connect(gain_node);\n\n // 出力につなげる\n gain_node.connect(this.romsounds_context.destination);\n\n // 音量を元の wav の3倍にする (1倍だと結構小さめ)\n gain_node.gain.value = 3;\n\n // 再生開始\n buffer_source_node.start(0);\n },\n }\n },\n // 字幕\n subtitle: {\n type: 'aribb24', // aribb24.js を有効化\n }\n });\n\n // デバッグ用にプレイヤーインスタンスも window 直下に入れる\n (window as any).player = this.player;\n\n // プレイヤー側のコントロール非表示タイマーを無効化(上書き)\n // 無効化しておかないと、controlDisplayTimer() と競合してしまう\n // 上書き元のコードは https://github.com/tsukumijima/DPlayer/blob/master/src/js/controller.js#L387-L395 にある\n this.player.controller.setAutoHide = (time: number) => {};\n\n // ***** コメント送信時のイベントハンドラー *****\n\n // コメントが送信されたときに、プレイヤーからのコメント送信から間もないかどうかのフラグを立てる (0.1秒後に解除する)\n // コメントを送信するとコメント入力フォームへのフォーカスが外れるため、ページ全体の keydown イベントでは\n // Enter キーの押下がコメント送信由来のイベントかキャプチャ拡大表示由来のイベントかを判断できない\n // そこで、コメント入力フォームフォーカス中に Enter キーが押された場合(=コメント送信時)に 0.1 秒間フラグを立てることで、\n // ショートカットキーハンドラーがコメント送信由来のイベントであることを判定できるようにしている\n this.player.template.commentInput.addEventListener('keydown', (event) => {\n if (event.code === 'Enter') {\n this.is_comment_send_just_did = true;\n setTimeout(() => this.is_comment_send_just_did = false, 100);\n }\n });\n\n // 「コメント送信後にコメント入力フォームを閉じる」がオフになっている時のために、プレイヤー側のコメント送信関数を上書き\n // 上書き部分以外の処理内容は概ね https://github.com/tsukumijima/DPlayer/blob/master/src/js/comment.js に準じる\n this.player.comment.send = () => {\n\n // コメント入力フォームへのフォーカスを外す (「コメント送信後にコメント入力フォームを閉じる」がオンのときだけ)\n if (Utils.getSettingsItem('close_comment_form_after_sending') === true) {\n this.player.template.commentInput.blur();\n }\n\n // 空コメントを弾く\n if (!this.player.template.commentInput.value.replace(/^\\s+|\\s+$/g, '')) {\n this.player.notice(this.player.tran('Please input danmaku content!'));\n return;\n }\n\n // コメントを送信\n this.player.danmaku.send(\n {\n text: this.player.template.commentInput.value,\n color: this.player.container.querySelector('.dplayer-comment-setting-color input:checked').value,\n type: this.player.container.querySelector('.dplayer-comment-setting-type input:checked').value,\n size: this.player.container.querySelector('.dplayer-comment-setting-size input:checked').value,\n },\n // 送信完了後にコメント入力フォームを閉じる ([コメント送信後にコメント入力フォームを閉じる] がオンのときだけ)\n () => {\n if (Utils.getSettingsItem('close_comment_form_after_sending') === true) {\n this.player.comment.hide();\n }\n },\n true,\n );\n\n // 重複送信を防ぐ\n this.player.template.commentInput.value = '';\n };\n\n // ***** 設定パネルのショートカット一覧へのリンクのイベントハンドラー *****\n\n // 設定パネルにショートカット一覧を表示するリンクを動的に追加する\n // タッチデバイスでは実行しない\n const is_touch_device = /iPhone|iPad|iPod|Macintosh|Android|Mobile/i.test(navigator.userAgent) && 'ontouchend' in document;\n if (is_touch_device === false) {\n this.player.template.settingOriginPanel.insertAdjacentHTML('beforeend', `\n
\n キーボードショートカット\n
\n \n \n \n
\n
`)\n\n // 設定パネルの高さを再設定\n const settingOriginPanelHeight = this.player.template.settingOriginPanel.scrollHeight;\n this.player.template.settingBox.style.clipPath = `inset(calc(100% - ${settingOriginPanelHeight}px) 0 0 round 7px)`;\n\n // 設定パネルのショートカット一覧を表示するリンクがクリックされたときのイベント\n // リアクティブではないので、手動でやらないといけない…\n this.$el.querySelector('.dplayer-setting-keyboard-shortcut').addEventListener('click', () => {\n this.player.setting.hide(); // 設定パネルを閉じる\n this.shortcut_key_modal = true;\n });\n }\n\n // ***** フルスクリーンのイベントハンドラー *****\n\n // フルスクリーンにするコンテナ要素(ページ全体)\n const fullscreen_container = document.querySelector('.v-application');\n this.fullscreen_handler = () => this.is_fullscreen = this.player.fullScreen.isFullScreen();\n if (fullscreen_container.onfullscreenchange !== undefined) {\n fullscreen_container.addEventListener('fullscreenchange', this.fullscreen_handler);\n } else {\n fullscreen_container.addEventListener('webkitfullscreenchange', this.fullscreen_handler);\n }\n\n // DPlayer のフルスクリーン関係のメソッドを無理やり上書きし、KonomiTV の UI と統合する\n // 上書き元のコードは https://github.com/tsukumijima/DPlayer/blob/master/src/js/fullscreen.js にある\n // フルスクリーンかどうか\n this.player.fullScreen.isFullScreen = (type: string) => {\n return !!(document.fullscreenElement || document.webkitFullscreenElement);\n }\n // フルスクリーンをリクエスト\n this.player.fullScreen.request = (type: string) => {\n\n // すでにフルスクリーンだったらキャンセルする\n if (this.player.fullScreen.isFullScreen()) {\n this.player.fullScreen.cancel();\n return;\n }\n\n // フルスクリーンをリクエスト\n // Safari は webkit のベンダープレフィックスが必要\n fullscreen_container.requestFullscreen = fullscreen_container.requestFullscreen || fullscreen_container.webkitRequestFullscreen;\n if (fullscreen_container.requestFullscreen) {\n fullscreen_container.requestFullscreen();\n }\n\n // 画面の向きを横に固定 (Screen Orientation API がサポートされている場合)\n if (screen.orientation) {\n screen.orientation.lock('landscape').catch(() => {});\n }\n }\n // フルスクリーンをキャンセル\n this.player.fullScreen.cancel = (type: string) => {\n\n // フルスクリーンを終了\n // Safari は webkit のベンダープレフィックスが必要\n document.exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen;\n if (document.exitFullscreen) {\n document.exitFullscreen();\n }\n\n // 画面の向きの固定を解除\n if (screen.orientation) {\n screen.orientation.unlock();\n }\n }\n\n // ***** 再生/停止/画質切り替え時のイベントハンドラー *****\n\n // 再生/停止されたとき\n // 通知バーからの制御など、画面から以外の外的要因で再生/停止が行われる事もある\n const on_play_or_pause = () => {\n\n // まだ設定パネルが表示されていたら非表示にする\n this.player.setting.hide();\n\n // コントロールを表示する\n this.controlDisplayTimer();\n }\n this.player.on('play', on_play_or_pause);\n this.player.on('pause', on_play_or_pause);\n\n // 画質の切り替えが開始されたときのイベント\n this.player.on('quality_start', () => {\n\n // ローディング中の背景画像をランダムで設定\n this.background_url = PlayerUtils.generatePlayerBackgroundURL();\n\n // イベントソースを閉じる\n if (this.eventsource !== null) {\n this.eventsource.close();\n this.eventsource = null;\n }\n\n // 新しい EventSource を作成\n // 画質ごとにイベント API は異なるため、一度破棄してから作り直す\n this.initEventHandler();\n });\n\n // 停止状態でかつ再生時間からバッファが 30 秒以上離れていないかを1分おきに監視し、そうなっていたら強制的にシークする\n // mpegts.js の仕様上、MSE に未再生のバッファがたまり過ぎると SourceBuffer が追加できなくなるため、強制的に接続が切断されてしまう\n this.interval_ids.push(window.setInterval(() => {\n if (this.player.video.paused && this.player.video.buffered.end(0) - this.player.video.currentTime > 30) {\n this.player.sync();\n }\n }, 60 * 1000));\n\n // ***** 文字スーパーのイベントハンドラー *****\n\n // 設定で文字スーパーが有効\n // 字幕が非表示の場合でも、文字スーパーは表示する\n if (Utils.getSettingsItem('tv_show_superimpose') === true) {\n this.player.plugins.aribb24Superimpose.show();\n this.player.on('subtitle_hide', () => {\n this.player.plugins.aribb24Superimpose.show();\n });\n // 設定で文字スーパーが無効\n } else {\n this.player.plugins.aribb24Superimpose.hide();\n this.player.on('subtitle_show', () => {\n this.player.plugins.aribb24Superimpose.hide();\n });\n }\n },\n\n // イベントハンドラーを初期化する\n initEventHandler() {\n\n // ***** プレイヤー再生開始時のイベントハンドラー *****\n\n // 必ず最初はローディング状態とする\n this.is_loading = true;\n\n // 音量を 0 に設定\n this.player.video.volume = 0;\n\n // 再生バッファを調整し、再生準備ができた段階でプレイヤーの背景を非表示にするイベントを登録\n // 実際に再生可能になるのを待ってから実行する\n // 画質切り替え時にも実行する必要があるので、あえてこの位置に記述している\n const on_canplay = () => {\n\n // 自分自身のイベントを登録解除 (重複実行を避ける)\n this.player.video.oncanplay = null;\n this.player.video.oncanplaythrough = null;\n\n // 再生バッファ調整のため、一旦停止させる\n // this.player.video.pause() を使うとプレイヤーの UI アイコンが停止してしまうので、代わりに playbackRate を使う\n this.player.video.playbackRate = 0;\n\n // 念のためさらに少しだけ待ってから\n // あえて await で待たずに非同期コールバックで実行している\n window.setTimeout(async () => {\n\n // 再生バッファを取得する (取得に失敗した場合は 0 を返す)\n const get_playback_buffer_sec = (): number => {\n try {\n return (Math.round((this.player.video.buffered.end(0) - this.player.video.currentTime) * 1000) / 1000);\n } catch (error) {\n // まだ再生準備が整っていないなどの理由で、再生バッファの取得に失敗した場合\n return 0;\n }\n }\n\n // 低遅延モードであれば低遅延向けの再生バッファを、そうでなければ通常の再生バッファをセット (秒単位)\n const playback_buffer_sec = Utils.getSettingsItem('tv_low_latency_mode') ?\n PLAYBACK_BUFFER_SEC_LOW_LATENCY : PLAYBACK_BUFFER_SEC;\n\n // 再生バッファが playback_buffer_sec を超えるまで 0.1 秒おきに再生バッファをチェックする\n // 再生バッファが playback_buffer_sec を切ると再生が途切れやすくなるので (特に動きの激しい映像)、\n // 再生開始までの時間を若干犠牲にして、再生バッファの調整と同期に時間を割く\n // playback_buffer_sec の値は mpegts.js に渡す liveSyncTargetLatency プロパティに渡す値と共通\n let current_playback_buffer_sec = get_playback_buffer_sec();\n while (current_playback_buffer_sec < playback_buffer_sec) {\n await Utils.sleep(0.1);\n current_playback_buffer_sec = get_playback_buffer_sec();\n }\n\n // 再生開始\n this.player.video.playbackRate = 1;\n\n // 再生が一時的に止まってバッファリングしているとき/再び再生されはじめたときのイベント\n // バッファリングの Progress Circular の表示を制御する\n // 同期が終わってからの方が都合が良い\n this.player.video.addEventListener('waiting', () => this.is_video_buffering = true);\n this.player.video.addEventListener('playing', () => this.is_video_buffering = false);\n\n // ローディング状態を解除し、映像を表示する\n this.is_loading = false;\n\n // バッファリング中の Progress Circular を非表示にする\n this.is_video_buffering = false;\n\n if (this.channel.is_radiochannel) {\n // ラジオチャンネルでは引き続き映像の代わりとして背景画像を表示し続ける\n this.is_background_display = true;\n } else {\n // 背景画像をフェードアウト\n this.is_background_display = false;\n }\n\n // 再生開始時に音量を徐々に上げる\n // いきなり再生されるよりも体験が良い\n const current_volume: number = this.player.user.get('volume');\n while ((this.player.video.volume + 0.05) < current_volume) {\n // 小数第2位以下を切り捨てて、浮動小数の誤差で 1 (100%) を微妙に超えてしまいエラーになるのを避ける\n this.player.video.volume = Utils.mathFloor(this.player.video.volume + 0.05, 2);\n await Utils.sleep(0.02);\n }\n this.player.video.volume = current_volume;\n\n }, 100);\n }\n this.player.video.oncanplay = on_canplay;\n this.player.video.oncanplaythrough = on_canplay;\n\n // ***** KonomiTV サーバーのイベント API のイベントハンドラー *****\n\n // EventSource を作成\n this.eventsource = new EventSource((this.player.quality.url as string).replace('/mpegts', '/events'));\n\n // 初回接続時のイベント\n this.eventsource.addEventListener('initial_update', (event_raw: MessageEvent) => {\n\n // ステータスが Standby であれば\n const event = JSON.parse(event_raw.data);\n if (event.status === 'Standby') {\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n }\n });\n\n // ステータスが更新されたときのイベント\n this.eventsource.addEventListener('status_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n console.log(`Status: ${event.status} / Detail: ${event.detail}`);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n\n // ステータスごとに処理を振り分け\n switch (event.status) {\n\n // Status: Standby\n case 'Standby': {\n\n // ステータス詳細をプレイヤーに表示\n if (!this.player.template.notice.textContent.includes('画質を')) { // 画質切り替えの表示を上書きしない\n this.player.notice(event.detail, -1);\n }\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n break;\n }\n\n // Status: ONAir\n case 'ONAir': {\n\n // ステータス詳細をプレイヤーから削除\n if (!this.player.template.notice.textContent.includes('画質を')) { // 画質切り替えの表示を上書きしない\n this.player.notice(this.player.template.notice.textContent, 0.000001);\n }\n\n // 前のプレイヤーインスタンスの Picture-in-Picture ウインドウが残っている場合、終了させてからもう一度切り替える\n // チャンネル切り替えが完了しても前の Picture-in-Picture ウインドウは再利用されないため、一旦終了させるしかない\n if (document.pictureInPictureElement) {\n document.exitPictureInPicture();\n this.player.video.requestPictureInPicture();\n }\n break;\n }\n\n // Status: Restart\n case 'Restart': {\n\n // ステータス詳細をプレイヤーに表示\n this.player.notice(event.detail, -1);\n\n // プレイヤーを再起動する\n this.player.switchVideo({\n url: this.player.quality.url,\n type: this.player.quality.type,\n });\n\n // 再起動しただけでは自動再生されないので、明示的に\n this.player.play();\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n break;\n }\n\n // Status: Offline\n case 'Offline': {\n\n // 基本的に Offline は放送休止中やエラーなどで復帰の見込みがない状態\n\n // ステータス詳細をプレイヤーに表示\n // 動画の読み込みエラーが送出された時にメッセージを上書きする\n this.player.notice(event.detail, -1);\n this.player.video.onerror = () => {\n this.player.notice(event.detail, -1);\n this.player.video.onerror = null;\n }\n\n // 描画されたコメントをクリア\n this.player.danmaku.clear()\n\n // 動画を停止する\n this.player.video.pause();\n\n // イベントソースを閉じる(復帰の見込みがないため)\n this.eventsource.close();\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n\n // バッファリング中の Progress Circular を非表示にする\n this.is_loading = false;\n this.is_video_buffering = false;\n break;\n }\n }\n });\n\n // ステータス詳細が更新されたときのイベント\n this.eventsource.addEventListener('detail_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n console.log(`Status: ${event.status} Detail:${event.detail}`);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n\n // Standby のときだけプレイヤーに表示\n if (event.status === 'Standby') {\n this.player.notice(event.detail, -1);\n\n // プレイヤーの背景を表示する\n if (!this.is_background_display) {\n this.is_background_display = true;\n }\n }\n });\n\n // クライアント数(だけ)が更新されたときのイベント\n this.eventsource.addEventListener('clients_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n });\n },\n\n // ショートカットキーを初期化する\n initShortcutKeyHandler() {\n\n const twitter_component = (this.$refs.Twitter as InstanceType);\n const tweet_form_element = twitter_component.$el.querySelector('.tweet-form__textarea');\n\n // IME 変換中の状態を保存する\n for (const element of document.querySelectorAll('input[type=text],input[type=search],textarea')) {\n element.addEventListener('compositionstart', () => this.is_ime_composing = true);\n element.addEventListener('compositionend', () => this.is_ime_composing = false);\n }\n\n // ショートカットキーハンドラー\n this.shortcut_key_handler = async (event: KeyboardEvent) => {\n\n const tag = document.activeElement.tagName.toUpperCase();\n const editable = document.activeElement.getAttribute('contenteditable');\n\n // 矢印キーのデフォルトの挙動(スクロール)を抑制\n // キーリピート周りで間引かれるイベントでも event.preventDefault() しないとスクロールしてしまうため、\n // 一番最初のタイミングでやっておく\n // input・textarea・contenteditable 状態の要素では実行しない\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code) &&\n (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true')) {\n event.preventDefault();\n }\n\n // キーリピート(押しっぱなし)状態の場合は基本実行しない\n // 押し続けると何度も同じ動作が実行されて大変な事になる…\n // ただ、キーリピートを使いたい場合もあるので、リピート状態をフラグとして保存する\n let is_repeat = false;\n if (event.repeat) is_repeat = true;\n\n // キーリピート状態は event.repeat を見る事でだいたい検知できるが、最初の何回かは検知できないこともある\n // そこで、0.05 秒以内に連続して発火したキーイベントは間引きも兼ねて実行しない\n const now = Date.now();\n if (now - this.shortcut_key_pressed_at < (0.05 * 1000)) return;\n this.shortcut_key_pressed_at = now; // 最終押下時刻を更新\n\n // 無名関数の中で実行する\n const result = await (async (): Promise => {\n\n // ***** ツイート入力フォームにフォーカスを当てる/フォーカスを外す *****\n\n // ツイート入力フォームにフォーカスしているときもこのショートカットが動くようにする\n // 以降の if 文で textarea フォーカス時のイベントをすべて弾いてしまっているため、前に持ってきている\n // Tab キーに割り当てている関係で、IME 変換中は実行しない(IME 変換中に実行すると文字変換ができなくなる)\n if (((tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') ||\n (document.activeElement === tweet_form_element)) && this.is_ime_composing === false) {\n if (event.code === 'Tab') {\n\n // ツイート入力フォームにフォーカスがすでに当たっていたら、フォーカスを外して終了\n if (document.activeElement === tweet_form_element) {\n tweet_form_element.blur();\n return true;\n }\n\n // パネルを開く\n this.is_panel_display = true;\n\n // どのタブを開いていたかに関係なく Twitter タブに切り替える\n this.tv_panel_active_tab = 'Twitter';\n\n // ツイート入力フォームの textarea 要素にフォーカスを当てる\n tweet_form_element.focus();\n\n // フォーカスを当てると勝手に横方向にスクロールされてしまうので、元に戻す\n this.$el.scrollLeft = 0;\n\n window.setTimeout(() => {\n\n // 他のタブから切り替えると一発でフォーカスが当たらないことがあるので、ちょっとだけ待ってから念押し\n // $nextTick() だと上手くいかなかった…\n tweet_form_element.focus();\n\n // フォーカスを当てると勝手に横方向にスクロールされてしまうので、元に戻す\n this.$el.scrollLeft = 0;\n\n }, 100); // 0.1秒\n\n return true;\n }\n }\n\n // ***** ツイートを送信する *****\n\n // ツイート入力フォームにフォーカスしているときもこのショートカットが動くようにする\n // Twitter タブ以外を開いているときは実行しない\n // 以降の if 文で textarea フォーカス時のイベントをすべて弾いてしまっているため、前に持ってきている\n if (((tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') ||\n (document.activeElement === tweet_form_element)) &&\n this.tv_panel_active_tab === 'Twitter' &&\n this.is_ime_composing === false) {\n // (Ctrl or Cmd or Shift) + Enter\n // Shift + Enter は隠し機能(間違えたとき用)\n if ((event.ctrlKey || event.metaKey || event.shiftKey) && event.code === 'Enter') {\n twitter_component.$el.querySelector('.tweet-button').click();\n return true;\n }\n }\n\n // ***** コメント入力フォームを閉じる *****\n\n // プレイヤーが初期化されていない時・Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.shiftKey && !event.altKey) {\n\n // コメント入力フォームが表示されているときのみ\n if (this.player.template.controller.classList.contains('dplayer-controller-comment')) {\n // Ctrl or Cmd + M\n if ((event.ctrlKey || event.metaKey) && event.code === 'KeyM') {\n this.player.comment.hide();\n return true;\n }\n }\n }\n\n // input・textarea・contenteditable 状態の要素でなければ\n // 文字入力中にショートカットキーが作動してしまわないように\n if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') {\n\n // キーリピートでない時・Ctrl / Cmd / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.altKey) {\n\n // ***** 数字キーでチャンネルを切り替える *****\n\n // Ctrl / Cmd キーが同時押しされていたら BS チャンネルの方を選局する\n const switch_channel_type = (event.shiftKey) ? 'BS' : 'GR';\n\n // 1~9キー\n let switch_remocon_id = null;\n if (event.code === 'Digit1' || event.code === 'Digit2' || event.code === 'Digit3' ||\n event.code === 'Digit4' || event.code === 'Digit5' || event.code === 'Digit6' ||\n event.code === 'Digit7' || event.code === 'Digit8' || event.code === 'Digit9') {\n switch_remocon_id = Number(event.code.replace('Digit', ''));\n }\n // 0キー: 10に割り当て\n if (event.code === 'Digit0') switch_remocon_id = 10;\n // -キー: 11に割り当て\n if (event.code === 'Minus') switch_remocon_id = 11;\n // ^キー: 12に割り当て\n if (event.code === 'Equal') switch_remocon_id = 12;\n // 1~9キー (テンキー)\n if (event.code === 'Numpad1' || event.code === 'Numpad2' || event.code === 'Numpad3' ||\n event.code === 'Numpad4' || event.code === 'Numpad5' || event.code === 'Numpad6' ||\n event.code === 'Numpad7' || event.code === 'Numpad8' || event.code === 'Numpad9') {\n switch_remocon_id = Number(event.code.replace('Numpad', ''));\n }\n // 0キー (テンキー): 10に割り当て\n if (event.code === 'Numpad0') switch_remocon_id = 10;\n\n // この時点でリモコン番号が取得できていたら実行\n if (switch_remocon_id !== null) {\n\n // 切り替え先のチャンネルを取得する\n const switch_channel = ChannelUtils.getChannelFromRemoconID(\n this.channels_list, switch_channel_type, switch_remocon_id);\n\n // チャンネルが取得できていれば、ルーティングをそのチャンネルに置き換える\n // 押されたキーに対応するリモコン番号のチャンネルがない場合や、現在と同じチャンネル ID の場合は何も起こらない\n if (switch_channel !== null && switch_channel.channel_id !== this.channel_id) {\n await this.$router.push({path: `/tv/watch/${switch_channel.channel_id}`});\n return true;\n }\n }\n }\n\n // キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // ***** キーボードショートカットの一覧を表示する *****\n\n // /(?)キー: キーボードショートカットの一覧を表示する\n if (event.code === 'Slash') {\n this.shortcut_key_modal = !this.shortcut_key_modal;\n return true;\n }\n\n // ***** パネルのタブを切り替える *****\n\n // Pキー: パネルの表示切り替え\n if (event.code === 'KeyP') {\n this.is_panel_display = !this.is_panel_display;\n return true;\n }\n // Kキー: 番組情報タブ\n if (event.code === 'KeyK') {\n this.tv_panel_active_tab = 'Program';\n return true;\n }\n // Lキー: チャンネルタブ\n if (event.code === 'KeyL') {\n this.tv_panel_active_tab = 'Channel';\n return true;\n }\n // ;(+)キー: コメントタブ\n if (event.code === 'Semicolon') {\n this.tv_panel_active_tab = 'Comment';\n return true;\n }\n // :(*)キー: Twitterタブ\n if (event.code === 'Quote') {\n this.tv_panel_active_tab = 'Twitter';\n return true;\n }\n\n // ***** Twitter タブ内のタブを切り替える *****\n\n // [(「): ツイート検索タブ\n if (event.code === 'BracketRight') {\n twitter_component.twitter_active_tab = 'Search';\n return true;\n }\n // ](」): タイムラインタブ\n if (event.code === 'Backslash') {\n twitter_component.twitter_active_tab = 'Timeline';\n return true;\n }\n // \\(¥)キー: キャプチャタブ\n if (event.code === 'IntlRo') {\n twitter_component.twitter_active_tab = 'Capture';\n return true;\n }\n }\n\n // Twitter タブ内のキャプチャタブが表示されている & Ctrl / Cmd / Shift / Alt のいずれも押されていないときだけ\n // キャプチャタブが表示されている時は、プレイヤー操作側の矢印キー/スペースキーのショートカットは動作しない(キーが重複するため)\n if (this.tv_panel_active_tab === 'Twitter' && twitter_component.twitter_active_tab === 'Capture' &&\n (!event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey)) {\n\n // ***** キャプチャにフォーカスする *****\n\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code)) {\n\n // キャプチャリストに一枚もキャプチャがない\n if (twitter_component.captures.length === 0) return false;\n\n // まだどのキャプチャにもフォーカスされていない場合は、一番新しいキャプチャにフォーカスして終了\n if (twitter_component.captures.some(capture => capture.focused === true) === false) {\n twitter_component.captures[twitter_component.captures.length - 1].focused = true;\n return true;\n }\n\n // 現在フォーカスされているキャプチャのインデックスを取得\n const focused_capture_index = twitter_component.captures.findIndex(capture => capture.focused === true);\n\n // ↑キー: 2つ前のキャプチャにフォーカスする\n // キャプチャリストは2列で並んでいるので、2つ後のキャプチャが現在フォーカスされているキャプチャの直上になる\n if (event.code === 'ArrowUp') {\n // 2つ前のキャプチャがないなら実行しない\n if (focused_capture_index - 2 < 0) return false;\n twitter_component.captures[focused_capture_index - 2].focused = true;\n }\n\n // ↓キー: 2つ後のキャプチャにフォーカスする\n // キャプチャリストは2列で並んでいるので、2つ後のキャプチャが現在フォーカスされているキャプチャの直下になる\n if (event.code === 'ArrowDown') {\n // 2つ後のキャプチャがないなら実行しない\n if (focused_capture_index + 2 > (twitter_component.captures.length - 1)) return false;\n twitter_component.captures[focused_capture_index + 2].focused = true;\n }\n\n // ←キー: 1つ前のキャプチャにフォーカスする\n if (event.code === 'ArrowLeft') {\n // 1つ前のキャプチャがないなら実行しない\n if (focused_capture_index - 1 < 0) return false;\n twitter_component.captures[focused_capture_index - 1].focused = true;\n }\n\n // ←キー: 1つ後のキャプチャにフォーカスする\n if (event.code === 'ArrowRight') {\n // 1つ後のキャプチャがないなら実行しない\n if (focused_capture_index + 1 > (twitter_component.captures.length - 1)) return false;\n twitter_component.captures[focused_capture_index + 1].focused = true;\n }\n\n // 現在フォーカスされているキャプチャのフォーカスを外す\n twitter_component.captures[focused_capture_index].focused = false;\n\n // 拡大表示のモーダルが開かれている場合は、フォーカスしたキャプチャをモーダルにセット\n // こうすることで、QuickLook みたいな挙動になる\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (twitter_component.zoom_capture_modal === true) {\n twitter_component.zoom_capture = focused_capture;\n }\n\n // 現在フォーカスされているキャプチャが見える位置までスクロール\n // block: 'nearest' の指定で、上下どちらにスクロールしてもフォーカスされているキャプチャが常に表示されるようになる\n const focused_capture_element =\n twitter_component.$el.querySelector(`img[src=\"${focused_capture.image_url}\"]`).parentElement;\n if (is_repeat) {\n // キーリピート状態ではスムーズスクロールがフォーカスの移動に追いつけずスクロールの挙動がおかしくなるため、\n // スムーズスクロールは無効にしてある\n focused_capture_element.scrollIntoView({block: 'nearest', inline: 'nearest', behavior: 'auto'});\n } else {\n focused_capture_element.scrollIntoView({block: 'nearest', inline: 'nearest', behavior: 'smooth'});\n }\n return true;\n }\n\n // ***** キャプチャを拡大表示する/拡大表示を閉じる *****\n\n if (event.code === 'Enter') {\n\n // Enter キーの押下がプレイヤー側のコメント送信由来のイベントの場合は実行しない\n if (this.is_comment_send_just_did) return false;\n\n // すでにモーダルが開かれている場合は、どのキャプチャが拡大表示されているかに関わらず閉じる\n if (twitter_component.zoom_capture_modal === true) {\n twitter_component.zoom_capture_modal = false;\n return true;\n }\n\n // 現在フォーカスされているキャプチャを取得\n // まだどのキャプチャにもフォーカスされていない場合は実行しない\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (focused_capture === undefined) return false;\n\n // モーダルを開き、モーダルで拡大表示するキャプチャとしてセット\n twitter_component.zoom_capture = focused_capture;\n twitter_component.zoom_capture_modal = true;\n return true;\n }\n\n // ***** キャプチャを選択する/選択を解除する *****\n\n if (event.code === 'Space') {\n\n // 現在フォーカスされているキャプチャを取得\n // まだどのキャプチャにもフォーカスされていない場合は実行しない\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (focused_capture === undefined) return false;\n\n // 「キャプチャリスト内のキャプチャがクリックされたときのイベント」を呼ぶ\n // 選択されていなければ選択され、選択されていれば選択が解除される\n // キャプチャの枚数制限などはすべて clickCapture() の中で処理される\n twitter_component.clickCapture(focused_capture);\n return true;\n }\n }\n\n // ***** 上下キーでチャンネルを切り替える *****\n\n // キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // ↑キー: 前のチャンネルに切り替え\n if (event.code === 'ArrowUp') {\n await this.$router.push({path: `/tv/watch/${this.channel_previous.channel_id}`});\n return true;\n }\n // ↓キー: 次のチャンネルに切り替え\n if (event.code === 'ArrowDown') {\n await this.$router.push({path: `/tv/watch/${this.channel_next.channel_id}`});\n return true;\n }\n }\n\n // ***** プレイヤーのショートカットキー *****\n\n // プレイヤーが初期化されていない時・Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.shiftKey && !event.altKey) {\n\n // Ctrl / Cmd + ↑キー: プレイヤーの音量を上げる\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowUp') {\n this.player.volume(this.player.volume() + 0.05);\n return true;\n }\n // Ctrl / Cmd + ↓キー: プレイヤーの音量を下げる\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowDown') {\n this.player.volume(this.player.volume() - 0.05);\n return true;\n }\n // Ctrl / Cmd + ←キー: 停止して0.5秒巻き戻し\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowLeft') {\n if (this.player.video.paused === false) this.player.video.pause();\n this.player.video.currentTime = this.player.video.currentTime - 0.5;\n return true;\n }\n // Ctrl / Cmd + →キー: 停止して0.5秒早送り\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowRight') {\n if (this.player.video.paused === false) this.player.video.pause();\n this.player.video.currentTime = this.player.video.currentTime + 0.5;\n return true;\n }\n }\n\n // プレイヤーが初期化されていない時・Ctrl / Cmd / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.ctrlKey && !event.metaKey && !event.altKey) {\n\n // Shift + Spaceキー + キーリピートでない時 + Twitter タブ表示時 + キャプチャタブ表示時: 再生/停止\n if (event.shiftKey === true && event.code === 'Space' && is_repeat === false &&\n this.tv_panel_active_tab === 'Twitter' && twitter_component.twitter_active_tab === 'Capture') {\n this.player.toggle();\n return true;\n }\n }\n\n // プレイヤーが初期化されていない時・キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // Spaceキー: 再生/停止\n if (event.code === 'Space') {\n this.player.toggle();\n return true;\n }\n // Fキー: フルスクリーンの切り替え\n if (event.code === 'KeyF') {\n this.player.fullScreen.toggle();\n return true;\n }\n // Wキー: ライブストリームの同期\n if (event.code === 'KeyW') {\n this.player.sync();\n return true;\n }\n // Eキー: Picture-in-Picture の表示切り替え\n if (event.code === 'KeyE') {\n if (document.pictureInPictureEnabled) {\n this.player.template.pipButton.click();\n }\n return true;\n }\n // Sキー: 字幕の表示切り替え\n if (event.code === 'KeyS') {\n this.player.subtitle.toggle();\n if (!this.player.subtitle.container.classList.contains('dplayer-subtitle-hide')) {\n this.player.notice(`${this.player.tran('Show subtitle')}`);\n } else {\n this.player.notice(`${this.player.tran('Hide subtitle')}`);\n }\n return true;\n }\n // Dキー: コメントの表示切り替え\n if (event.code === 'KeyD') {\n this.player.template.showDanmaku.click();\n if (this.player.template.showDanmakuToggle.checked) {\n this.player.notice(`${this.player.tran('Show comment')}`);\n } else {\n this.player.notice(`${this.player.tran('Hide comment')}`);\n }\n return true;\n }\n // Cキー: 映像をキャプチャ\n if (event.code === 'KeyC') {\n await this.capture_handler.captureAndSave(this.channel, false);\n return true;\n }\n // Vキー: 映像を実況コメントを付けてキャプチャ\n if (event.code === 'KeyV') {\n await this.capture_handler.captureAndSave(this.channel, true);\n return true;\n }\n // Mキー: コメント入力フォームにフォーカス\n if (event.code === 'KeyM') {\n this.player.controller.show();\n this.player.comment.show();\n this.controlDisplayTimer();\n window.setTimeout(() => this.player.template.commentInput.focus(), 100);\n return true;\n }\n }\n }\n return false;\n })();\n\n // 無名関数を実行した後の戻り値が true ならショートカットキーの操作を実行したことになるので、デフォルトのキー操作を封じる\n if (result === true) {\n event.preventDefault();\n }\n };\n\n // ページ上でキーが押されたときのイベントを登録\n document.addEventListener('keydown', this.shortcut_key_handler);\n },\n\n // キャプチャ関連のイベントを初期化する\n initCaptureHandler() {\n\n // キャプチャハンドラーを初期化\n this.capture_handler = new PlayerCaptureHandler(this.player, (blob: Blob, filename: string) => {\n (this.$refs.Twitter as InstanceType).addCaptureList(blob, filename);\n });\n\n // キャプチャボタンがクリックされたときのイベント\n // ショートカットからのキャプチャでも同じイベントがトリガーされる\n const capture_button = this.$el.querySelector('.dplayer-icon.dplayer-capture-icon');\n capture_button.addEventListener('click', async () => {\n await this.capture_handler.captureAndSave(this.channel, false);\n });\n\n // コメント付きキャプチャボタンがクリックされたときのイベント\n // ショートカットからのキャプチャでも同じイベントがトリガーされる\n const comment_capture_button = this.$el.querySelector('.dplayer-icon.dplayer-comment-capture-icon');\n comment_capture_button.addEventListener('click', async () => {\n await this.capture_handler.captureAndSave(this.channel, true);\n });\n },\n\n\n // 再生セッションを破棄する\n // チャンネルを切り替える際に実行される\n destroy(is_destroy_player = false) {\n\n // clearInterval() ですべての setInterval(), setTimeout() の実行を止める\n // clearInterval() と clearTimeout() は中身共通なので問題ない\n for (const interval_id of this.interval_ids) {\n window.clearInterval(interval_id);\n }\n\n // コントロール表示制御用タイマーを止める\n window.clearTimeout(this.control_interval_id);\n\n // interval_ids をクリア\n this.interval_ids = [];\n\n // 再びローディング状態にする\n this.is_loading = true;\n\n // プレイヤーの背景を隠す\n this.is_background_display = false;\n\n // プレイヤーに破棄が可能なフラグをつける\n this.player.KonomiTVCanDestroy = true;\n\n // イベントソースを閉じる\n if (this.eventsource !== null) {\n this.eventsource.close();\n this.eventsource = null;\n }\n\n // アニメーション分待ってから実行\n this.interval_ids.push(window.setTimeout(() => {\n\n // プレイヤーを停止する\n this.player.video.pause();\n\n // is_destroy_player が true の時は、ここで DPlayer 自体を破棄する\n // false の時は次の initPlayer() が実行されるまで破棄されない\n if (is_destroy_player === true && this.player !== null) {\n try {\n this.player.destroy();\n } catch (error) {\n // mpegts.js をうまく破棄できない場合\n if (this.player.plugins.mpegts !== undefined) {\n this.player.plugins.mpegts.destroy();\n }\n }\n this.player = null;\n }\n\n }, 0.4 * 1000)); // 0.4 秒\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Watch.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Watch.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Watch.vue?vue&type=template&id=6a0c19bf&scoped=true&\"\nimport script from \"./Watch.vue?vue&type=script&lang=ts&\"\nexport * from \"./Watch.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Watch.vue?vue&type=style&index=0&id=6a0c19bf&prod&lang=scss&\"\nimport style1 from \"./Watch.vue?vue&type=style&index=1&id=6a0c19bf&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"6a0c19bf\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('v-card',{staticClass:\"settings-container d-flex px-5 py-5 mx-auto background\",attrs:{\"elevation\":\"0\",\"width\":\"100%\",\"max-width\":\"1000\"}},[_c('v-navigation-drawer',{staticClass:\"settings-navigation flex-shrink-0 background\",attrs:{\"permanent\":\"\",\"width\":\"100%\",\"height\":\"auto\"}},[_c('v-list-item',{staticClass:\"px-4\"},[_c('v-list-item-content',[_c('h1',[_vm._v(\"設定\")])])],1),_c('v-list',{staticClass:\"mt-2 px-0\",attrs:{\"nav\":\"\"}},[_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/general\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 3px\"},attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"全般\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/account\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"アカウント\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/jikkyo\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 2px\"},attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"ニコニコ実況\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/twitter\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 1px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"Twitter\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/environment\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"環境設定\")])],1)],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\nexport default Vue.extend({\n name: 'Settings-Index',\n components: {\n Header,\n Navigation,\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Index.vue?vue&type=template&id=036b263a&scoped=true&\"\nimport script from \"./Index.vue?vue&type=script&lang=ts&\"\nexport * from \"./Index.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Index.vue?vue&type=style&index=0&id=036b263a&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"036b263a\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"19px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"全般\")])],1),_c('div',{staticClass:\"settings__content\"},[_c('div',{staticClass:\"settings__item settings__item--sync-disabled\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"テレビのストリーミング画質\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" テレビをライブストリーミングするときの既定の画質を設定します。\"),_c('br'),_vm._v(\" ストリーミング画質はプレイヤーの設定からいつでも切り替えられます。\"),_c('br')]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" [1080p (60fps)] は、通常 30fps (60i) の映像を補間することで、ほかの画質よりも滑らか(ぬるぬる)な映像で再生できます。ただし、再生負荷が少し高くなります。\"),_c('br'),_vm._v(\" [1080p (60fps)] で視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーでは CPU 使用率が高くなり、再生に支障が出ることがあります。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tv_streaming_quality},model:{value:(_vm.settings.tv_streaming_quality),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_streaming_quality\", $$v)},expression:\"settings.tv_streaming_quality\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\",class:{'settings__item--disabled': _vm.PlayerUtils.isHEVCVideoSupported() === false}},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_data_saver_mode\"}},[_vm._v(\"テレビを通信節約モードで視聴する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_data_saver_mode\"}},[_vm._v(\" テレビをライブストリーミングするときに、通信節約モードで視聴するかを設定します。\"),_c('br'),_vm._v(\" 通信節約モードでは、H.265 / HEVC という圧縮率の高いコーデックを使い、画質はほぼそのまま、通信量を通常の 2/3 程度に抑えながら視聴できます。ただし、再生負荷が高くなります。\"),_c('br'),_vm._v(\" 通信節約モードで視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーではまともに再生できない可能性が高いです。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_data_saver_mode\",\"inset\":\"\",\"hide-details\":\"\",\"disabled\":_vm.PlayerUtils.isHEVCVideoSupported() === false},model:{value:(_vm.settings.tv_data_saver_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_data_saver_mode\", $$v)},expression:\"settings.tv_data_saver_mode\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_low_latency_mode\"}},[_vm._v(\"テレビを低遅延で視聴する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_low_latency_mode\"}},[_vm._v(\" テレビをライブストリーミングするときに、低遅延で視聴するかを設定します。\"),_c('br'),_vm._v(\" 低遅延ストリーミングがオンのときは、放送波との遅延を最短 1.9 秒に抑えて視聴できます。\"),_c('br'),_vm._v(\" また、約 3 秒以上遅延したときに少しだけ再生速度を早める (1.1x) ことで、滑らかにストリーミングの遅れを取り戻します。\"),_c('br'),_vm._v(\" 宅外視聴などのネットワークが不安定になりがちな環境では、一度低遅延ストリーミングをオフにしてみると、映像のカクつきを改善できるかもしれません。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_low_latency_mode\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.tv_low_latency_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_low_latency_mode\", $$v)},expression:\"settings.tv_low_latency_mode\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_show_superimpose\"}},[_vm._v(\"テレビをみるときに文字スーパーを表示する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_show_superimpose\"}},[_vm._v(\" テレビをライブストリーミングするときに、文字スーパーを表示するかを設定します。\"),_c('br'),_vm._v(\" 文字スーパーは、緊急地震速報の赤テロップや、NHK BS のニュース速報のテロップなどで利用されています。とくに理由がなければ、オンのままにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_show_superimpose\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.tv_show_superimpose),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_show_superimpose\", $$v)},expression:\"settings.tv_show_superimpose\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"既定のパネルの表示状態\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 視聴画面を開いたときに、右側のパネルをどう表示するかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.panel_display_state},model:{value:(_vm.settings.panel_display_state),callback:function ($$v) {_vm.$set(_vm.settings, \"panel_display_state\", $$v)},expression:\"settings.panel_display_state\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"テレビをみるときに既定で表示されるパネルのタブ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" テレビの視聴画面を開いたときに、右側のパネルで最初に表示されるタブを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tv_panel_active_tab},model:{value:(_vm.settings.tv_panel_active_tab),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_panel_active_tab\", $$v)},expression:\"settings.tv_panel_active_tab\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('label',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕のフォント\")]),_c('label',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーで字幕表示をオンにしているときの、字幕のフォントを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.caption_font},model:{value:(_vm.settings.caption_font),callback:function ($$v) {_vm.$set(_vm.settings, \"caption_font\", $$v)},expression:\"settings.caption_font\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"always_border_caption_text\"}},[_vm._v(\"字幕の文字を常に縁取って描画する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"always_border_caption_text\"}},[_vm._v(\" プレイヤーで字幕表示をオンにしているときに、字幕の文字を常に縁取って描画するかを設定します。\"),_c('br'),_vm._v(\" 字幕は縁取られていた方が視認性が良く、見た目的にもきれいです。とくに理由がなければ、オンのままにしておくことをおすすめします。\"),_c('br'),_vm._v(\" この設定をオフにしているときも、字幕データ側で明示的に縁取りするように指定されていれば、オンにしているとき同様に文字が縁取られて描画されます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"always_border_caption_text\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.always_border_caption_text),callback:function ($$v) {_vm.$set(_vm.settings, \"always_border_caption_text\", $$v)},expression:\"settings.always_border_caption_text\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"specify_caption_background_color\"}},[_vm._v(\"字幕の背景色を指定する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"specify_caption_background_color\"}},[_vm._v(\" プレイヤーで字幕表示をオンにしているときに、字幕の背景色を明示的に指定するかを設定します。\"),_c('br'),_vm._v(\" この設定をオフにしているときは、字幕データ側で指定されている背景色で描画します。とくに理由がなければ、オフのままにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"specify_caption_background_color\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.specify_caption_background_color),callback:function ($$v) {_vm.$set(_vm.settings, \"specify_caption_background_color\", $$v)},expression:\"settings.specify_caption_background_color\"}})],1),_c('div',{staticClass:\"settings__item\",class:{'settings__item--disabled': _vm.settings.specify_caption_background_color === false}},[_c('label',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕の背景色\")]),_c('label',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーで字幕表示をオンにしているときの、字幕の背景色を設定します。\"),_c('br'),_vm._v(\" 上の [字幕の背景色を指定する] をオンにしているときのみ有効です。透明度 (アルファチャンネル) を 0 に設定すれば、字幕の背景を非表示にできます。\"),_c('br')]),_c('div',{ref:\"caption_background_color\",staticClass:\"settings__item-label\"},[_c('v-color-picker',{staticClass:\"settings__item-form\",attrs:{\"hide-details\":\"\",\"flat\":true,\"show-alpha\":true,\"show-swatches\":false,\"hide-inputs\":false,\"width\":690,\"canvas-height\":80,\"disabled\":_vm.settings.specify_caption_background_color === false},model:{value:(_vm.settings.caption_background_color),callback:function ($$v) {_vm.$set(_vm.settings, \"caption_background_color\", $$v)},expression:\"settings.caption_background_color\"}})],1)]),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"capture_copy_to_clipboard\"}},[_vm._v(\"キャプチャをクリップボードにコピーする\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"capture_copy_to_clipboard\"}},[_vm._v(\" プレイヤーでキャプチャを撮ったときに、撮ったキャプチャをクリップボードにもコピーするかを設定します。\"),_c('br'),_vm._v(\" クリップボードの履歴をサポートしていない OS では、この設定をオンにした状態でキャプチャを撮ると、以前のクリップボードが上書きされます。注意してください。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"capture_copy_to_clipboard\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.capture_copy_to_clipboard),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_copy_to_clipboard\", $$v)},expression:\"settings.capture_copy_to_clipboard\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"キャプチャの保存先\")]),_c('div',{staticClass:\"settings__item-label\"},[_c('p',[_vm._v(\" キャプチャした画像をブラウザでダウンロードするか、KonomiTV サーバーにアップロードするかを設定します。\"),_c('br'),_vm._v(\" ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方同時に行うこともできます。\"),_c('br')]),_c('p',[_vm._v(\" ブラウザでダウンロードすると、視聴中のデバイスのダウンロードフォルダに保存されます。\"),_c('br'),_vm._v(\" 視聴中のデバイスにそのまま保存されるためシンプルですが、保存先のフォルダを変更できないこと、PC 版 Chrome では毎回ダウンロードバーが表示されてしまうことがデメリットです。\"),_c('br')]),_c('p',[_vm._v(\" KonomiTV サーバーにアップロードすると、環境設定で指定されたキャプチャ保存フォルダに保存されます。視聴したデバイスにかかわらず、今までに撮ったキャプチャをひとつのフォルダにまとめて保存できます。\"),_c('br'),_vm._v(\" 他のデバイスでキャプチャを見るにはキャプチャ保存フォルダをネットワークに共有する必要があること、スマホ・タブレットではネットワーク上のフォルダへのアクセスがやや面倒なことがデメリットです。\"),_c('br')])]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.capture_save_mode},model:{value:(_vm.settings.capture_save_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_save_mode\", $$v)},expression:\"settings.capture_save_mode\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕が表示されているときのキャプチャの保存モード\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 字幕が表示されているときに、キャプチャした画像に字幕を合成するかを設定します。\"),_c('br'),_vm._v(\" 映像のみのキャプチャと、字幕を合成したキャプチャを両方同時に保存することもできます。\"),_c('br'),_vm._v(\" なお、字幕が表示されていない場合は、常に映像のみ (+コメント付きキャプチャではコメントを合成して) 保存されます。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.capture_caption_mode},model:{value:(_vm.settings.capture_caption_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_caption_mode\", $$v)},expression:\"settings.capture_caption_mode\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"設定をエクスポート\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" このデバイス(ブラウザ)に保存されている設定データをエクスポート(ダウンロード)できます。\"),_c('br'),_vm._v(\" ダウンロードした設定データ (KonomiTV-Settings.json) は、[設定をインポート] からインポートできます。異なるサーバーの KonomiTV を同じ設定で使いたいときなどに使ってください。\"),_c('br')])]),_c('v-btn',{staticClass:\"settings__save-button mt-4\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.exportSettings()}}},[_c('Icon',{staticClass:\"mr-3\",attrs:{\"icon\":\"fa6-solid:download\",\"height\":\"19px\"}}),_vm._v(\"設定をエクスポート \")],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading error--text text--lighten-1\"},[_vm._v(\"設定をインポート\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" [設定をエクスポート] でダウンロードした設定データを、このデバイス(ブラウザ)にインポートできます。\"),_c('br'),_vm._v(\" 設定をインポートすると、それまでこのデバイス(ブラウザ)に保存されていた設定が、すべてインポート先の設定データで上書きされます。元に戻すことはできません。 \")]),_c('v-file-input',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"設定データ (KonomiTV-Settings.json) を選択\",\"dense\":_vm.is_form_dense,\"accept\":\"application/json\",\"prepend-icon\":\"\",\"prepend-inner-icon\":\"mdi-paperclip\"},model:{value:(_vm.import_settings_file),callback:function ($$v) {_vm.import_settings_file=$$v},expression:\"import_settings_file\"}})],1),_c('v-btn',{staticClass:\"settings__save-button error mt-5\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.importSettings()}}},[_c('Icon',{staticClass:\"mr-3\",attrs:{\"icon\":\"fa6-solid:upload\",\"height\":\"19px\"}}),_vm._v(\"設定をインポート \")],1)],1)])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('v-card',{staticClass:\"settings-container d-flex px-5 py-5 mx-auto background\",attrs:{\"elevation\":\"0\",\"width\":\"100%\",\"max-width\":\"1000\"}},[_c('div',[_c('v-navigation-drawer',{staticClass:\"settings-navigation flex-shrink-0 background\",attrs:{\"permanent\":\"\",\"width\":\"195\",\"height\":\"auto\"}},[_c('v-list-item',{staticClass:\"px-4\"},[_c('v-list-item-content',[_c('h1',[_vm._v(\"設定\")])])],1),_c('v-list',{staticClass:\"mt-2 px-0\",attrs:{\"nav\":\"\"}},[_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/general\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 3px\"},attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"全般\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/account\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"アカウント\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/jikkyo\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 2px\"},attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"ニコニコ実況\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/twitter\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 1px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"Twitter\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/environment\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"環境設定\")])],1)],1)],1)],1)],1),_c('v-card',{staticClass:\"settings ml-5 px-7 py-7 background lighten-1\",attrs:{\"width\":\"100%\"}},[_vm._t(\"default\")],2)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\n// 設定のベース画面なので、ロジックは基本置かない\nexport default Vue.extend({\n name: 'Settings-Base',\n components: {\n Header,\n Navigation,\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Base.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Base.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Base.vue?vue&type=template&id=03345d7e&scoped=true&\"\nimport script from \"./Base.vue?vue&type=script&lang=ts&\"\nexport * from \"./Base.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Base.vue?vue&type=style&index=0&id=03345d7e&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"03345d7e\",\n null\n \n)\n\nexport default component.exports","\n\nimport Vue from 'vue';\n\nimport Base from '@/views/Settings/Base.vue';\nimport Utils, { PlayerUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-General',\n components: {\n Base,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n PlayerUtils: PlayerUtils,\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // テレビのストリーミング画質の選択肢\n tv_streaming_quality: [\n {'text': '1080p (60fps) (1時間あたり約3.24GB / 7.2Mbps)', 'value': '1080p-60fps'},\n {'text': '1080p (1時間あたり約2.31GB / 5.1Mbps)', 'value': '1080p'},\n {'text': '810p (1時間あたり約1.92GB / 4.2Mbps)', 'value': '810p'},\n {'text': '720p (1時間あたり約1.33GB / 3.0Mbps)', 'value': '720p'},\n {'text': '540p (1時間あたり約1.00GB / 2.2Mbps)', 'value': '540p'},\n {'text': '480p (1時間あたり約0.74GB / 1.6Mbps)', 'value': '480p'},\n {'text': '360p (1時間あたり約0.40GB / 0.9Mbps)', 'value': '360p'},\n {'text': '240p (1時間あたり約0.23GB / 0.5Mbps)', 'value': '240p'},\n ],\n\n // 既定のパネルの表示状態の選択肢\n panel_display_state: [\n {'text': '前回の状態を復元する', 'value': 'RestorePreviousState'},\n {'text': '常に表示する', 'value': 'AlwaysDisplay'},\n {'text': '常に折りたたむ', 'value': 'AlwaysFold'},\n ],\n\n // テレビをみるときに既定で表示されるパネルのタブの選択肢\n tv_panel_active_tab: [\n {'text': '番組情報タブ', 'value': 'Program'},\n {'text': 'チャンネルタブ', 'value': 'Channel'},\n {'text': 'コメントタブ', 'value': 'Comment'},\n {'text': 'Twitter タブ', 'value': 'Twitter'},\n ],\n\n // 字幕のフォントの選択肢\n caption_font: [\n {'text': 'Windows TV ゴシック', 'value': 'Windows TV Gothic'},\n {'text': 'Windows TV 丸ゴシック', 'value': 'Windows TV MaruGothic'},\n {'text': 'Windows TV 太丸ゴシック', 'value': 'Windows TV FutoMaruGothic'},\n {'text': 'ヒラギノTV丸ゴ', 'value': 'Hiragino TV Sans Rd S'},\n {'text': '新丸ゴ ARIB', 'value': 'TT-ShinMGo-regular'},\n {'text': 'Rounded M+ 1m for ARIB', 'value': 'Rounded M+ 1m for ARIB'},\n {'text': 'Noto Sans JP', 'value': 'Noto Sans JP Caption'},\n {'text': 'デフォルトのフォント', 'value': 'sans-serif'},\n ],\n\n // キャプチャの保存先の選択肢\n capture_save_mode: [\n {'text': 'ブラウザでダウンロード', 'value': 'Browser'},\n {'text': 'KonomiTV サーバーにアップロード', 'value': 'UploadServer'},\n {'text': 'ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方行う', 'value': 'Both'},\n ],\n\n // 字幕が表示されているときのキャプチャの保存モードの選択肢\n capture_caption_mode: [\n {'text': '映像のみのキャプチャを保存する', 'value': 'VideoOnly'},\n {'text': '字幕を合成したキャプチャを保存する', 'value': 'CompositingCaption'},\n {'text': '映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する', 'value': 'Both'},\n ],\n\n // 選択された設定データ (KonomiTV-Settings.json) が入る\n import_settings_file: null as File | null,\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {}\n const setting_keys = [\n 'tv_streaming_quality',\n 'tv_data_saver_mode',\n 'tv_low_latency_mode',\n 'tv_show_superimpose',\n 'panel_display_state',\n 'tv_panel_active_tab',\n 'caption_font',\n 'always_border_caption_text',\n 'specify_caption_background_color',\n 'caption_background_color',\n 'capture_copy_to_clipboard',\n 'capture_save_mode',\n 'capture_caption_mode',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n\n // 設定データをエクスポートする\n exportSettings() {\n\n // JSON のままの設定データを LocalStorage から直に取得\n // \"KonomiTV-Settings\" キーがないときはデフォルト設定を JSON 化したものを入れる\n const settings_json = localStorage.getItem('KonomiTV-Settings') || JSON.stringify(Utils.default_settings);\n\n // ダウンロードさせるために Blob にしてから、KonomiTV-Settings.json としてダウンロード\n const settings_json_blob = new Blob([settings_json], {type: 'application/json'});\n Utils.downloadBlobData(settings_json_blob, 'KonomiTV-Settings.json');\n this.$message.success('設定をエクスポートしました。');\n },\n\n // 設定データをインポートする\n async importSettings() {\n\n // 設定データが選択されていないときは実行しない\n if (this.import_settings_file === null) {\n this.$message.error('インポートする設定データを選択してください!');\n return;\n }\n\n try {\n\n // 選択された設定データの JSON を取得してデコード\n // そのまま突っ込んでもいいんだけど、念のため一度オブジェクトになおしておく\n const settings = JSON.parse(await this.import_settings_file.text());\n\n // LocalStorage に直に保存\n // このとき、既存の設定データはすべて上書きされる\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(settings));\n\n // 設定データをサーバーに同期する\n await Utils.syncClientSettingsToServer();\n\n // 設定を適用するためリロード\n this.$message.success('設定をインポートしました。');\n window.setTimeout(() => this.$router.go(0), 300);\n\n } catch (error) {\n this.$message.error('設定データが不正なため、インポートできませんでした。');\n return;\n }\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./General.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./General.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./General.vue?vue&type=template&id=2c52b1a4&\"\nimport script from \"./General.vue?vue&type=script&lang=ts&\"\nexport * from \"./General.vue?vue&type=script&lang=ts&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"25px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"アカウント\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[(_vm.user === null)?_c('div',{staticClass:\"account\"},[_c('div',{staticClass:\"account-wrapper\"},[_c('img',{staticClass:\"account__icon\",attrs:{\"src\":\"/assets/images/account-icon-default.png\"}}),_c('div',{staticClass:\"account__info\"},[_c('div',{staticClass:\"account__info-name\"},[_c('span',{staticClass:\"account__info-name-text\"},[_vm._v(\"ログインしていません\")])]),_c('span',{staticClass:\"account__info-id\"},[_vm._v(\"Not logged in\")])])]),_c('v-btn',{staticClass:\"account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"140\",\"height\":\"56\",\"depressed\":\"\",\"to\":\"/login/\"}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-in\"}}),_vm._v(\"ログイン \")],1)],1):_vm._e(),(_vm.user !== null)?_c('div',{staticClass:\"account\"},[_c('div',{staticClass:\"account-wrapper\"},[_c('img',{staticClass:\"account__icon\",attrs:{\"src\":_vm.user_icon_blob}}),_c('div',{staticClass:\"account__info\"},[_c('div',{staticClass:\"account__info-name\"},[_c('span',{staticClass:\"account__info-name-text\"},[_vm._v(_vm._s(_vm.user.name))]),(_vm.user.is_admin)?_c('span',{staticClass:\"account__info-admin\"},[_vm._v(\"管理者\")]):_vm._e()]),_c('span',{staticClass:\"account__info-id\"},[_vm._v(\"User ID: \"+_vm._s(_vm.user.id))])])]),_c('v-btn',{staticClass:\"account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"140\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logout()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-out\"}}),_vm._v(\"ログアウト \")],1)],1):_vm._e(),(_vm.is_logged_in === false)?_c('div',{staticClass:\"account-register\"},[_c('div',{staticClass:\"account-register__heading\"},[_vm._v(\" KonomiTV アカウントにログインすると、\"),_c('br'),_vm._v(\"より便利な機能が使えます! \")]),_c('div',{staticClass:\"account-register__feature\"},[_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"ニコニコ実況にコメントする\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"テレビを見ながらニコニコ実況にコメントできます。別途、ニコニコアカウントとの連携が必要です。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fa-brands:twitter\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"Twitter 連携機能\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"テレビを見ながら Twitter にツイートしたり、検索したツイートをリアルタイムで表示できます。別途、Twitter アカウントとの連携が必要です。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fluent:arrow-sync-20-filled\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"設定をデバイス間で同期\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"ピン留めしたチャンネルなど、ブラウザに保存されている各種設定をブラウザやデバイスをまたいで同期できます。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fa-solid:sliders-h\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"環境設定をブラウザから変更\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"管理者権限があれば、環境設定をブラウザから変更できます。一番最初に作成されたアカウントには、自動で管理者権限が付与されます。\")])])],1)]),_c('div',{staticClass:\"account-register__description\"},[_vm._v(\" KonomiTV アカウントの作成に必要なものはユーザー名とパスワードだけです。\"),_c('br'),_vm._v(\" アカウントはローカルにインストールした KonomiTV サーバーごとに保存されます。\"),_c('br'),_vm._v(\" 外部のサービスには保存されませんので、ご安心ください。\"),_c('br')]),_c('v-btn',{staticClass:\"account-register__button\",attrs:{\"color\":\"secondary\",\"width\":\"100%\",\"max-width\":\"250\",\"height\":\"50\",\"depressed\":\"\",\"to\":\"/register/\"}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-add-20-filled\",\"height\":\"24\"}}),_vm._v(\"アカウントを作成 \")],1)],1):_vm._e(),(_vm.is_logged_in === true)?_c('div',[_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"sync_settings\"}},[_vm._v(\"設定をデバイス間で同期する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"sync_settings\"}},[_vm._v(\" KonomiTV の設定を、同じアカウントにログインしているデバイス同士で同期するかを設定します。\"),_c('br'),_vm._v(\" 同期を有効にすると、同期が有効なデバイスすべてで同じ設定が使えます。ピン留めしたチャンネルやハッシュタグリストなども同期されます。\"),_c('br'),_vm._v(\" ストリーミング画質やコメントの遅延時間など、デバイスごとに最適な設定が異なるものは、同期を有効にしたあとも引き続きこのデバイス(ブラウザ)のみに反映されます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"sync_settings\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.sync_settings),callback:function ($$v) {_vm.sync_settings=$$v},expression:\"sync_settings\"}})],1),_c('v-dialog',{attrs:{\"max-width\":\"530\"},model:{value:(_vm.sync_settings_dialog),callback:function ($$v) {_vm.sync_settings_dialog=$$v},expression:\"sync_settings_dialog\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"justify-center\"},[_vm._v(\"設定データの競合\")]),_c('v-card-text',[_vm._v(\" このデバイスの設定と、サーバーに保存されている設定が競合しています。\"),_c('br'),_vm._v(\" 一度上書きすると、元に戻すことはできません。慎重に選択してください。\"),_c('br')]),_c('div',{staticClass:\"d-flex flex-column px-4 pb-4\"},[_c('v-btn',{staticClass:\"settings__save-button error--text text--lighten-1\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.overrideServerSettingsFromClient()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:document-arrow-up-16-filled\",\"height\":\"22px\"}}),_vm._v(\" サーバーに保存されている設定を、このデバイスの設定で上書きする \")],1),_c('v-btn',{staticClass:\"settings__save-button error--text text--lighten-1 mt-3\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.overrideClientSettingsFromServer()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:document-arrow-down-16-filled\",\"height\":\"22px\"}}),_vm._v(\" このデバイスの設定を、サーバーに保存されている設定で上書きする \")],1),_c('v-btn',{staticClass:\"settings__save-button mt-3\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.sync_settings_dialog = false}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:dismiss-16-filled\",\"height\":\"22px\"}}),_vm._v(\" キャンセル \")],1)],1)],1)],1),_c('v-form',{ref:\"settings_username\",staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ユーザー名\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントのユーザー名を設定します。アルファベットだけでなく日本語や記号も使えます。\"),_c('br'),_vm._v(\" 同じ KonomiTV サーバー上の他のアカウントと同じユーザー名には変更できません。\"),_c('br')]),_c('v-text-field',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"dense\":_vm.is_form_dense,\"rules\":[_vm.settings_username_validation]},model:{value:(_vm.settings_username),callback:function ($$v) {_vm.settings_username=$$v},expression:\"settings_username\"}})],1),_c('v-btn',{staticClass:\"settings__save-button\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountInfo('username')}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"ユーザー名を更新 \")],1),_c('v-form',{staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"アイコン画像\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントのアイコン画像を設定します。\"),_c('br'),_vm._v(\" アップロードされた画像は自動的に 400×400 の正方形にリサイズされます。\"),_c('br')]),_c('v-file-input',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"アイコン画像を選択\",\"dense\":_vm.is_form_dense,\"accept\":\"image/jpeg, image/png\",\"prepend-icon\":\"\",\"prepend-inner-icon\":\"mdi-paperclip\"},model:{value:(_vm.settings_icon),callback:function ($$v) {_vm.settings_icon=$$v},expression:\"settings_icon\"}})],1),_c('v-btn',{staticClass:\"settings__save-button mt-5\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountIcon()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"アイコン画像を更新 \")],1),_c('v-form',{ref:\"settings_password\",staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"新しいパスワード\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントの新しいパスワードを設定します。\"),_c('br')]),_c('v-text-field',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"placeholder\":\"新しいパスワード\",\"dense\":_vm.is_form_dense,\"type\":_vm.settings_password_showing ? 'text' : 'password',\"append-icon\":_vm.settings_password_showing ? 'mdi-eye' : 'mdi-eye-off',\"rules\":[_vm.settings_password_validation]},on:{\"click:append\":function($event){_vm.settings_password_showing = !_vm.settings_password_showing}},model:{value:(_vm.settings_password),callback:function ($$v) {_vm.settings_password=$$v},expression:\"settings_password\"}})],1),_c('v-btn',{staticClass:\"settings__save-button\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountInfo('password')}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"パスワードを更新 \")],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item mt-6\"},[_c('div',{staticClass:\"settings__item-heading error--text text--lighten-1\"},[_vm._v(\"アカウントを削除\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 現在ログインしている KonomiTV アカウントを削除します。\"),_c('br'),_vm._v(\" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。\"),_c('br')])]),_c('v-dialog',{attrs:{\"max-width\":\"385\"},scopedSlots:_vm._u([{key:\"activator\",fn:function({ on, attrs }){return [_c('v-btn',_vm._g(_vm._b({staticClass:\"settings__save-button error mt-5\",attrs:{\"depressed\":\"\"}},'v-btn',attrs,false),on),[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:delete-16-filled\",\"height\":\"24px\"}}),_vm._v(\"アカウントを削除 \")],1)]}}],null,false,974850237),model:{value:(_vm.account_delete_confirm_dialog),callback:function ($$v) {_vm.account_delete_confirm_dialog=$$v},expression:\"account_delete_confirm_dialog\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"justify-center\"},[_vm._v(\"本当にアカウントを削除しますか?\")]),_c('v-card-text',[_vm._v(\" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。\"),_c('br'),_vm._v(\" 本当にアカウントを削除しますか? \")]),_c('v-card-actions',[_c('v-spacer'),_c('v-btn',{attrs:{\"color\":\"text\",\"text\":\"\"},on:{\"click\":function($event){_vm.account_delete_confirm_dialog = false}}},[_vm._v(\"キャンセル\")]),_c('v-btn',{attrs:{\"color\":\"error\",\"text\":\"\"},on:{\"click\":function($event){return _vm.deleteAccount()}}},[_vm._v(\"削除\")])],1)],1)],1)],1):_vm._e()])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Account',\n components: {\n Base,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // ユーザーアカウントのアイコンの Blob URL\n user_icon_blob: '',\n\n // ユーザー名とパスワード\n // ログイン画面やアカウント作成画面の data と同一のもの\n settings_username: null as string | null,\n settings_username_validation: (value: string | null) => {\n if (value === '' || value === null) return 'ユーザー名を入力してください。';\n if (/^.{2,}$/.test(value) === false) return 'ユーザー名は2文字以上で入力してください。';\n return true;\n },\n settings_password: null as string | null,\n settings_password_showing: true, // アカウント情報変更時は既定でパスワードを表示する\n settings_password_validation: (value: string | null) => {\n if (value === '' || value === null) return 'パスワードを入力してください。';\n // 正規表現の参考: https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128\n if (/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(value) === false) return 'パスワードは4文字以上の半角英数記号を入力してください。';\n return true;\n },\n\n // アイコン画像\n settings_icon: null as File | null,\n\n // アカウント削除確認ダイヤログ\n account_delete_confirm_dialog: null,\n\n // 設定を同期するかの設定値\n sync_settings: Utils.getSettingsItem('sync_settings') as boolean,\n\n // 設定を同期するときのダイヤログ\n sync_settings_dialog: false,\n }\n },\n async created() {\n\n // 表示されているアカウント情報を更新\n // アクセストークンが無効化されている可能性もあるので、アクセストークンの有無に関わらず実行する\n await this.syncAccountInfo();\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // sync_settings の値の変更を監視する\n async sync_settings() {\n\n // 同期がオンになった & ダイヤログが表示されていない\n if (this.sync_settings === true && this.sync_settings_dialog === false) {\n\n try {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // 同期対象の設定キーのみで設定データをまとめ直す\n // sync_settings には同期対象外の設定は含まれない\n const sync_settings: {[key: string]: any} = {};\n for (const sync_settings_key of Utils.sync_settings_keys) {\n if (sync_settings_key in settings) {\n sync_settings[sync_settings_key] = settings[sync_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n sync_settings[sync_settings_key] = Utils.default_settings[sync_settings_key];\n }\n }\n\n // 同期対象のこのクライアントの設定を再度 JSON にする(文字列比較のため)\n const sync_settings_json = JSON.stringify(sync_settings);\n\n // サーバーから設定データ (生の JSON) をダウンロード\n // 一度オブジェクトにしたものを再度 JSON にする(文字列比較のため)\n const server_sync_settings_json: string = JSON.stringify((await Vue.axios.get('/settings/client')).data);\n\n // このクライアントの設定とサーバーに保存されている設定が一致しない(=競合している)\n if (sync_settings_json !== server_sync_settings_json) {\n\n // 一度同期をオフにして、クライアントとサーバーどちらの設定を使うのかを選択させるダイヤログを表示\n this.sync_settings_dialog = true;\n this.sync_settings = false;\n\n // このクライアントの設定とサーバーに保存されている設定が一致する\n } else {\n\n // 特に設定の同期をオンにしても問題ないので、そのまま有効にする\n Utils.setSettingsItem('sync_settings', true);\n }\n\n } catch (error) {\n // 何らかの理由でエラーになったとき\n this.$message.error(`サーバーから設定データを取得できませんでした。(HTTP Error ${error.response.status})`);\n }\n\n // 同期がオフになった & ダイヤログが表示されていない\n } else if (this.sync_settings === false && this.sync_settings_dialog === false) {\n Utils.setSettingsItem('sync_settings', false);\n }\n }\n },\n methods: {\n\n // このクライアントの設定でサーバー上の設定を上書きする\n async overrideServerSettingsFromClient() {\n\n // 強制的にこのクライアントの設定をサーバーに同期\n await Utils.syncClientSettingsToServer(true);\n\n // 設定の同期を有効化\n this.sync_settings = true;\n Utils.setSettingsItem('sync_settings', true);\n\n // ダイヤログを閉じる\n this.sync_settings_dialog = false;\n },\n\n // サーバー上の設定でこのクライアントの設定を上書きする\n async overrideClientSettingsFromServer() {\n\n // 強制的にサーバーに保存されている設定データをこのクライアントに同期する\n // 設定の同期を有効化する前に実行しておくのが重要\n await Utils.syncServerSettingsToClient(true);\n\n // 設定の同期を有効化\n // Utils.setSettingsItem() した段階で設定データがサーバーにアップロードされてしまうので、\n // それよりも前に Utils.syncServerSettingsToClient(true) でサーバー上の設定データを同期させておく必要がある\n // さもなければ、サーバー上の設定データがこのクライアントの設定で上書きされてしまい、overrideServerSettingsFromClient() と同じ挙動になってしまう\n this.sync_settings = true;\n Utils.setSettingsItem('sync_settings', true);\n\n // ダイヤログを閉じる\n this.sync_settings_dialog = false;\n },\n\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n const response = await Vue.axios.get('/users/me');\n this.user = response.data;\n this.settings_username = this.user.name;\n\n // 表示中のアイコン画像を更新\n await this.syncAccountIcon();\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n console.log('Not logged in.');\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n // まだアクセストークンが残っているかもしれないので、明示的にログアウト\n Utils.deleteAccessToken();\n }\n }\n },\n\n async syncAccountIcon() {\n\n // ユーザーアカウントのアイコンを取得する\n // 認証が必要な URL は img タグからは直で読み込めないため\n const icon_response = await Vue.axios.get('/users/me/icon', {\n responseType: 'arraybuffer',\n });\n\n // Blob URL を生成\n this.user_icon_blob = URL.createObjectURL(new Blob([icon_response.data], {type: 'image/png'}));\n },\n\n async updateAccountInfo(update_type: 'username' | 'password') {\n\n // すべてのバリデーションが通過したときのみ\n // ref: https://qiita.com/Hijiri_Ishi/items/56cac99c8f3806a6fa24\n if (update_type === 'username') {\n if ((this.$refs.settings_username as any).validate() === false) return;\n } else {\n if ((this.$refs.settings_password as any).validate() === false) return;\n }\n\n try {\n\n // アカウント情報更新 API にリクエスト\n // レスポンスは 204 No Content なので不要\n if (update_type === 'username') {\n await Vue.axios.put('/users/me', {username: this.settings_username});\n this.$message.show('ユーザー名を更新しました。');\n } else {\n await Vue.axios.put('/users/me', {password: this.settings_password});\n this.$message.show('パスワードを更新しました。');\n }\n\n // 表示中のアカウント情報を更新\n await this.syncAccountInfo();\n\n } catch (error) {\n\n // アカウント情報の更新に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Specified username is duplicated': {\n this.$message.error('ユーザー名が重複しています。');\n break;\n }\n case 'Specified username is not accepted due to system limitations': {\n this.$message.error('ユーザー名に token と me は使えません。');\n break;\n }\n default: {\n this.$message.error(`アカウント情報を更新できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n },\n\n async updateAccountIcon() {\n\n // アイコン画像が選択されていないなら更新しない\n if (this.settings_icon === null) {\n this.$message.error('アップロードする画像を選択してください!');\n return;\n }\n\n // アイコン画像の File オブジェクト (= Blob) を FormData に入れる\n // multipart/form-data で送るために必要\n // ref: https://r17n.page/2020/02/04/nodejs-axios-file-upload-api/\n const form_data = new FormData();\n form_data.append('image', this.settings_icon);\n\n try {\n\n // アカウントアイコン画像更新 API にリクエスト\n await Vue.axios.put('/users/me/icon', form_data, {headers: {'Content-Type': 'multipart/form-data'}});\n\n // 表示中のアイコン画像を更新\n await this.syncAccountIcon();\n\n } catch (error) {\n\n // アカウント情報の更新に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Please upload JPEG or PNG image': {\n this.$message.error('JPEG または PNG 画像をアップロードしてください。');\n break;\n }\n default: {\n this.$message.error(`アイコン画像を更新できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n },\n\n async deleteAccount() {\n\n // ダイヤログを閉じる\n this.account_delete_confirm_dialog = false;\n\n // アカウント削除 API にリクエスト\n await Vue.axios.delete('/users/me');\n\n // 設定の同期を無効化\n Utils.setSettingsItem('sync_settings', false);\n\n // ブラウザからアクセストークンを削除\n Utils.deleteAccessToken();\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n this.$message.show('アカウントを削除しました。');\n },\n\n logout() {\n\n // 設定の同期を無効化\n Utils.setSettingsItem('sync_settings', false);\n\n // ブラウザからアクセストークンを削除\n // これをもってログアウトしたことになる(それ以降の Axios のリクエストにはアクセストークンが含まれなくなる)\n Utils.deleteAccessToken();\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n this.$message.success('ログアウトしました。');\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Account.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Account.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Account.vue?vue&type=template&id=12036e32&scoped=true&\"\nimport script from \"./Account.vue?vue&type=script&lang=ts&\"\nexport * from \"./Account.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Account.vue?vue&type=style&index=0&id=12036e32&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"12036e32\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"19px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"ニコニコ実況\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[(_vm.user.niconico_user_id === null)?_c('div',{staticClass:\"niconico-account niconico-account--anonymous\"},[_c('div',{staticClass:\"niconico-account-wrapper\"},[_c('Icon',{staticClass:\"flex-shrink-0\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"45px\"}}),_c('div',{staticClass:\"niconico-account__info ml-4\"},[_c('div',{staticClass:\"niconico-account__info-name\"},[_c('span',{staticClass:\"niconico-account__info-name-text\"},[_vm._v(\"ニコニコアカウントと連携していません\")])]),_c('span',{staticClass:\"niconico-account__info-description\"},[_vm._v(\" ニコニコアカウントと連携すると、テレビを見ながらニコニコ実況にコメントできるようになります。 \")])])],1),_c('v-btn',{staticClass:\"niconico-account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"130\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.loginNiconicoAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-connected-20-filled\",\"height\":\"26\"}}),_vm._v(\"連携する \")],1)],1):_vm._e(),(_vm.user.niconico_user_id !== null)?_c('div',{staticClass:\"niconico-account\"},[_c('div',{staticClass:\"niconico-account-wrapper\"},[_c('img',{staticClass:\"niconico-account__icon\",attrs:{\"src\":this.niconico_user_icon_url}}),_c('div',{staticClass:\"niconico-account__info\"},[_c('div',{staticClass:\"niconico-account__info-name\"},[_c('span',{staticClass:\"niconico-account__info-name-text\"},[_vm._v(_vm._s(_vm.user.niconico_user_name)+\" と連携しています\")])]),_c('span',{staticClass:\"niconico-account__info-description\"},[_c('span',{staticClass:\"mr-2\"},[_vm._v(\"Niconico User ID:\")]),_c('a',{staticClass:\"mr-2\",attrs:{\"href\":`https://www.nicovideo.jp/user/${_vm.user.niconico_user_id}`,\"target\":\"_blank\"}},[_vm._v(_vm._s(_vm.user.niconico_user_id))]),(_vm.user.niconico_user_premium == true)?_c('span',{staticClass:\"secondary--text\"},[_vm._v(\"(Premium)\")]):_vm._e()])])]),_c('v-btn',{staticClass:\"niconico-account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"130\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logoutNiconicoAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-disconnected-20-filled\",\"height\":\"26\"}}),_vm._v(\"連携解除 \")],1)],1):_vm._e(),_c('div',{staticClass:\"settings__item mt-7\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントのミュート設定\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 表示したくないコメントを、画面やコメントリストに表示しないようにミュートできます。\"),_c('br')])]),_c('v-btn',{staticClass:\"settings__save-button mt-4\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.comment_mute_settings_modal = !_vm.comment_mute_settings_modal}}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"19px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"コメントのミュート設定を開く\")])],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの速さ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーに流れるコメントの速さを設定します。\"),_c('br'),_vm._v(\" たとえば 1.2 に設定すると、コメントが 1.2 倍速く流れます。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"step\":0.1,\"min\":0.5,\"max\":2},model:{value:(_vm.settings.comment_speed_rate),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_speed_rate\", $$v)},expression:\"settings.comment_speed_rate\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの文字サイズ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーに流れるコメントの文字サイズの基準値を設定します。\"),_c('br'),_vm._v(\" 実際の文字サイズは画面の大きさに合わせて調整されます。既定の文字サイズは 34px です。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"min\":20,\"max\":60},model:{value:(_vm.settings.comment_font_size),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_font_size\", $$v)},expression:\"settings.comment_font_size\"}})],1),_c('div',{staticClass:\"settings__item settings__item--sync-disabled\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの遅延時間\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーやコメントリストに表示されるコメントを何秒遅らせて反映するかを設定します。\"),_c('br'),_vm._v(\" 通常は 1.75 秒程度で大丈夫です。ネットワークが遅いなどでタイムラグが大きいときだけ、映像の遅延に合わせて調整してください。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"step\":0.25,\"min\":0,\"max\":10},model:{value:(_vm.settings.comment_delay_time),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_delay_time\", $$v)},expression:\"settings.comment_delay_time\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"close_comment_form_after_sending\"}},[_vm._v(\"コメント送信後にコメント入力フォームを閉じる\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"close_comment_form_after_sending\"}},[_vm._v(\" コメントを送信したあとに、コメント入力フォームを自動的に閉じるかを設定します。\"),_c('br'),_vm._v(\" 基本的にはオンのままにしておくことをおすすめします。コメント入力フォームが表示されたままだと、大部分のショートカットキーが文字入力と競合して使えないためです。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"close_comment_form_after_sending\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.close_comment_form_after_sending),callback:function ($$v) {_vm.$set(_vm.settings, \"close_comment_form_after_sending\", $$v)},expression:\"settings.close_comment_form_after_sending\"}})],1)],1),_c('CommentMuteSettings',{model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport CommentMuteSettings from '@/components/Settings/CommentMuteSettings.vue';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Jikkyo',\n components: {\n Base,\n CommentMuteSettings,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // ニコニコアカウントのユーザーアイコンの URL\n niconico_user_icon_url: '',\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [\n 'comment_speed_rate',\n 'comment_font_size',\n 'comment_delay_time',\n 'close_comment_form_after_sending',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n // 初回描画で niconico_user_id が null かを判定するだけのためにセットしている\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n }\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n const response = await Vue.axios.get('/users/me');\n this.user = response.data;\n\n // ニコニコアカウントのユーザーアイコンの URL を生成 (ニコニコアカウントと連携されている場合のみ)\n if (this.user.niconico_user_id !== null) {\n const user_id_slice = this.user.niconico_user_id.toString().slice(0, 4);\n this.niconico_user_icon_url =\n `https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/${user_id_slice}/${this.user.niconico_user_id}.jpg`;\n }\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n async loginNiconicoAccount() {\n\n // ログインしていない場合はエラーにする\n if (this.is_logged_in === false) {\n this.$message.warning('連携をはじめるには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // ニコニコアカウントと連携するための認証 URL を取得\n const authorization_url = (await Vue.axios.get('/niconico/auth')).data.authorization_url;\n\n // OAuth 連携のため、認証 URL をポップアップウインドウで開く\n // window.open() の第2引数はユニークなものにしておくと良いらしい\n // ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1 (大変参考になりました)\n const popup_window = window.open(authorization_url, 'KonomiTV-OAuthPopup', Utils.getWindowFeatures());\n\n // 認証完了 or 失敗後、ポップアップウインドウから送信される文字列を受信\n const onMessage = async (event) => {\n\n // すでにウインドウが閉じている場合は実行しない\n if (popup_window.closed) return;\n\n // 受け取ったオブジェクトに KonomiTV-OAuthPopup キーがない or そもそもオブジェクトではない際は実行しない\n // ブラウザの拡張機能から結構余計な message が飛んでくるっぽい…。\n if (Utils.typeof(event.data) !== 'object') return;\n if (('KonomiTV-OAuthPopup' in event.data) === false) return;\n\n // 認証は完了したので、ポップアップウインドウを閉じ、リスナーを解除する\n if (popup_window) popup_window.close();\n window.removeEventListener('message', onMessage);\n\n // ステータスコードと詳細メッセージを取得\n const authorization_status = event.data['KonomiTV-OAuthPopup']['status'] as number;\n const authorization_detail = event.data['KonomiTV-OAuthPopup']['detail'] as string;\n console.log(`NiconicoAuthCallbackAPI: Status: ${authorization_status} / Detail: ${authorization_detail}`);\n\n // OAuth 連携に失敗した\n if (authorization_status !== 200) {\n if (authorization_detail.startsWith('Authorization was denied (access_denied)')) {\n this.$message.error('ニコニコアカウントとの連携がキャンセルされました。');\n } else if (authorization_detail.startsWith('Failed to get access token (HTTP Error ')) {\n const error = authorization_detail.replace('Failed to get access token ', '');\n this.$message.error(`アクセストークンの取得に失敗しました。${error}`);\n } else if (authorization_detail.startsWith('Failed to get access token (Connection Timeout)')) {\n this.$message.error('アクセストークンの取得に失敗しました。ニコニコで障害が発生している可能性があります。');\n } else if (authorization_detail.startsWith('Failed to get user information (HTTP Error ')) {\n const error = authorization_detail.replace('Failed to get user information ', '');\n this.$message.error(`ニコニコアカウントのユーザー情報の取得に失敗しました。${error}`);\n } else if (authorization_detail.startsWith('Failed to get user information (Connection Timeout)')) {\n this.$message.error('ニコニコアカウントのユーザー情報の取得に失敗しました。ニコニコで障害が発生している可能性があります。');\n } else {\n this.$message.error(`ニコニコアカウントとの連携に失敗しました。(${authorization_detail})`);\n }\n return;\n }\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success('ニコニコアカウントと連携しました。');\n };\n\n // postMessage() を受信するリスナーを登録\n window.addEventListener('message', onMessage);\n },\n\n async logoutNiconicoAccount() {\n\n // ニコニコアカウント連携解除 API にリクエスト\n await Vue.axios.delete('/niconico/logout');\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success('ニコニコアカウントとの連携を解除しました。');\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Jikkyo.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Jikkyo.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Jikkyo.vue?vue&type=template&id=786083d5&scoped=true&\"\nimport script from \"./Jikkyo.vue?vue&type=script&lang=ts&\"\nexport * from \"./Jikkyo.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Jikkyo.vue?vue&type=style&index=0&id=786083d5&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"786083d5\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"22px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"Twitter\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[_c('div',{staticClass:\"twitter-accounts\"},[(_vm.user.twitter_accounts.length > 0)?_c('div',{staticClass:\"twitter-accounts__heading\"},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-board-20-filled\",\"height\":\"30\"}}),_vm._v(\"連携中のアカウント \")],1):_vm._e(),(_vm.user.twitter_accounts.length === 0)?_c('div',{staticClass:\"twitter-accounts__guide\"},[_c('Icon',{staticClass:\"flex-shrink-0\",attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"45px\"}}),_c('div',{staticClass:\"ml-4\"},[_c('div',{staticClass:\"font-weight-bold text-h6\"},[_vm._v(\"Twitter アカウントと連携していません\")]),_c('div',{staticClass:\"text--text text--darken-1 text-subtitle-2 mt-1\"},[_vm._v(\" Twitter アカウントと連携すると、テレビを見ながら Twitter にツイートしたり、ほかの実況ツイートをリアルタイムで表示できるようになります。 \")])])],1):_vm._e(),_vm._l((_vm.user.twitter_accounts),function(twitter_account){return _c('div',{key:twitter_account.id,staticClass:\"twitter-account\"},[_c('img',{staticClass:\"twitter-account__icon\",attrs:{\"src\":twitter_account.icon_url}}),_c('div',{staticClass:\"twitter-account__info\"},[_c('div',{staticClass:\"twitter-account__info-name\"},[_c('span',{staticClass:\"twitter-account__info-name-text\"},[_vm._v(_vm._s(twitter_account.name))])]),_c('span',{staticClass:\"twitter-account__info-screen-name\"},[_vm._v(\"@\"+_vm._s(twitter_account.screen_name))])]),_c('v-btn',{staticClass:\"twitter-account__logout ml-auto\",attrs:{\"width\":\"124\",\"height\":\"52\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logoutTwitterAccount(twitter_account.screen_name)}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-disconnected-20-filled\",\"height\":\"24\"}}),_vm._v(\"連携解除 \")],1)],1)}),_c('v-btn',{staticClass:\"twitter-account__login\",attrs:{\"color\":\"secondary\",\"max-width\":\"250\",\"height\":\"50\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.loginTwitterAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-connected-20-filled\",\"height\":\"24\"}}),_vm._v(\"連携するアカウントを追加 \")],1)],2),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"fold_panel_after_sending_tweet\"}},[_vm._v(\"ツイート送信後にパネルを閉じる\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"fold_panel_after_sending_tweet\"}},[_vm._v(\" ツイートを送信した後に、表示中のパネルを閉じる(折りたたむ)かを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"fold_panel_after_sending_tweet\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.fold_panel_after_sending_tweet),callback:function ($$v) {_vm.$set(_vm.settings, \"fold_panel_after_sending_tweet\", $$v)},expression:\"settings.fold_panel_after_sending_tweet\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"reset_hashtag_when_program_switches\"}},[_vm._v(\"番組が切り替わったときにハッシュタグフォームをリセットする\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"reset_hashtag_when_program_switches\"}},[_vm._v(\" チャンネルを切り替えたときや、視聴中の番組が終了し次の番組の放送が開始されたときに、ハッシュタグフォームをリセットするかを設定します。\"),_c('br'),_vm._v(\" この設定をオンにしておけば、「誤って前番組のハッシュタグをつけたまま次番組の実況ツイートをしてしまう」といったミスを回避できます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"reset_hashtag_when_program_switches\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.reset_hashtag_when_program_switches),callback:function ($$v) {_vm.$set(_vm.settings, \"reset_hashtag_when_program_switches\", $$v)},expression:\"settings.reset_hashtag_when_program_switches\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"auto_add_watching_channel_hashtag\"}},[_vm._v(\"視聴中のチャンネルに対応する局タグを自動的に追加する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"auto_add_watching_channel_hashtag\"}},[_vm._v(\" ハッシュタグフォームに、視聴中のチャンネルに対応する局タグ (#nhk, #tokyomx など) を自動的に追加するかを設定します。\"),_c('br'),_vm._v(\" 現時点で、局タグは三大首都圏の地上波・BS の一部チャンネル・AT-X にのみ対応しています。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"auto_add_watching_channel_hashtag\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.auto_add_watching_channel_hashtag),callback:function ($$v) {_vm.$set(_vm.settings, \"auto_add_watching_channel_hashtag\", $$v)},expression:\"settings.auto_add_watching_channel_hashtag\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"既定で表示される Twitter タブ内のタブ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 視聴画面を開いたときに、パネルの Twitter タブの中で最初に表示されるタブを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.twitter_active_tab},model:{value:(_vm.settings.twitter_active_tab),callback:function ($$v) {_vm.$set(_vm.settings, \"twitter_active_tab\", $$v)},expression:\"settings.twitter_active_tab\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ツイートにつけるハッシュタグの位置\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" ツイート本文から見て、ハッシュタグをどの位置につけてツイートするかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tweet_hashtag_position},model:{value:(_vm.settings.tweet_hashtag_position),callback:function ($$v) {_vm.$set(_vm.settings, \"tweet_hashtag_position\", $$v)},expression:\"settings.tweet_hashtag_position\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ツイートするキャプチャに番組タイトルの透かしを描画する\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" ツイートするキャプチャに、視聴中の番組タイトルの透かしを描画するかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tweet_capture_watermark_position},model:{value:(_vm.settings.tweet_capture_watermark_position),callback:function ($$v) {_vm.$set(_vm.settings, \"tweet_capture_watermark_position\", $$v)},expression:\"settings.tweet_capture_watermark_position\"}})],1)])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Twitter',\n components: {\n Base,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // 既定で表示されるパネルのタブの選択肢\n twitter_active_tab: [\n {'text': 'ツイート検索タブ', 'value': 'Search'},\n {'text': 'タイムラインタブ', 'value': 'Timeline'},\n {'text': 'キャプチャタブ', 'value': 'Capture'},\n ],\n\n // ツイートにつけるハッシュタグの位置の選択肢\n tweet_hashtag_position: [\n {'text': 'ツイート本文の前に追加する', 'value': 'Prepend'},\n {'text': 'ツイート本文の後に追加する', 'value': 'Append'},\n {'text': 'ツイート本文の前に追加してから改行する', 'value': 'PrependWithLineBreak'},\n {'text': 'ツイート本文の後に改行してから追加する', 'value': 'AppendWithLineBreak'},\n ],\n\n // ツイートするキャプチャに番組タイトルの透かしを描画する位置の選択肢\n tweet_capture_watermark_position: [\n {'text': '透かしを描画しない', 'value': 'None'},\n {'text': '透かしをキャプチャの左上に描画する', 'value': 'TopLeft'},\n {'text': '透かしをキャプチャの右上に描画する', 'value': 'TopRight'},\n {'text': '透かしをキャプチャの左下に描画する', 'value': 'BottomLeft'},\n {'text': '透かしをキャプチャの右下に描画する', 'value': 'BottomRight'},\n ],\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [\n 'fold_panel_after_sending_tweet',\n 'reset_hashtag_when_program_switches',\n 'auto_add_watching_channel_hashtag',\n 'twitter_active_tab',\n 'tweet_hashtag_position',\n 'tweet_capture_watermark_position',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n }\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n this.user = (await Vue.axios.get('/users/me')).data;\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n async loginTwitterAccount() {\n\n // ログインしていない場合はエラーにする\n if (this.is_logged_in === false) {\n this.$message.warning('連携をはじめるには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // Twitter アカウントと連携するための認証 URL を取得\n const authorization_url = (await Vue.axios.get('/twitter/auth')).data.authorization_url;\n\n // OAuth 連携のため、認証 URL をポップアップウインドウで開く\n // window.open() の第2引数はユニークなものにしておくと良いらしい\n // ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1 (大変参考になりました)\n const popup_window = window.open(authorization_url, 'KonomiTV-OAuthPopup', Utils.getWindowFeatures());\n\n // 認証完了 or 失敗後、ポップアップウインドウから送信される文字列を受信\n const onMessage = async (event) => {\n\n // すでにウインドウが閉じている場合は実行しない\n if (popup_window.closed) return;\n\n // 受け取ったオブジェクトに KonomiTV-OAuthPopup キーがない or そもそもオブジェクトではない際は実行しない\n // ブラウザの拡張機能から結構余計な message が飛んでくるっぽい…。\n if (Utils.typeof(event.data) !== 'object') return;\n if (('KonomiTV-OAuthPopup' in event.data) === false) return;\n\n // 認証は完了したので、ポップアップウインドウを閉じ、リスナーを解除する\n if (popup_window) popup_window.close();\n window.removeEventListener('message', onMessage);\n\n // ステータスコードと詳細メッセージを取得\n const authorization_status = event.data['KonomiTV-OAuthPopup']['status'] as number;\n const authorization_detail = event.data['KonomiTV-OAuthPopup']['detail'] as string;\n console.log(`TwitterAuthCallbackAPI: Status: ${authorization_status} / Detail: ${authorization_detail}`);\n\n // OAuth 連携に失敗した\n if (authorization_status !== 200) {\n if (authorization_detail.startsWith('Authorization was denied by user')) {\n this.$message.error('Twitter アカウントとの連携がキャンセルされました。');\n } else if (authorization_detail.startsWith('Failed to get access token')) {\n this.$message.error('アクセストークンの取得に失敗しました。');\n } else if (authorization_detail.startsWith('Failed to get user information')) {\n this.$message.error('Twitter アカウントのユーザー情報の取得に失敗しました。');\n } else {\n this.$message.error(`Twitter アカウントとの連携に失敗しました。(${authorization_detail})`);\n }\n return;\n }\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n // ログイン中のユーザーに紐づく Twitter アカウントのうち、一番 updated_at が新しいものを取得\n // ログインすると updated_at が更新されるため、この時点で一番 updated_at が新しいアカウントが今回連携したものだと判断できる\n // ref: https://stackoverflow.com/a/12192544/17124142 (ISO8601 のソートアルゴリズム)\n const current_twitter_account = [...this.user.twitter_accounts].sort((a, b) => {\n return (a.updated_at < b.updated_at) ? 1 : ((a.updated_at > b.updated_at) ? -1 : 0);\n })[0];\n\n this.$message.success(`Twitter @${current_twitter_account.screen_name} と連携しました。`);\n };\n\n // postMessage() を受信するリスナーを登録\n window.addEventListener('message', onMessage);\n },\n\n async logoutTwitterAccount(screen_name: string) {\n\n // Twitter アカウント連携解除 API にリクエスト\n await Vue.axios.delete(`/twitter/accounts/${screen_name}`);\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success(`Twitter @${screen_name} との連携を解除しました。`);\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Twitter.vue?vue&type=template&id=1970b264&scoped=true&\"\nimport script from \"./Twitter.vue?vue&type=script&lang=ts&\"\nexport * from \"./Twitter.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Twitter.vue?vue&type=style&index=0&id=1970b264&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"1970b264\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"22px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"環境設定\")])],1),_c('div',{staticClass:\"settings__content\"},[_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"鋭意開発中…\")])])])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Environment',\n components: {\n Base,\n },\n data() {\n return {\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Environment.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Environment.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Environment.vue?vue&type=template&id=39075e10&\"\nimport script from \"./Environment.vue?vue&type=script&lang=ts&\"\nexport * from \"./Environment.vue?vue&type=script&lang=ts&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"login-container-wrapper d-flex align-center w-100 mb-13\"},[_c('v-card',{staticClass:\"login-container px-10 pt-8 pb-11 mx-auto background lighten-1\",attrs:{\"elevation\":\"10\",\"width\":\"100%\",\"max-width\":\"450\"}},[_c('v-card-title',{staticClass:\"login__logo justify-center pb-7\"},[_c('v-img',{attrs:{\"max-width\":\"250\",\"src\":\"/assets/images/logo.svg\"}})],1),_c('v-divider'),_c('v-form',{ref:\"login\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('v-text-field',{staticClass:\"mt-12\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"hide-details\":\"\",\"autofocus\":\"\",\"dense\":_vm.is_form_dense},model:{value:(_vm.username),callback:function ($$v) {_vm.username=$$v},expression:\"username\"}}),_c('v-text-field',{staticClass:\"mt-8\",attrs:{\"outlined\":\"\",\"placeholder\":\"パスワード\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"type\":_vm.password_showing ? 'text' : 'password',\"append-icon\":_vm.password_showing ? 'mdi-eye' : 'mdi-eye-off'},on:{\"click:append\":function($event){_vm.password_showing = !_vm.password_showing}},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}}),_c('v-btn',{staticClass:\"login-button mt-5\",attrs:{\"color\":\"secondary\",\"depressed\":\"\",\"width\":\"100%\",\"height\":\"56\"},on:{\"click\":function($event){return _vm.login()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-in\"}}),_vm._v(\"ログイン \")],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Login',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n username: '' as string,\n password: '' as string,\n password_showing: false,\n }\n },\n methods: {\n async login() {\n\n // ユーザー名またはパスワードが空\n if (this.username === '' || this.password === '') return;\n\n try {\n\n // ログインしてアクセストークンを取得する\n const response = await Vue.axios.post('/users/token', new URLSearchParams({\n username: this.username,\n password: this.password,\n }));\n\n // 取得したアクセストークンを保存\n console.log('Login successful.');\n console.log(response.data);\n Utils.saveAccessToken(response.data.access_token);\n\n // アカウントページに遷移\n this.$message.success('ログインしました。');\n await this.$router.push({path: '/settings/account'});\n\n } catch (error) {\n\n // ログインに失敗\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n console.log('Failed to login.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Incorrect username': {\n this.$message.error('ログインできませんでした。そのユーザー名のアカウントは存在しません。');\n break;\n }\n case 'Incorrect password': {\n this.$message.error('ログインできませんでした。パスワードを間違えていませんか?');\n break;\n }\n default: {\n this.$message.error(`ログインできませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Login.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Login.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Login.vue?vue&type=template&id=0c2bb32a&scoped=true&\"\nimport script from \"./Login.vue?vue&type=script&lang=ts&\"\nexport * from \"./Login.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Login.vue?vue&type=style&index=0&id=0c2bb32a&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0c2bb32a\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"register-container-wrapper d-flex align-center w-100 mb-13\"},[_c('v-card',{staticClass:\"register-container px-10 pt-8 pb-11 mx-auto background lighten-1\",attrs:{\"elevation\":\"10\",\"width\":\"100%\",\"max-width\":\"450\"}},[_c('v-card-title',{staticClass:\"register__logo flex-column justify-center\"},[_c('v-img',{attrs:{\"max-width\":\"250\",\"src\":\"/assets/images/logo.svg\"}}),_c('h4',{staticClass:\"mt-10\"},[_vm._v(\"アカウントを作成\")])],1),_c('v-divider'),_c('v-form',{ref:\"register\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('v-text-field',{staticClass:\"mt-10\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"autofocus\":\"\",\"dense\":_vm.is_form_dense,\"rules\":[_vm.username_validation]},model:{value:(_vm.username),callback:function ($$v) {_vm.username=$$v},expression:\"username\"}}),_c('v-text-field',{staticClass:\"mt-2\",attrs:{\"outlined\":\"\",\"placeholder\":\"パスワード\",\"dense\":_vm.is_form_dense,\"type\":_vm.password_showing ? 'text' : 'password',\"append-icon\":_vm.password_showing ? 'mdi-eye' : 'mdi-eye-off',\"rules\":[_vm.password_validation]},on:{\"click:append\":function($event){_vm.password_showing = !_vm.password_showing}},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}}),_c('v-btn',{staticClass:\"register-button mt-5\",attrs:{\"color\":\"secondary\",\"depressed\":\"\",\"width\":\"100%\",\"height\":\"56\"},on:{\"click\":function($event){return _vm.register()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-add-20-filled\",\"height\":\"24\"}}),_vm._v(\"アカウントを作成 \")],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Register',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n username: null as string | null,\n username_validation: (value: string | null) => {\n if (value === '' || value === null) return 'ユーザー名を入力してください。';\n if (/^.{2,}$/.test(value) === false) return 'ユーザー名は2文字以上で入力してください。';\n return true;\n },\n password: null as string | null,\n password_showing: true, // アカウント作成時は既定でパスワードを表示する\n password_validation: (value: string | null) => {\n if (value === '' || value === null) return 'パスワードを入力してください。';\n // 正規表現の参考: https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128\n if (/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(value) === false) return 'パスワードは4文字以上の半角英数記号を入力してください。';\n return true;\n },\n }\n },\n methods: {\n async register() {\n\n // すべてのバリデーションが通過したときのみ\n // ref: https://qiita.com/Hijiri_Ishi/items/56cac99c8f3806a6fa24\n if ((this.$refs.register as any).validate() === false) return;\n\n try {\n\n // アカウント作成 API にリクエスト\n const response = await Vue.axios.post('/users', {\n username: this.username,\n password: this.password,\n });\n\n console.log('Account created.')\n console.log(response.data);\n\n } catch (error) {\n\n // アカウントの作成に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n\n console.log('Failed to create account.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Specified username is duplicated': {\n this.$message.error('ユーザー名が重複しています。');\n break;\n }\n case 'Specified username is not accepted due to system limitations': {\n this.$message.error('ユーザー名に token と me は使えません。');\n break;\n }\n default: {\n this.$message.error(`アカウントを作成できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n return; // 処理を中断\n }\n\n // ここから先の処理はログイン画面とほぼ同じ\n try {\n\n // アカウントを作成できたので、ログインしてアクセストークンを取得する\n const response = await Vue.axios.post('/users/token', new URLSearchParams({\n username: this.username,\n password: this.password,\n }));\n\n // 取得したアクセストークンを保存\n console.log('Login successful.');\n console.log(response.data);\n Utils.saveAccessToken(response.data.access_token);\n\n // アカウントページに遷移\n this.$message.success('アカウントを作成しました。');\n await this.$router.push({path: '/settings/account'});\n\n } catch (error) {\n\n // ログインに失敗\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n console.log('Failed to login.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Incorrect username': {\n this.$message.error('ログインできませんでした。そのユーザー名のアカウントは存在しません。');\n break;\n }\n case 'Incorrect password': {\n this.$message.error('ログインできませんでした。パスワードを間違えていませんか?');\n break;\n }\n default: {\n this.$message.error(`ログインできませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Register.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Register.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Register.vue?vue&type=template&id=d0eaf0ae&scoped=true&\"\nimport script from \"./Register.vue?vue&type=script&lang=ts&\"\nexport * from \"./Register.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Register.vue?vue&type=style&index=0&id=d0eaf0ae&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"d0eaf0ae\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_vm._m(0)],1)],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"d-flex justify-center align-center w-100\"},[_c('div',{staticClass:\"d-flex justify-center align-center flex-column\"},[_c('h1',[_vm._v(\"Not Found, or Under Development...\")]),_c('span',{staticClass:\"mt-4 text--text text--darken-1\"},[_vm._v(\"お探しのページは存在しないか、鋭意開発中です。\")])])])\n}]\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\nexport default Vue.extend({\n name: 'NotFound',\n components: {\n Header,\n Navigation,\n },\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./NotFound.vue?vue&type=template&id=daa4530a&scoped=true&\"\nimport script from \"./NotFound.vue?vue&type=script&lang=ts&\"\nexport * from \"./NotFound.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./NotFound.vue?vue&type=style&index=0&id=daa4530a&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"daa4530a\",\n null\n \n)\n\nexport default component.exports","\nimport Vue from 'vue';\nimport VueRouter from 'vue-router';\n\nimport TVHome from '@/views/TV/Home.vue';\nimport TVWatch from '@/views/TV/Watch.vue';\nimport SettingsIndex from '@/views/Settings/Index.vue';\nimport SettingsGeneral from '@/views/Settings/General.vue';\nimport SettingsAccount from '@/views/Settings/Account.vue';\nimport SettingsJikkyo from '@/views/Settings/Jikkyo.vue';\nimport SettingsTwitter from '@/views/Settings/Twitter.vue';\nimport SettingsEnvironment from '@/views/Settings/Environment.vue';\nimport Login from '@/views/Login.vue';\nimport Register from '@/views/Register.vue';\nimport NotFound from '@/views/NotFound.vue';\nimport Utils from '@/utils';\n\nVue.use(VueRouter);\n\nconst router = new VueRouter({\n\n // History API モード\n mode: 'history',\n\n // ルーティングのベース URL\n base: process.env.BASE_URL,\n\n // ルーティング設定\n routes: [\n {\n path: '/',\n redirect: '/tv/',\n },\n {\n path: '/tv/',\n name: 'TV Home',\n component: TVHome,\n },\n {\n path: '/tv/watch/:channel_id',\n name: 'TV Watch',\n component: TVWatch,\n },\n {\n path: '/settings/',\n name: 'Settings Index',\n component: SettingsIndex,\n beforeEnter: (to, from, next) => {\n\n // スマホ縦画面・スマホ横画面・タブレット縦画面では設定一覧画面を表示する(画面サイズの関係)\n if (Utils.isSmartphoneVertical() || Utils.isSmartphoneHorizontal() || Utils.isTabletVertical()) {\n next(); // 通常通り遷移\n return;\n }\n\n // それ以外の画面サイズでは全般設定にリダイレクト\n next({path: '/settings/general/'});\n }\n },\n {\n path: '/settings/general',\n name: 'Settings General',\n component: SettingsGeneral,\n },\n {\n path: '/settings/account',\n name: 'Settings Account',\n component: SettingsAccount,\n },\n {\n path: '/settings/jikkyo',\n name: 'Settings Jikkyo',\n component: SettingsJikkyo,\n },\n {\n path: '/settings/twitter',\n name: 'Settings Twitter',\n component: SettingsTwitter,\n },\n {\n path: '/settings/environment',\n name: 'Settings Environment',\n component: SettingsEnvironment,\n },\n {\n path: '/login/',\n name: 'Login',\n component: Login,\n },\n {\n path: '/register/',\n name: 'Register',\n component: Register,\n },\n {\n path: '*',\n name: 'NotFound',\n component: NotFound,\n },\n ],\n\n // ページ遷移時のスクロールの挙動の設定\n // ref: https://v3.router.vuejs.org/ja/guide/advanced/scroll-behavior.html\n scrollBehavior (to, from, savedPosition) {\n if (savedPosition) {\n // 戻る/進むボタンが押されたときは保存されたスクロール位置を使う\n return savedPosition;\n } else {\n // それ以外は常に先頭にスクロールする\n return {x: 0, y: 0};\n }\n }\n});\n\nexport default router;\n","/* eslint-disable no-console */\n\nimport { register } from 'register-service-worker';\n\nif (process.env.NODE_ENV === 'production') {\n register(`${process.env.BASE_URL}service-worker.js`, {\n ready () {\n console.log(\n 'App is being served from cache by a service worker.\\n' +\n 'For more details, visit https://goo.gl/AFskqB'\n )\n },\n registered () {\n console.log('Service worker has been registered.')\n },\n cached () {\n console.log('Content has been cached for offline use.')\n },\n updatefound () {\n console.log('New content is downloading.')\n },\n updated () {\n console.log('New content is available; please refresh.')\n },\n offline () {\n console.log('No internet connection found. App is running in offline mode.')\n },\n error (error) {\n console.error('Error during service worker registration:', error)\n }\n });\n}\n","\nimport { Icon } from '@iconify/vue2';\nimport { polyfill as SeamlessScrollPolyfill } from \"seamless-scroll-polyfill\";\nimport Vue from 'vue';\nimport VueAxios from 'vue-axios';\nimport VueVirtualScroller from 'vue-virtual-scroller';\nimport 'vue-virtual-scroller/dist/vue-virtual-scroller.css';\nimport VuetifyMessageSnackbar from 'vuetify-message-snackbar';\nimport VTooltip from 'v-tooltip';\nimport 'v-tooltip/dist/v-tooltip.css';\n\nimport App from '@/App.vue';\nimport VTabItem from '@/components/VTabItem';\nimport VTabs from '@/components/VTabs';\nimport VTabsItems from '@/components/VTabsItems';\nimport axios from '@/plugins/axios';\nimport vuetify from '@/plugins/vuetify';\nimport router from '@/router';\nimport '@/service-worker';\nimport Utils from './utils';\n\n// スムーズスクロール周りの API の polyfill を適用\n// Element.scrollInfoView() のオプション指定を使うために必要\nSeamlessScrollPolyfill();\n\n// Production Tip を非表示に\nVue.config.productionTip = false;\n\n// Axios を使う\nVue.use(VueAxios, axios);\n\n// vue-virtual-scroller を使う\nVue.use(VueVirtualScroller);\n\n// vuetify-message-snackbar を使う\n// マイナーな OSS(しかも中国語…)だけど、Snackbar を関数で呼びたかったのでちょうどよかった\n// ref: https://github.com/thinkupp/vuetify-message-snackbar\nVue.use(VuetifyMessageSnackbar, {\n // 画面上に配置しない\n top: false,\n // 画面下に配置する\n bottom: true,\n // デフォルトの背景色\n color: '#433532',\n // ダークテーマを適用する\n dark: true,\n // 影 (Elevation) の設定\n elevation: 8,\n // 2.5秒でタイムアウト\n timeout: 2500,\n // 要素が非表示になった後に DOM から要素を削除する\n\tautoRemove: true,\n // 閉じるボタンのテキスト\n\tcloseButtonContent: '閉じる',\n\t// Vuetify のインスタンス\n\tvuetifyInstance: vuetify,\n});\n\n// VTooltip を使う\n// タッチデバイスでは無効化する\n// ref: https://v-tooltip.netlify.app/guide/config.html#default-values\nconst trigger = window.matchMedia('(hover: none)').matches ? [] : ['hover', 'focus', 'touch'];\nVTooltip.options.themes.tooltip.showTriggers = trigger;\nVTooltip.options.themes.tooltip.hideTriggers = trigger;\nVTooltip.options.themes.tooltip.delay.show = 0;\nVTooltip.options.offset = [0, 7];\nVue.use(VTooltip);\n\n// Iconify(アイコン)のグローバルコンポーネント\nVue.component('Icon', Icon);\n\n// VTabItem の挙動を改善するグローバルコンポーネント\nVue.component('v-tab-item-fix', VTabItem);\n\n// VTabs の挙動を改善するグローバルコンポーネント\nVue.component('v-tabs-fix', VTabs);\n\n// VTabsItems の挙動を改善するグローバルコンポーネント\nVue.component('v-tabs-items-fix', VTabsItems);\n\n// Vue を初期化\nnew Vue({\n router,\n vuetify,\n render: h => h(App),\n}).$mount('#app');\n\n// ログイン時かつ設定の同期が有効なとき、ページ遷移に関わらず、常に3秒おきにサーバーから設定を取得する\n// 初回のページレンダリングに間に合わないのは想定内(同期の完了を待つこともできるが、それだと表示速度が遅くなるのでしょうがない)\nwindow.setInterval(async () => {\n if (Utils.getAccessToken() !== null && Utils.getSettingsItem('sync_settings') === true) {\n Utils.syncServerSettingsToClient();\n }\n}, 3 * 1000); // 3秒おき\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","__webpack_require__.amdO = {};","var deferred = [];\n__webpack_require__.O = function(result, chunkIds, fn, priority) {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar chunkIds = deferred[i][0];\n\t\tvar fn = deferred[i][1];\n\t\tvar priority = deferred[i][2];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tvar getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.hmd = function(module) {\n\tmodule = Object.create(module);\n\tif (!module.children) module.children = [];\n\tObject.defineProperty(module, 'exports', {\n\t\tenumerable: true,\n\t\tset: function() {\n\t\t\tthrow new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);\n\t\t}\n\t});\n\treturn module;\n};","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t143: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = function(parentChunkLoadingFunction, data) {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkKonomiTV\"] = self[\"webpackChunkKonomiTV\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [998], function() { return __webpack_require__(8435); })\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["_vm","_c","script","component","render","staticRenderFns","VTabItem","h","props","name","this","computedTransition","on","beforeEnter","onBeforeTransition","afterEnter","onAfterTransition","enterCancelled","onTransitionCancelled","beforeLeave","afterLeave","leaveCancelled","enter","onEnter","genWindowItem","VTabsBar","data","items","methods","register","item","activeItem","internalIndex","push","sort","a","b","index_a","$slots","default","findIndex","element","$vnode","key","index_b","$on","onClick","mandatory","selectedValues","length","updateMandatory","updateItem","indexOf","undefined","updateInternalValue","unregister","constructor","super","options","call","VTabs","genBar","slider","style","height","convertToUnit","activeClass","centerActive","dark","light","optional","mobileBreakpoint","nextIcon","prevIcon","showArrows","value","internalValue","callSlider","change","val","ref","setTextColor","computedColor","setBackgroundColor","backgroundColor","$createElement","genSlider","VTabsItems","updateReverse","oldVal","itemsLength","lastIndex","continuous","Utils","localStorage","getItem","setItem","JSON","stringify","default_settings","settings","parse","new_settings","default_settings_key","Object","keys","syncClientSettingsToServer","force","getAccessToken","sync_settings","uploading_settings","sleep","server_settings","Vue","server_settings_key","server_settings_value","entries","error","sync_settings_key","sync_settings_keys","access_token","removeItem","test","navigator","userAgent","blob","filename","blob_url","URL","createObjectURL","link","document","createElement","download","href","click","revokeObjectURL","content","html_escape_table","replace","match","popupSizeWidth","popupSizeHeight","window","screen","posTop","posLeft","width","class_name","activeElement","classList","contains","matchMedia","matches","base","Math","floor","seconds","Promise","resolve","setTimeout","Date","now","prototype","toString","slice","toLowerCase","text","escapeHTML","pattern","process","location","protocol","host","pinned_channel_ids","showed_panel_last_time","selected_twitter_account_id","saved_twitter_hashtags","tv_streaming_quality","tv_data_saver_mode","tv_low_latency_mode","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_copy_to_clipboard","capture_save_mode","capture_caption_mode","comment_speed_rate","comment_font_size","comment_delay_time","close_comment_form_after_sending","muted_comment_keywords","muted_niconico_user_ids","mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments","fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position","ChannelUtils","channel_id","is_pretty","result","groups","channel_type","toUpperCase","channel_force","channels_list","remocon_id","channel_type_pretty","channels","get","index","channel","getChannelType","previous_index","next_index","PlayerCaptureHandler","player","captured_callback","player_container","container","querySelector","insertAdjacentHTML","comment_capture_button","capture_button","canvas","OffscreenCanvas","canvas_context","getContext","alpha","desynchronized","willReadFrequently","async","video","videoWidth","videoHeight","with_comments","total_time","time","is_radiochannel","notice","danmaku","showing","addHighlight","filename_base","dayjs","format","filename_caption","caption_canvas","plugins","aribb24Caption","getRawCanvas","superimpose_canvas","aribb24Superimpose","is_caption_showing","isShowing","isPresent","is_superimpose_showing","caption_text","getTextContent","export_and_save","program","is_caption_composited","is_comment_composited","exportToBlob","console","log","mathFloor","setEXIFDataToCapture","includes","getSettingsItem","downloadBlobData","uploadCaptureToServer","capture_normal","capture_caption","image_bitmap","createImageBitmap","promises","drawImage","comments_image","createCommentsImage","drawComments","filename_real","program_present","close","all","bitmap_canvas","transferFromImageBitmap","removeHighlight","capture","copyBlobToClipboard","convertBlobToPng","add","remove","html","svg","trim","image","Image","src","encodeURIComponent","decode","comments_html","template","outerHTML","comment","querySelectorAll","position","getBoundingClientRect","left","replaceAll","commentsHTMLtoSVGImage","offsetWidth","offsetHeight","draw_scale_ratio","draw_height","convertToBlob","type","quality","reject","toBlob","Error","captured_playback_position","diff","start_time","json","captured_at","network_id","service_id","event_id","title","description","end_time","duration","datetime","exif","piexif","TagValues","ImageIFD","XResolution","YResolution","ResolutionUnit","YCbCrPositioning","DateTime","Software","version","XPComment","Buffer","ExifIFD","ExifVersion","ComponentsConfiguration","FlashpixVersion","ColorSpace","DateTimeOriginal","DateTimeDigitized","exif_string","dump","blob_string","reader","FileReader","onload","onerror","readAsBinaryString","blob_string_new","insert","buffer","Uint8Array","charCodeAt","Blob","form_data","FormData","append","headers","PlayerUtils","background_count","random","padStart","canPlayType","ProgramUtils","mark","pattern1","RegExp","pattern2","isSameOrAfter","isSameOrBefore","isBetween","pause_time_start","hour","minute","second","pause_time_end","pause_time_start_23","pause_time_end_23","default_value","progress","is_short","axios_instance","axios","interceptors","request","use","config","baseURL","Vuetify","VSnackbar","VBtn","VIcon","theme","themes","primary","secondary","twitter","lighten1","lighten2","gray","black","background","lighten3","darken1","darken2","darken3","customProperties","staticClass","attrs","model","callback","expression","Array","from","channels_type","_v","tab","class","rawName","id","staticStyle","directives","preventDefault","$event","removePinnedChannel","stopPropagation","domProps","_s","latest_version","is_update_available","version_info","components","Header","Navigation","is_loading","interval_ids","Map","created","update","residue_second","getTime","setInterval","beforeDestroy","interval_id","clearInterval","channels_response","filter","is_display","GR","set","BS","CS","CATV","SKY","STARDIGIO","updatePinnedChannelList","addPinnedChannel","splice","is_update_tab","pinned_channels","pinned_channel_id","pinned_channel_type","pinned_channel","find","has","isPinnedChannel","controlDisplayTimer","modifiers","api_base_url","backgroundImage","background_url","is_video_buffering","channel_previous","is_panel_display","shortcut_key_modal","shortcut_key_column_name","shortcut_key_column","shortcut_keys","key_name","IProgramDefault","detail","is_free","genre","video_type","video_codec","video_resolution","primary_audio_type","primary_audio_language","primary_audio_sampling_rate","secondary_audio_type","secondary_audio_language","secondary_audio_sampling_rate","IChannelDefault","transport_stream_id","channel_number","channel_name","channel_comment","is_subchannel","viewers","program_following","_l","getProgramTime","getProgramProgress","required","addMutedKeywords","addMutedNiconicoUserIDs","is_comment_list_dropdown_display","scopedSlots","active","displayCommentListDropdown","initialize_failed_message","is_manual_scroll","scrollCommentList","_setup","comment_mute_settings_modal","muted_comment_keyword","muted_comment_keyword_match_type","muted_niconico_user_id","prop","event","Boolean","interval_timer_id","map","keyword","user_id","setting_keys","setting_key","new_muted_comment_keywords","new_muted_niconico_user_ids","watch","deep","handler","setting_value","$emit","mute_vulgar_comments_pattern","mute_abusive_discriminatory_prejudiced_comments_pattern","CommentMuteSettings","is_auto_scrolling","user","comment_list","comment_list_element","comment_list_dropdown_top","comment_list_dropdown_comment","watch_session","comment_session","vpos_base_timestamp","keep_seat_interval_id","resize_observer","resize_observer_element","destroy","unobserve","new_channel","old_channel","$el","is_user_scrolling","onmousedown","x","clientX","clientWidth","onmouseup","on_user_scrolling","is_dragging","ontouchstart","ontouchend","ontouchmove","onwheel","onscroll","scrollTop","scrollHeight","initReserveObserver","comment_session_info","initWatchSession","unix","initCommentSession","message","watch_session_info","is_success","WebSocket","audience_token","addEventListener","send","messageServer","uri","threadId","vposBaseTime","yourPostKey","readyState","keepIntervalSec","code","waitTimeSec","disconnect_reason","onclose","reason","comment_list_buffer","is_received_initial_comment","message_server","thread_id","your_post_key","event_raw","thread","resultcode","ping","chat","yourpost","isMutedComment","color","size","mail","commands","split","command","getCommentColor","getCommentPosition","shift","comment_dict","no","date","my_post","visibilityState","paused","draw","onvisibilitychange","niconico_user_id","niconico_user_premium","color_table","position_table","vpos","onmessage","success","animation_timeout_id","on_resize","video_element","comment_area_element","clientHeight","letter_box_height","threshold","comment_area_vertical_margin","comment_area_width","comment_area_height","gcd","y","gcd_result","comment_area_height_aspect","transition","setProperty","clearTimeout","removeProperty","ResizeObserver","observe","smooth","scrollTo","top","behavior","startsWith","endsWith","currentTarget","$refs","comment_list_wrapper","_self","_setupProxy","$route","params","decorateProgramInfo","getAttribute","genre_index","major","middle","getChannelForceType","detail_text","detail_heading","URLtoLink","zoom_capture_modal","clickCapture","tweet_hashtag","is_tweet_hashtag_form_focused","is_hashtag_list_display","tweet_text","is_tweet_text_form_focused","is_logged_in_twitter","tweet_letter_count","twitter_account","hashtag","updateTweetLetterCount","formatHashtag","draggable","is_virtual_keyboard_display","is_logged_in","selected_twitter_account","is_twitter_account_list_display","editing","zoom_capture","captures","captures_element","tweet_captures","is_admin","niconico_user_name","twitter_accounts","created_at","updated_at","syncAccountInfo","some","twitter_account_index","image_url","old_channel_hashtag","getChannelHashtag","_a","response","status","clickAccountButton","fullscreenElement","exitFullscreen","$router","path","pasteClipboardData","clipboard_item","clipboardData","file","getAsFile","addCaptureList","updateSelectedTwitterAccount","selected","focused","$nextTick","context","font","fillStyle","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY","textAlign","textBaseline","fillText","from_hashtag_list","tweet_hashtag_array","channel_hashtag","join","tweet_capture","drawProgramTitleOnCapture","blur","screen_name","PLAYBACK_BUFFER_SEC_LOW_LATENCY","PLAYBACK_BUFFER_SEC","Channel","Comment","Program","Twitter","is_background_display","is_control_display","is_fullscreen","is_ime_composing","is_comment_send_just_did","control_interval_id","channel_next","romsounds_context","romsounds_buffers","eventsource","fullscreen_handler","capture_handler","shortcut_key_handler","shortcut_key_pressed_at","shortcut_key_list","left_column","icon","icon_height","shortcuts","right_column","virtualKeyboard","overlaysContent","ongeometrychange","target","boundingRect","init","AudioContext","url","audio_data","responseType","decodeAudioData","beforeRouteUpdate","to","next","getPreviousAndCurrentAndNextChannel","generatePlayerBackgroundURL","channel_response","channel_response_data","KonomiTVCanDestroy","initPlayer","initEventHandler","initCaptureHandler","removeEventListener","initShortcutKeyHandler","audioItem","mpegts","audioValue","textContent","tran","switchPrimaryAudio","delete","artwork","sizes","mediaSession","metadata","MediaMetadata","artist","setPositionState","playbackRate","setActionHandler","play","pause","is_player_event","is_touch_device","timeout","controller","hide","setting","isShow","show","playback_buffer_sec","DPlayer","lang","live","liveSyncMinBufferSize","loop","airplay","autoplay","hotkey","screenshot","volume","defaultQuality","qualities","hevc_prefix","isHEVCVideoSupported","speedRate","fontSize","apiBackend","read","sendComment","pluginOptions","enableWorker","liveSync","liveSyncMaxLatency","liveSyncTargetLatency","liveSyncPlaybackRate","aribb24","normalFont","forceStrokeColor","forceBackgroundColor","drcsReplacement","enableRawCanvas","useStrokeText","usePUA","PRACallback","state","resume","buffer_source_node","createBufferSource","gain_node","createGain","connect","destination","gain","start","subtitle","setAutoHide","commentInput","settingOriginPanel","settingOriginPanelHeight","settingBox","clipPath","fullscreen_container","fullScreen","isFullScreen","onfullscreenchange","webkitFullscreenElement","cancel","requestFullscreen","webkitRequestFullscreen","orientation","lock","catch","webkitExitFullscreen","unlock","on_play_or_pause","buffered","end","currentTime","sync","on_canplay","oncanplay","oncanplaythrough","get_playback_buffer_sec","round","current_playback_buffer_sec","current_volume","EventSource","clients_count","pictureInPictureElement","exitPictureInPicture","requestPictureInPicture","switchVideo","clear","twitter_component","tweet_form_element","tag","tagName","editable","is_repeat","repeat","focus","scrollLeft","ctrlKey","metaKey","shiftKey","altKey","switch_channel_type","switch_remocon_id","Number","switch_channel","getChannelFromRemoconID","focused_capture_index","focused_capture","focused_capture_element","parentElement","scrollIntoView","block","inline","toggle","pictureInPictureEnabled","pipButton","showDanmaku","showDanmakuToggle","checked","captureAndSave","is_destroy_player","exportSettings","import_settings_file","Base","is_form_dense","settings_json","settings_json_blob","$message","go","user_icon_blob","overrideServerSettingsFromClient","settings_username","settings_icon","$$v","settings_password_showing","settings_password","account_delete_confirm_dialog","settings_username_validation","settings_password_validation","sync_settings_dialog","sync_settings_json","server_sync_settings_json","syncAccountIcon","icon_response","update_type","validate","username","password","logout","$set","niconico_user_icon_url","user_id_slice","warning","authorization_url","popup_window","open","onMessage","closed","authorization_status","authorization_detail","loginTwitterAccount","current_twitter_account","password_showing","URLSearchParams","password_validation","username_validation","_m","VueRouter","router","mode","routes","redirect","TVHome","TVWatch","SettingsIndex","SettingsGeneral","SettingsAccount","SettingsJikkyo","SettingsTwitter","SettingsEnvironment","Login","Register","NotFound","scrollBehavior","savedPosition","ready","registered","cached","updatefound","updated","offline","SeamlessScrollPolyfill","VueAxios","VueVirtualScroller","VuetifyMessageSnackbar","bottom","elevation","autoRemove","closeButtonContent","vuetifyInstance","vuetify","trigger","VTooltip","Icon","App","$mount","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","loaded","__webpack_modules__","m","amdO","deferred","O","chunkIds","fn","priority","notFulfilled","Infinity","i","fulfilled","j","every","r","n","getter","__esModule","d","definition","o","defineProperty","enumerable","g","globalThis","Function","e","hmd","create","children","obj","hasOwnProperty","Symbol","toStringTag","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","chunkLoadingGlobal","self","forEach","bind","__webpack_exports__"],"sourceRoot":""} \ No newline at end of file diff --git a/client/dist/assets/js/app.66d563f0.js b/client/dist/assets/js/app.66d563f0.js new file mode 100644 index 00000000..72e7b170 --- /dev/null +++ b/client/dist/assets/js/app.66d563f0.js @@ -0,0 +1,2 @@ +(function(){"use strict";var t={4195:function(t,e,s){var i=s(2856),a=s(5742),n=s(144),o=s(2346),r=s(4801),l=s(1797),c=s.n(l),_=s(9652),d=s(998),m=function(){var t=this,e=t._self._c;return e(d.Z,{attrs:{id:"app"}},[e("transition",[e("router-view")],1)],1)},u=[],p=s(1001),h={},g=(0,p.Z)(h,m,u,!1,null,null,null),v=g.exports,f=s(2165),w=f.Z.extend({render(t){return t("transition",{props:{name:this.computedTransition},on:{beforeEnter:this.onBeforeTransition,afterEnter:this.onAfterTransition,enterCancelled:this.onTransitionCancelled,beforeLeave:this.onBeforeTransition,afterLeave:this.onAfterTransition,leaveCancelled:this.onTransitionCancelled,enter:this.onEnter}},[this.genWindowItem()])}}),y=s(5352),b=s(8481),C=s(530),x=C.Z.extend({data(){return{items:[]}},methods:{register(t){const e=this.items[this.internalIndex];this.items.push(t),this.items.sort(((t,e)=>{const s=this.$slots.default.findIndex((e=>t.$vnode.key===e.key)),i=this.$slots.default.findIndex((t=>e.$vnode.key===t.key));return s-i})),t.$on("change",(()=>this.onClick(t))),this.mandatory&&!this.selectedValues.length&&this.updateMandatory(),this.updateItem(t,this.items.indexOf(t)),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))},unregister(t){const e=this.items[this.internalIndex];this.constructor.super.options.methods.unregister.call(this,t),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))}}}),k=b.Z.extend({methods:{genBar(t,e){const s={style:{height:(0,y.kb)(this.height)},props:{activeClass:this.activeClass,centerActive:this.centerActive,dark:this.dark,light:this.light,mandatory:!this.optional,mobileBreakpoint:this.mobileBreakpoint,nextIcon:this.nextIcon,prevIcon:this.prevIcon,showArrows:this.showArrows,value:this.internalValue},on:{"call:slider":this.callSlider,change:t=>{this.internalValue=t}},ref:"items"};return this.setTextColor(this.computedColor,s),this.setBackgroundColor(this.backgroundColor,s),this.$createElement(x,s,[this.genSlider(e),t])}}}),O=s(5085),S=O.Z.extend({data(){return{items:[]}},methods:{register(t){const e=this.items[this.internalIndex];this.items.push(t),this.items.sort(((t,e)=>{const s=this.$slots.default.findIndex((e=>t.$vnode.key===e.key)),i=this.$slots.default.findIndex((t=>e.$vnode.key===t.key));return s-i})),t.$on("change",(()=>this.onClick(t))),this.mandatory&&!this.selectedValues.length&&this.updateMandatory(),this.updateItem(t,this.items.indexOf(t)),void 0!==e&&this.items.indexOf(e)!==this.internalValue&&this.updateInternalValue(this.items.indexOf(e))},unregister(t){const e=this.items[this.internalIndex];this.constructor.super.options.methods.unregister.call(this,t),void 0!==e&&this.updateInternalValue(this.items.indexOf(e))},updateReverse(t,e){const s=this.items.length,i=s-1;return s<=2?t":">"};return t.replace(/[&"'<>]/g,(t=>e[t]))}static getWindowFeatures(){const t=650,e=window.screen.height>=800?800:window.screen.height-100,s=(window.screen.height-e)/2,i=(window.screen.width-t)/2;return`toolbar=0,status=0,top=${s},left=${i},width=${t},height=${e},modal=yes,alwaysRaised=yes`}static hasActiveElementClass(t){return null!==document.activeElement&&document.activeElement.classList.contains(t)}static isSmartphoneHorizontal(){return window.matchMedia("(max-width: 1000px) and (max-height: 450px)").matches}static isSmartphoneVertical(){return window.matchMedia("(max-width: 600px) and (min-height: 450.01px)").matches}static isTabletHorizontal(){return window.matchMedia("(max-width: 1264px) and (max-height: 850px)").matches}static isTabletVertical(){return window.matchMedia("(max-width: 850px) and (min-height: 850.01px)").matches}static isTouchDevice(){return window.matchMedia("(hover: none)").matches}static mathFloor(t,e=0){return Math.floor(t*10**e)/10**e}static async sleep(t){return await new Promise((e=>setTimeout(e,1e3*t)))}static time(){return Date.now()/1e3}static typeof(t){return Object.prototype.toString.call(t).slice(8,-1).toLowerCase()}static URLtoLink(t){t=I.escapeHTML(t);const e=/(https?:\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/gi;return t.replace(e,'$1')}}I.version="0.6.1",I.api_base_url=(()=>`${window.location.protocol}//${window.location.host}/api`)(),I.default_settings={pinned_channel_ids:[],showed_panel_last_time:!0,selected_twitter_account_id:null,saved_twitter_hashtags:[],tv_streaming_quality:"1080p",tv_data_saver_mode:!1,tv_low_latency_mode:!0,tv_show_superimpose:!0,panel_display_state:"RestorePreviousState",tv_panel_active_tab:"Program",caption_font:"Windows TV MaruGothic",always_border_caption_text:!0,specify_caption_background_color:!1,caption_background_color:"#00000080",capture_copy_to_clipboard:!1,capture_save_mode:"Browser",capture_caption_mode:"Both",sync_settings:!1,comment_speed_rate:1,comment_font_size:34,comment_delay_time:1.75,close_comment_form_after_sending:!0,muted_comment_keywords:[],muted_niconico_user_ids:[],mute_vulgar_comments:!0,mute_abusive_discriminatory_prejudiced_comments:!0,mute_big_size_comments:!0,mute_fixed_comments:!1,mute_colored_comments:!1,mute_consecutive_same_characters_comments:!1,fold_panel_after_sending_tweet:!1,reset_hashtag_when_program_switches:!0,auto_add_watching_channel_hashtag:!0,twitter_active_tab:"Capture",tweet_hashtag_position:"Append",tweet_capture_watermark_position:"None"},I.sync_settings_keys=["pinned_channel_ids","saved_twitter_hashtags","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_save_mode","capture_caption_mode","comment_speed_rate","comment_font_size","close_comment_form_after_sending","muted_comment_keywords","muted_niconico_user_ids","mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments","fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position"],I.uploading_settings=!1;class T{static getChannelType(t,e=!1){const s=t.match("(?[a-z]+)[0-9]+").groups.channel_type.toUpperCase();if(!0!==e)return s;switch(s){case"GR":return"地デジ";case"STARDIGIO":return"StarDigio";default:return s}}static getChannelForceType(t){return null===t?"normal":t>=1e3?"festival":t>=200?"so-many":t>=100?"many":"normal"}static getChannelFromRemoconID(t,e,s){const i=e.replace("GR","地デジ").replace("STARDIGIO","StarDigio"),a=t.get(i);for(let n=0;n\n \n \n \n \n '),this.player_container.querySelector(".dplayer-icons.dplayer-icons-right").insertAdjacentHTML("afterbegin",'\n
\n \n \n \n
\n '),this.comment_capture_button=this.player_container.querySelector(".dplayer-comment-capture-icon"),this.capture_button=this.player_container.querySelector(".dplayer-capture-icon"),this.canvas="OffscreenCanvas"in window?new OffscreenCanvas(0,0):document.createElement("canvas"),this.canvas_context=this.canvas.getContext("2d",{alpha:!1,desynchronized:!0,willReadFrequently:!1}),this.canvas.width=0,this.canvas.height=0,t.on("loadedmetadata",(async()=>{this.canvas.width=t.video.videoWidth,this.canvas.height=t.video.videoHeight;while(0===this.canvas.width&&0===this.canvas.height)await I.sleep(.1),this.canvas.width=t.video.videoWidth,this.canvas.height=t.video.videoHeight}))}async captureAndSave(t,e){const s=I.time();if(!0===t.is_radiochannel)return void this.player.notice("ラジオチャンネルはキャプチャできません。");if(0===this.canvas.width&&0===this.canvas.height)return void this.player.notice("読み込み中はキャプチャできません。");if(!0===e&&!1===this.player.danmaku.showing)return void this.player.notice("コメントを付けてキャプチャするには、コメント表示をオンにしてください。");this.addHighlight(e);const i=`Capture_${A()().format("YYYYMMDD-HHmmss")}`,a=`${i}.jpg`,n=`${i}_caption.jpg`,o=this.player.plugins.aribb24Caption.getRawCanvas(),r=this.player.plugins.aribb24Superimpose.getRawCanvas(),l=!0===this.player.plugins.aribb24Caption.isShowing&&this.player.plugins.aribb24Caption.isPresent(),c=!0===this.player.plugins.aribb24Superimpose.isShowing&&this.player.plugins.aribb24Superimpose.isPresent(),_=l?this.player.plugins.aribb24Caption.getTextContent():null,d=async(t,e,s,i,a,n)=>{const o=I.time();let r;try{r=await this.exportToBlob(t)}catch(l){return this.player.notice("キャプチャの保存に失敗しました…"),!1}return console.log("[PlayerCaptureHandler] Export to Blob:",I.mathFloor(I.time()-o,3),"sec"),r=await this.setEXIFDataToCapture(r,s,i,a,n),["Browser","Both"].includes(I.getSettingsItem("capture_save_mode"))&&I.downloadBlobData(r,e),["UploadServer","Both"].includes(I.getSettingsItem("capture_save_mode"))&&this.uploadCaptureToServer(r,e),r};let m=null,u=null;const p=await createImageBitmap(this.player.video);if(!1!==e||!1!==c||!1!==l&&"VideoOnly"!==I.getSettingsItem("capture_caption_mode")){const s=[];this.canvas_context.drawImage(p,0,0,this.canvas.width,this.canvas.height),!0===c&&this.canvas_context.drawImage(r,0,0,this.canvas.width,this.canvas.height);let i=null;!0===e&&(i=await this.createCommentsImage(),await this.drawComments(i)),(["VideoOnly","Both"].includes(I.getSettingsItem("capture_caption_mode"))||!1===l)&&s.push((async()=>{const s="CompositingCaption"===I.getSettingsItem("capture_caption_mode")?n:a,i=await d(this.canvas,s,t.program_present,_,!1,e);m=!1!==i&&{blob:i,filename:s},!1!==m&&this.captured_callback(m.blob,m.filename)})()),["CompositingCaption","Both"].includes(I.getSettingsItem("capture_caption_mode"))&&!0===l&&s.push((async()=>{!0===e&&(this.canvas_context.drawImage(p,0,0,this.canvas.width,this.canvas.height),!0===c&&this.canvas_context.drawImage(r,0,0,this.canvas.width,this.canvas.height)),p.close(),this.canvas_context.drawImage(o,0,0,this.canvas.width,this.canvas.height),!0===e&&await this.drawComments(i);const s=await d(this.canvas,n,t.program_present,_,!0,e);if(u=!1!==s&&{blob:s,filename:n},!1!==u){if("Both"===I.getSettingsItem("capture_caption_mode"))while(null===m)await I.sleep(.01);this.captured_callback(u.blob,u.filename)}})()),await Promise.all(s)}else{const s="OffscreenCanvas"in window?new OffscreenCanvas(p.width,p.height):document.createElement("canvas");s.width=p.width,s.height=p.height;const i=s.getContext("bitmaprenderer",{alpha:!1});i.transferFromImageBitmap(p),p.close();const o="CompositingCaption"===I.getSettingsItem("capture_caption_mode")?n:a,r=await d(s,o,t.program_present,_,!1,e);m=!1!==r&&{blob:r,filename:o},!1!==m&&this.captured_callback(m.blob,m.filename)}console.log("[PlayerCaptureHandler] Total:",I.mathFloor(I.time()-s,3),"sec"),this.removeHighlight(e);for(const g of[m,u])if(I.getSettingsItem("capture_copy_to_clipboard")&&null!==g&&!1!==g)try{await(0,P.FH)(await(0,P.BD)(g.blob))}catch(h){this.player.notice("クリップボードへのキャプチャのコピーに失敗しました…"),console.error(h)}}addHighlight(t=!1){t?this.comment_capture_button.classList.add("dplayer-capturing"):this.capture_button.classList.add("dplayer-capturing")}removeHighlight(t=!1){t?this.comment_capture_button.classList.remove("dplayer-capturing"):this.capture_button.classList.remove("dplayer-capturing")}async commentsHTMLtoSVGImage(t,e,s){const i=`\n \n \n
\n \n ${t}\n
\n
\n
\n `.trim(),a=new Image;return a.src=`data:image/svg+xml;charset=utf-8,${encodeURIComponent(i)}`,await a.decode(),a}async createCommentsImage(){let t=this.player.template.danmaku.outerHTML;for(const e of this.player_container.querySelectorAll(".dplayer-danmaku-move")){const s=e.getBoundingClientRect().left-this.player.video.getBoundingClientRect().left;t=t.replace(/transform: translateX\(.*?\);/,`left: ${s}px;`).replaceAll("border: 2px solid #E64F97;","")}return await this.commentsHTMLtoSVGImage(t,this.player.template.danmaku.offsetWidth,this.player.template.danmaku.offsetHeight)}async drawComments(t){const e=this.canvas.width/this.player.template.danmaku.offsetWidth,s=this.player.template.danmaku.offsetHeight*e;this.canvas_context.drawImage(t,0,0,this.canvas.width,s)}async exportToBlob(t){return t instanceof OffscreenCanvas?await t.convertToBlob({type:"image/jpeg",quality:.99}):new Promise(((e,s)=>{t.toBlob((t=>{null!==t?e(t):s(new Error("Failed to convert canvas to blob"))}),"image/jpeg",.99)}))}async setEXIFDataToCapture(t,e,s,i,a){const n=A()().diff(A()(e.start_time),"second",!0),o={captured_at:A()().format("YYYY-MM-DDTHH:mm:ss+09:00"),captured_playback_position:n,network_id:e.network_id,service_id:e.service_id,event_id:e.event_id,title:e.title,description:e.description,start_time:e.start_time,end_time:e.end_time,duration:e.duration,caption_text:s,is_caption_composited:i,is_comment_composited:a},r=A()().format("YYYY:MM:DD HH:mm:ss"),l={"0th":{[$.TagValues.ImageIFD.XResolution]:[72,1],[$.TagValues.ImageIFD.YResolution]:[72,1],[$.TagValues.ImageIFD.ResolutionUnit]:2,[$.TagValues.ImageIFD.YCbCrPositioning]:1,[$.TagValues.ImageIFD.DateTime]:r,[$.TagValues.ImageIFD.Software]:`KonomiTV version ${I.version}`,[$.TagValues.ImageIFD.XPComment]:[...j.lW.from(JSON.stringify(o),"ucs2")]},Exif:{[$.TagValues.ExifIFD.ExifVersion]:"0230",[$.TagValues.ExifIFD.ComponentsConfiguration]:"\0",[$.TagValues.ExifIFD.FlashpixVersion]:"0100",[$.TagValues.ExifIFD.ColorSpace]:1,[$.TagValues.ExifIFD.DateTimeOriginal]:r,[$.TagValues.ExifIFD.DateTimeDigitized]:r}},c=$.dump(l),_=await new Promise(((e,s)=>{const i=new FileReader;i.onload=()=>e(i.result),i.onerror=s,i.readAsBinaryString(t)})),d=$.insert(c,_),m=new Uint8Array(d.length);for(let u=0;u$1').replace(n,'$1')}{A().extend(E()),A().extend(L()),A().extend(B());const t=A()(),s=A()().hour(0).minute(0).second(0),i=A()().hour(6).minute(59).second(59),a=A()().hour(23).minute(0).second(0),n=A()().hour(23).minute(59).second(59);return t.isSameOrAfter(s)&&t.isSameOrBefore(i)||t.isSameOrAfter(a)&&t.isSameOrBefore(n)?"title"===e?"放送休止":"この時間は放送を休止しています。":"title"===e?"番組情報がありません":"この時間の番組情報を取得できませんでした。"}}static getAttribute(t,e,s){return null!==t&&void 0!==t[e]&&null!==t[e]?t[e]:s}static getProgramProgress(t){if(null!==t){const e=A()(A()()).diff(t.start_time,"second");return e/t.duration*100}return 0}static getProgramTime(t,e=!1){if(null!==t&&"2000-01-01T00:00:00+09:00"!==t.start_time){A().locale("ja");const s=A()(t.start_time),i=A()(t.end_time),a=t.duration/60;return!0===e?`${s.format("HH:mm")} ~ ${i.format("HH:mm")}`:`${s.format("YYYY/MM/DD (dd) HH:mm")} ~ ${i.format("HH:mm")} (${a}分)`}return!0===e?"--:-- ~ --:--":"----/--/-- (-) --:-- ~ --:-- (--分)"}}var F=I,R=s(196);const M=R.ZP.create();M.interceptors.request.use((t=>{void 0===t.baseURL&&(t.baseURL=F.api_base_url);const e=F.getAccessToken();return null!==e&&(t.headers["Authorization"]=`Bearer ${e}`),t.headers["X-KonomiTV-Version"]=F.version,t}));var U=M,q=s(1858),G=s(9258),W=s(4562),Y=s(4324);n["default"].use(q.Z),n["default"].component("v-snackbar",G.Z),n["default"].component("v-btn",W.Z),n["default"].component("v-icon",Y.Z);var X=new q.Z({theme:{dark:!0,themes:{dark:{primary:"#E64F97",secondary:"#E33157",twitter:{base:"#4F82E6",lighten1:"#799FEC",lighten2:"#41A5F1"},gray:"#66514C",black:"#110A09",background:{base:"#1E1310",lighten1:"#2F221F",lighten2:"#433532",lighten3:"#4c3c38"},text:{base:"#FFEAEA",darken1:"#D9C7C7",darken2:"#8E7F7E",darken3:"#786968"}}},options:{customProperties:!0}}}),J=s(8345),Q=s(8718),tt=s(626),et=s(7069),st=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"channels-container channels-container--home",class:{"channels-container--loading":t.is_loading}},[e("v-tabs-fix",{staticClass:"channels-tab",attrs:{centered:""},model:{value:t.tab,callback:function(e){t.tab=e},expression:"tab"}},t._l(Array.from(t.channels_list),(function([s]){return e(tt.Z,{key:s,staticClass:"channels-tab__item"},[t._v(t._s(s))])})),1),e("v-tabs-items-fix",{staticClass:"channels-list",model:{value:t.tab,callback:function(e){t.tab=e},expression:"tab"}},t._l(Array.from(t.channels_list),(function([s,i]){return e("v-tab-item-fix",{key:s,staticClass:"channels-tabitem"},[e("div",{staticClass:"channels",class:`channels--tab-${s} channels--length-${i.length}`},[t._l(i,(function(s){return e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],key:s.id,staticClass:"channel",attrs:{to:`/tv/watch/${s.channel_id}`}},[e("div",{staticClass:"channel__broadcaster"},[e("img",{staticClass:"channel__broadcaster-icon",attrs:{src:`${t.Utils.api_base_url}/channels/${s.channel_id}/logo`}}),e("div",{staticClass:"channel__broadcaster-content"},[e("span",{staticClass:"channel__broadcaster-name"},[t._v("Ch: "+t._s(s.channel_number)+" "+t._s(s.channel_name))]),e("div",{staticClass:"channel__broadcaster-status"},[e("div",{staticClass:"channel__broadcaster-status-force",class:`channel__broadcaster-status-force--${t.ChannelUtils.getChannelForceType(s.channel_force)}`},[e("Icon",{attrs:{icon:"fa-solid:fire-alt",height:"12px"}}),e("span",{staticClass:"ml-1"},[t._v("勢い:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(t.ProgramUtils.getAttribute(s,"channel_force","--")))]),e("span",{staticStyle:{"margin-left":"3px"}},[t._v(" コメ/分")])],1),e("div",{staticClass:"channel__broadcaster-status-viewers ml-4"},[e("Icon",{attrs:{icon:"fa-solid:eye",height:"14px"}}),e("span",{staticClass:"ml-1"},[t._v("視聴数:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(s.viewers))])],1)])]),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip",value:t.isPinnedChannel(s.channel_id)?"ピン留めを外す":"ピン留めする",expression:"isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'"}],staticClass:"channel__broadcaster-pin",class:{"channel__broadcaster-pin--pinned":t.isPinnedChannel(s.channel_id)},on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.isPinnedChannel(s.channel_id)?t.removePinnedChannel(s.channel_id):t.addPinnedChannel(s.channel_id)},mousedown:function(t){t.preventDefault(),t.stopPropagation()}}},[e("Icon",{attrs:{icon:"fluent:pin-20-filled",width:"24px"}})],1)]),e("div",{staticClass:"channel__program-present"},[e("div",{staticClass:"channel__program-present-title-wrapper"},[e("span",{staticClass:"channel__program-present-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_present,"title"))}}),e("span",{staticClass:"channel__program-present-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(s.program_present)))])]),e("span",{staticClass:"channel__program-present-description",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_present,"description"))}})]),e(Q.Z),e("div",{staticClass:"channel__program-following"},[e("div",{staticClass:"channel__program-following-title"},[e("span",{staticClass:"channel__program-following-title-decorate"},[t._v("NEXT")]),e("Icon",{staticClass:"channel__program-following-title-icon",attrs:{icon:"fluent:fast-forward-20-filled",width:"16px"}}),e("span",{staticClass:"channel__program-following-title-text",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(s.program_following,"title"))}})],1),e("span",{staticClass:"channel__program-following-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(s.program_following)))])]),e("div",{staticClass:"channel__progressbar"},[e("div",{staticClass:"channel__progressbar-progress",style:`width:${t.ProgramUtils.getProgramProgress(s.program_present)}%;`})])],1)})),"ピン留め"===s&&0===i.length?e("div",{staticClass:"pinned-container d-flex justify-center align-center w-100"},[e("div",{staticClass:"d-flex justify-center align-center flex-column"},[e("h2",[t._v("ピン留めされているチャンネルがありません。")]),e("div",{staticClass:"mt-4 text--text text--darken-1"},[t._v("各チャンネルの "),e("Icon",{staticStyle:{position:"relative",bottom:"-5px"},attrs:{icon:"fluent:pin-20-filled",width:"22px"}}),t._v(" アイコンから、よくみるチャンネルをこのタブにピン留めできます。")],1),e("div",{staticClass:"mt-2 text--text text--darken-1"},[t._v("チャンネルをピン留めすると、このタブが最初に表示されます。")])])]):t._e()],2)])})),1)],1)],1)],1)},it=[],at=function(){var t=this,e=t._self._c;return e("header",{staticClass:"header"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"konomitv-logo ml-3 ml-md-6",attrs:{to:"/tv/"}},[e("img",{staticClass:"konomitv-logo__image",attrs:{src:"/assets/images/logo.svg",height:"21"}})]),e(Q.Z)],1)},nt=[],ot={},rt=(0,p.Z)(ot,at,nt,!1,null,"506af489",null),lt=rt.exports,ct=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"navigation-container elevation-8"},[e("nav",{staticClass:"navigation"},[e("div",{staticClass:"navigation-scroll"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/tv/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:tv-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("テレビをみる")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/videos/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:movies-and-tv-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("ビデオをみる")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/timetable/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:calendar-ltr-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("番組表")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/captures/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:image-multiple-24-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("キャプチャ")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/watchlists/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"ic:round-playlist-play",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("ウォッチリスト")])],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/histories/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:history-16-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("視聴履歴")])],1),e(Q.Z),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"navigation__link",attrs:{"active-class":"navigation__link--active",to:"/settings/"}},[e("Icon",{staticClass:"navigation__link-icon",attrs:{icon:"fluent:settings-20-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("設定")])],1),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.top",value:t.is_update_available?`アップデートがあります (version ${t.latest_version})`:"",expression:"is_update_available ? `アップデートがあります (version ${latest_version})` : ''",modifiers:{top:!0}}],staticClass:"navigation__link",class:{"navigation__link--version":t.Utils.version.includes("-dev")},attrs:{"active-class":"navigation__link--active",href:"https://github.com/tsukumijima/KonomiTV"}},[e("Icon",{staticClass:"navigation__link-icon",class:{"navigation__link-icon--highlight":t.is_update_available},attrs:{icon:"fluent:info-16-regular",width:"26px"}}),e("span",{staticClass:"navigation__link-text"},[t._v("version "+t._s(t.Utils.version))])],1)],1)])])},_t=[],dt=n["default"].extend({name:"Navigation",data(){return{Utils:F,latest_version:"",is_update_available:!1}},async created(){try{const t=(await n["default"].axios.get("/version")).data;this.latest_version=t.latest_version,(!1===t.version.includes("-dev")&&t.version!==t.latest_version||!0===t.version.includes("-dev")&&t.version.replace("-dev","")===t.latest_version)&&(this.is_update_available=!0)}catch(t){throw new Error(t)}}}),mt=dt,ut=(0,p.Z)(mt,ct,_t,!1,null,"3c027344",null),pt=ut.exports,ht=n["default"].extend({name:"TV-Home",components:{Header:lt,Navigation:pt},data(){return{Utils:F,ChannelUtils:T,ProgramUtils:V,tab:null,is_loading:!0,interval_ids:[],channels_list:new Map,pinned_channel_ids:[]}},created(){this.update();const t=60-Math.floor((new Date).getTime()/1e3)%60;this.interval_ids.push(window.setTimeout((()=>{this.update(),this.interval_ids.push(window.setInterval((()=>{this.update()}),3e4))}),1e3*t))},beforeDestroy(){for(const t of this.interval_ids)window.clearInterval(t)},methods:{async update(){let t;try{t=await n["default"].axios.get("/channels")}catch(s){return void console.error(s)}const e=t=>t.is_display;this.channels_list=new Map,t.data.GR.length>0&&this.channels_list.set("地デジ",t.data.GR.filter(e)),t.data.BS.length>0&&this.channels_list.set("BS",t.data.BS.filter(e)),t.data.CS.length>0&&this.channels_list.set("CS",t.data.CS.filter(e)),t.data.CATV.length>0&&this.channels_list.set("CATV",t.data.CATV.filter(e)),t.data.SKY.length>0&&this.channels_list.set("SKY",t.data.SKY.filter(e)),t.data.STARDIGIO.length>0&&this.channels_list.set("StarDigio",t.data.STARDIGIO.filter(e)),this.updatePinnedChannelList(!!this.is_loading),this.is_loading=!1},addPinnedChannel(t){this.pinned_channel_ids=F.getSettingsItem("pinned_channel_ids"),this.pinned_channel_ids.push(t),F.setSettingsItem("pinned_channel_ids",this.pinned_channel_ids),this.updatePinnedChannelList()},removePinnedChannel(t){this.pinned_channel_ids=F.getSettingsItem("pinned_channel_ids"),this.pinned_channel_ids.splice(this.pinned_channel_ids.indexOf(t),1),F.setSettingsItem("pinned_channel_ids",this.pinned_channel_ids),this.updatePinnedChannelList()},updatePinnedChannelList(t=!0){this.pinned_channel_ids=F.getSettingsItem("pinned_channel_ids");const e=[];for(const s of this.pinned_channel_ids){const t=T.getChannelType(s,!0),i=this.channels_list.get(t).find((t=>t.channel_id===s));void 0!==i&&e.push(i)}this.channels_list.has("ピン留め")?this.channels_list.set("ピン留め",e):this.channels_list=new Map([["ピン留め",e],...this.channels_list]),0===e.length&&!0===t&&(this.tab=1)},isPinnedChannel(t){return this.pinned_channel_ids.includes(t)}}}),gt=ht,vt=(0,p.Z)(gt,st,it,!1,null,"0185e37f",null),ft=vt.exports,wt=s(9582),yt=s(4886),bt=s(266),Ct=s(4061),xt=s(3305),kt=s(1713),Ot=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("main",{staticClass:"watch-container",class:{"watch-container--control-display":t.is_control_display,"watch-container--panel-display":t.is_panel_display,"watch-container--fullscreen":t.is_fullscreen}},[e("nav",{staticClass:"watch-navigation",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"watch-navigation__icon",attrs:{to:"/tv/"}},[e("img",{staticClass:"watch-navigation__icon-image",attrs:{src:"/assets/images/icon.svg",width:"23px"}})]),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"テレビをみる",expression:"'テレビをみる'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/tv/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:tv-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"ビデオをみる",expression:"'ビデオをみる'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/videos/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:movies-and-tv-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"番組表",expression:"'番組表'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/timetable/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:calendar-ltr-20-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"キャプチャ",expression:"'キャプチャ'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/captures/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:image-multiple-24-regular",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"ウォッチリスト",expression:"'ウォッチリスト'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/watchlists/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"ic:round-playlist-play",width:"26px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"視聴履歴",expression:"'視聴履歴'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/histories/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:history-16-regular",width:"26px"}})],1),e(Q.Z),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:"設定",expression:"'設定'",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",to:"/settings/"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:settings-20-regular",width:"26px"}})],1),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.right",value:`version ${t.Utils.version}`,expression:"`version ${Utils.version}`",modifiers:{right:!0}}],staticClass:"watch-navigation__link",attrs:{"active-class":"watch-navigation__link--active",href:"https://github.com/tsukumijima/KonomiTV"}},[e("Icon",{staticClass:"watch-navigation__link-icon",attrs:{icon:"fluent:info-16-regular",width:"26px"}})],1)],1),e("div",{staticClass:"watch-content",on:{mousemove:function(e){return t.controlDisplayTimer(e,!0)},touchmove:function(e){return t.controlDisplayTimer(e,!0)},click:function(e){return t.controlDisplayTimer(e,!0)}}},[e("header",{staticClass:"watch-header"},[e("img",{staticClass:"watch-header__broadcaster",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("span",{staticClass:"watch-header__program-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"title"))}}),e("span",{staticClass:"watch-header__program-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_present,!0)))]),e(Q.Z),e("span",{staticClass:"watch-header__now"},[t._v(t._s(t.time))])],1),e("div",{staticClass:"watch-player",class:{"watch-player--loading":t.is_loading,"watch-player--virtual-keyboard-display":t.is_virtual_keyboard_display&&t.Utils.hasActiveElementClass("dplayer-comment-input")}},[e("div",{staticClass:"watch-player__background",class:{"watch-player__background--display":t.is_background_display},style:{backgroundImage:`url(${t.background_url})`}},[e("img",{staticClass:"watch-player__background-logo",attrs:{src:"/assets/images/logo.svg"}})]),e(xt.Z,{staticClass:"watch-player__buffering",class:{"watch-player__buffering--display":t.is_video_buffering&&(t.is_loading||null!==t.player&&!t.player.video.paused)},attrs:{indeterminate:"",size:"60",width:"6"}}),e("div",{staticClass:"watch-player__dplayer"}),e("div",{staticClass:"watch-player__button",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.top",value:"前のチャンネル",expression:"'前のチャンネル'",modifiers:{top:!0}}],staticClass:"switch-button switch-button-up",attrs:{to:`/tv/watch/${t.channel_previous.channel_id}`}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:ios-arrow-left-24-filled",width:"32px",rotate:"1"}})],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"switch-button switch-button-panel switch-button-panel--open",on:{click:function(e){t.is_panel_display=!t.is_panel_display}}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:navigation-16-filled",width:"32px"}})],1),e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"},{name:"tooltip",rawName:"v-tooltip.bottom",value:"次のチャンネル",expression:"'次のチャンネル'",modifiers:{bottom:!0}}],staticClass:"switch-button switch-button-down",attrs:{to:`/tv/watch/${t.channel_next.channel_id}`}},[e("Icon",{staticClass:"switch-button-icon",attrs:{icon:"fluent:ios-arrow-right-24-filled",width:"33px",rotate:"1"}})],1)],1)],1)]),e("div",{staticClass:"watch-panel",on:{mousemove:function(e){return t.controlDisplayTimer(e)},touchmove:function(e){return t.controlDisplayTimer(e)},click:function(e){return t.controlDisplayTimer(e)}}},[e("div",{staticClass:"watch-panel__header"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-close-button",on:{click:function(e){t.is_panel_display=!1}}},[e("Icon",{staticClass:"panel-close-button__icon",attrs:{icon:"akar-icons:chevron-right",width:"25px"}}),e("span",{staticClass:"panel-close-button__text"},[t._v("閉じる")])],1),e(Q.Z),e("div",{staticClass:"panel-broadcaster"},[e("img",{staticClass:"panel-broadcaster__icon",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("div",{staticClass:"panel-broadcaster__number"},[t._v(t._s(t.channel.channel_number))]),e("div",{staticClass:"panel-broadcaster__name"},[t._v(t._s(t.channel.channel_name))])])],1),e("div",{staticClass:"watch-panel__content-container"},[e("Program",{staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Program"===t.tv_panel_active_tab},attrs:{channel:t.channel}}),e("Channel",{staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Channel"===t.tv_panel_active_tab},attrs:{channels_list:t.channels_list}}),e("Comment",{ref:"Comment",staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Comment"===t.tv_panel_active_tab},attrs:{channel:t.channel,player:t.player}}),e("Twitter",{ref:"Twitter",staticClass:"watch-panel__content",class:{"watch-panel__content--active":"Twitter"===t.tv_panel_active_tab},attrs:{channel:t.channel,player:t.player,is_virtual_keyboard_display:t.is_virtual_keyboard_display},on:{panel_folding_requested:function(e){t.is_panel_display=!1}}})],1),e("div",{staticClass:"watch-panel__navigation"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Program"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Program"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-solid:info-circle",width:"33px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("番組情報")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Channel"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Channel"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-solid:broadcast-tower",width:"34px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("チャンネル")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Comment"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Comment"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"bi:chat-left-text-fill",width:"29px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("コメント")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"panel-navigation-button",class:{"panel-navigation-button--active":"Twitter"===t.tv_panel_active_tab},on:{click:function(e){t.tv_panel_active_tab="Twitter"}}},[e("Icon",{staticClass:"panel-navigation-button__icon",attrs:{icon:"fa-brands:twitter",width:"34px"}}),e("span",{staticClass:"panel-navigation-button__text"},[t._v("Twitter")])],1)])])]),e(Ct.Z,{attrs:{"max-width":"1000",transition:"slide-y-transition"},model:{value:t.shortcut_key_modal,callback:function(e){t.shortcut_key_modal=e},expression:"shortcut_key_modal"}},[e(wt.Z,[e(yt.EB,{staticClass:"px-5 pt-4 pb-3 d-flex align-center font-weight-bold"},[e("Icon",{attrs:{icon:"fluent:keyboard-20-filled",height:"28px"}}),e("span",{staticClass:"ml-3"},[t._v("キーボードショートカット")]),e(Q.Z),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"d-flex align-center rounded-circle cursor-pointer px-2 py-2",on:{click:function(e){t.shortcut_key_modal=!1}}},[e("Icon",{attrs:{icon:"fluent:dismiss-12-filled",width:"23px",height:"23px"}})],1)],1),e("div",{staticClass:"px-5 pb-4"},[e(kt.Z,t._l(t.shortcut_key_list,(function(s,i){return e(bt.Z,{key:i,attrs:{cols:"6"}},t._l(s,(function(s){return e("div",{key:s.name,staticClass:"mt-3"},[e("div",{staticClass:"text-subtitle-1 d-flex align-center font-weight-bold"},[e("Icon",{attrs:{icon:s.icon,height:s.icon_height}}),e("span",{staticClass:"ml-2"},[t._v(t._s(s.name))])],1),t._l(s.shortcuts,(function(s){return e("div",{key:s.name,staticClass:"mt-3"},[e("div",{staticClass:"text-subtitle-2 mt-2 d-flex align-center font-weight-medium"},[e("span",{staticClass:"mr-2",domProps:{innerHTML:t._s(s.name)}}),e("div",{staticClass:"ml-auto d-flex align-center flex-shrink-0"},t._l(s.keys,(function(i,a){return e("div",{key:i.name,staticClass:"ml-auto d-flex align-center"},[e("span",{staticClass:"shortcut-key"},[t._l(i.name.split(";"),(function(t){return e("Icon",{directives:[{name:"show",rawName:"v-show",value:!0===i.icon,expression:"key.icon === true"}],key:t,attrs:{icon:t,height:"18px"}})})),!1===i.icon?e("span",{domProps:{innerHTML:t._s(i.name)}}):t._e()],2),a({id:Date.now()+e,match:t.match,pattern:t.pattern}))),muted_comment_keyword_match_type:[{text:"部分一致",value:"partial"},{text:"前方一致",value:"forward"},{text:"後方一致",value:"backward"},{text:"完全一致",value:"exact"},{text:"正規表現",value:"regex"}],muted_niconico_user_ids:F.getSettingsItem("muted_niconico_user_ids").map(((t,e)=>({id:Date.now()+e,user_id:t}))),settings:(()=>{const t={},e=["mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments"];for(const s of e)t[s]=F.getSettingsItem(s);return t})()}},created(){this.interval_timer_id=window.setInterval((()=>{const t=F.getSettingsItem("muted_comment_keywords");JSON.stringify(this.muted_comment_keywords)!==JSON.stringify(t)&&(this.muted_comment_keywords=t.map(((t,e)=>({id:Date.now()+e,match:t.match,pattern:t.pattern}))));const e=F.getSettingsItem("muted_niconico_user_ids");JSON.stringify(this.muted_niconico_user_ids.map((t=>t.user_id)))!==JSON.stringify(e)&&(this.muted_niconico_user_ids=e.map(((t,e)=>({id:Date.now()+e,user_id:t}))))}),1e3)},beforeDestroy(){window.clearInterval(this.interval_timer_id)},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))F.setSettingsItem(t,e)}},muted_comment_keywords:{deep:!0,handler(){F.setSettingsItem("muted_comment_keywords",this.muted_comment_keywords.map((t=>(delete t.id,t))))}},muted_niconico_user_ids:{deep:!0,handler(){F.setSettingsItem("muted_niconico_user_ids",this.muted_niconico_user_ids.map((t=>t.user_id)))}},showing(){this.comment_mute_settings_modal=this.showing},comment_mute_settings_modal(){this.$emit("change",this.comment_mute_settings_modal)}}}),Yt=Wt,Xt=(0,p.Z)(Yt,qt,Gt,!1,null,"7179f41e",null),Jt=Xt.exports;const Qt=new RegExp(j.lW.from("cHJwcnzvvZDvvZLvvZDvvZJ8U0VYfFPjgIdYfFPil69YfFPil4tYfFPil49YfO+8s++8pe+8uHzvvLPjgIfvvLh877yz4pev77y4fO+8s+KXi++8uHzvvLPil4/vvLh844Ki44OA44Or44OIfOOCouODiuOCpXzjgqLjg4rjg6t844Kk44Kr6IetfOOCpOOBj3zjgYbjgpPjgZN844Km44Oz44KzfOOBhuOCk+OBoXzjgqbjg7Pjg4F844Ko44Kt44ObfOOBiOOBoeOBiOOBoXzjgYjjgaPjgaF844Ko44OD44OBfOOBiOOBo+OCjXzjgqjjg4Pjg61844GI44KNfOOCqOODrXzlt6Xlj6N844GK44GV44KP44KK44G+44KTfOOBiuOBl+OBo+OBk3zjgqrjgrfjg4PjgrN844Kq44OD44K144OzfOOBiuOBo+OBseOBhHzjgqrjg4Pjg5HjgqR844Kq44OK44OL44O8fOOBiuOBquOBu3zjgqrjg4rjg5t844GK44Gx44GEfOOCquODkeOCpHzjgYpwfOOBiu+9kHzjgqrjg5Xjg5HjgrN844Ks44Kk44K444OzfOOCreODs+OCv+ODnnzjgY/jgbHjgYJ844GP44Gx44GBfOOCr+ODquODiOODquOCuXzjgq/jg7Pjg4t844GU44GP44GU44GP44GU44GP44GU44GPfOOCs+ODs+ODieODvOODoHzjgrbjg7zjg6Hjg7N844K344KzfOOBl+OBk+OBl+OBk3zjgrfjgrPjgrfjgrN844GZ44GR44GZ44GRfOOBm+OBhOOBiOOBjXzjgZnjgYXjgYXjgYXjgYXjgYV844GZ44GG44GG44GG44GG44GGfOOCu+OCr+ODreOCuXzjgrvjg4Pjgq/jgrl844K744OV44OsfOOBoeOBo+OBseOBhHzjgaHjgaPjg5HjgqR844OB44OD44OR44KkfOOBoeOCk+OBk3zjgaHjgIfjgZN844Gh4pev44GTfOOBoeKXi+OBk3zjgaHil4/jgZN844OB44Oz44KzfOODgeOAh+OCs3zjg4Hil6/jgrN844OB4peL44KzfOODgeKXj+OCs3zjgaHjgpPjgb1844Gh44CH44G9fOOBoeKXr+OBvXzjgaHil4vjgb1844Gh4peP44G9fOODgeODs+ODnXzjg4HjgIfjg51844OB4pev44OdfOODgeKXi+ODnXzjg4Hil4/jg51844Gh44KT44Gh44KTfOODgeODs+ODgeODs3zjgabjgYPjgpPjgabjgYPjgpN844OG44Kj44Oz44OG44Kj44OzfOODhuOCo+ODs+ODnXzjg4fjgqvjgYR844OH44Oq44OY44OrfOiEseOBknzjgbHjgYTjgoLjgb9844OR44OR5rS7fOOBteOBhuODu3zjgbXjgYbigKZ844G144GFfO++jO+9qXzjgbXjgY/jgonjgb/jgYvjgZF844G144GP44KJ44KT44GnfOOBuuOBo+OBn3zjgbrjgo3jgbrjgo1844Oa44Ot44Oa44OtfO++je++n+++m+++je++n+++m3zjg5Xjgqfjg6l844G844Gj44GNfOODneODq+ODjnzjgbzjgo3jgpN844Oc44Ot44OzfO++ju++nu++m+++nXzjgb3jgo3jgop844Od44Ot44OqfO++ju++n+++m+++mHzjg57jg7PjgY3jgaR844Oe44Oz44Kt44OEfOOBvuOCk+OBk3zjgb7jgIfjgZN844G+4pev44GTfOOBvuKXi+OBk3zjgb7il4/jgZN844Oe44Oz44KzfOODnuOAh+OCs3zjg57il6/jgrN844Oe4peL44KzfOODnuKXj+OCs3zjgb7jgpPjgZXjgpN844KC44Gj44GT44KKfOODouODg+OCs+ODqnzjgoLjgb/jgoLjgb9844Oi44Of44Oi44OffOODpOOBo+OBn3zjg6TjgaPjgaZ844Ok44KJfOOChOOCieOBm+OCjXzjg6Tjgop844Ok44KLfOODpOOCjHzjg6Tjgo1844Op44OW44ObfOODr+ODrOODoXzllph86Zmw5qC4fOmZsOiMjnzpmbDllId85rer5aSifOmZsOavm3znlKPjgoHjgot85aWz44Gu5a2Q44Gu5pelfOaxmuOBo+OBleOCk3zlp6Z86aiO5LmX5L2NfOmHkeeOiXzmnIjntYx85b6M6IOM5L2NfOWtkOS9nOOCinzlsITnsr585L+h6ICFfOeyvua2snzpgI/jgZF85oCn5LqkfOeyvuWtkHzmraPluLjkvY185oCn5b60fOaAp+eahHznlJ/nkIZ85a+45q2i44KBfOe0oOadkHzmirHjgYR85oqx44GLfOaKseOBjXzmirHjgY985oqx44GRfOaKseOBk3zkubPpppZ85oGl5Z6ifOS4reOBoOOBl3zkuK3lh7rjgZd85bC/fOaKnOOBhHzmipzjgZHjgarjgYR85oqc44GR44KLfOaKnOOBkeOCjHzohqjjgol85YuD6LW3fOaPieOBvnzmj4njgb985o+J44KAfOaPieOCgXzmvKvmuZZ844CH772efOKXr++9nnzil4vvvZ584peP772efOOAh+ODg+OCr+OCuXzil6/jg4Pjgq/jgrl84peL44OD44Kv44K5fOKXj+ODg+OCr+OCuQ==","base64").toString()),te=new RegExp(j.lW.from("44CCfOOCouOCueODmnzjgqTjgqvjgox844Kk44Op44Gk44GPfOOCpuOCuHzjgqbjg7zjg6h844Km44OofOOCpuODqOOCr3zjgqbjg7J844GN44KC44GEfOOCreODouOCpHzjgq3jg6LjgYR844KtL+ODoC/jg4F844Ks44Kk44K4fO+9tu++nu+9su+9vO++nnzjgqzjgq1844Kr44K5fOOCreODg+OCunzjgY3jgaHjgYzjgYR844Kt44OB44Ks44KkfOOCreODoOODgXzjg4Hjg6fjg7N85Y2D44On44OzfOOBpOOCk+OBvHzjg4Tjg7Pjg5x844ON44OI44Km44OofOOBq+OBoOOBguOBgnzjg4vjg4B8776G776A776efOODkeODvOODqHzjg5Hjg6h844OR44Oo44KvfOOBtuOBo+OBlXzjg5bjg4PjgrV844G244GV44GEfOODluOCteOCpHzjgb7jgazjgZF844Oh44Kv44OpfOODkOOCq3zjg6DjgqvjgaTjgY986bq755Sf44K744Oh44Oz44OIfOaFsOWuieWppnzlrrPlhZB85aSW5a2XfOWnpuWbvXzpn5Plm7186Z+T5LitfOmfk+aXpXzln7rlnLDlpJZ85rCX5oyB44Gh5oKqfOWbveS6pOaWree1tnzmrrp86aCD44GZfOWcqOaXpXzlj4LmlL/mqKl85q2744GtfOawj+OBrXzvvoDvvot85q255YyVfOatueODknzpmpzlrrN85pat5LqkfOS4remfk3zmnJ3prq585b6055So5belfOWjunzml6Xpn5N85pel5bidfOeymOedgHzlj43ml6V86aas6bm/fOeZuumBlHzmnLR85aOy5Zu9fOS4jeW/q3zplpPmipzjgZF86Z2W5Zu9","base64").toString());var ee=n["default"].extend({name:"Panel-CommentTab",components:{CommentMuteSettings:Jt},props:{channel:{type:Object,required:!0},player:{type:null,required:!0}},data(){return{Utils:F,is_manual_scroll:!1,is_auto_scrolling:!1,user:null,comment_list:[],comment_list_element:null,is_comment_list_dropdown_display:!1,comment_list_dropdown_top:0,comment_list_dropdown_comment:null,watch_session:null,comment_session:null,initialize_failed_message:null,vpos_base_timestamp:0,keep_seat_interval_id:0,resize_observer:null,resize_observer_element:null,comment_mute_settings_modal:!1}},beforeDestroy(){this.destroy(),null!==this.resize_observer&&this.resize_observer.unobserve(this.resize_observer_element)},watch:{async channel(t,e){if(t.channel_id!==e.channel_id){if("gr000"!==e.channel_id&&(await F.sleep(.5),this.channel.channel_id!==t.channel_id))return;this.destroy(),null===this.comment_list_element&&(this.comment_list_element=this.$el.querySelector(".comment-list"));let i=!1;this.comment_list_element.onmousedown=t=>{const e=t.clientX-this.comment_list_element.getBoundingClientRect().left;e>this.comment_list_element.clientWidth&&(i=!0)},this.comment_list_element.onmouseup=t=>{const e=t.clientX-this.comment_list_element.getBoundingClientRect().left;e>this.comment_list_element.clientWidth&&(i=!1)};const a=()=>{i=!0,window.setTimeout((()=>i=!1),100)};let o=!1;this.comment_list_element.ontouchstart=()=>o=!0,this.comment_list_element.ontouchend=()=>o=!1,this.comment_list_element.ontouchmove=()=>!0===o?a():"",this.comment_list_element.onwheel=a,this.comment_list_element.onscroll=async()=>{!1===this.is_auto_scrolling&&!0===i&&(this.is_manual_scroll=!0,await F.sleep(.1),this.comment_list_element.scrollTop+this.comment_list_element.offsetHeight>this.comment_list_element.scrollHeight-10&&(this.is_manual_scroll=!1))},await this.initReserveObserver();try{this.user=(await n["default"].axios.get("/users/me")).data}catch(s){this.user=null}try{const t=await this.initWatchSession();this.vpos_base_timestamp=100*A()(t["vpos_base_time"]).unix(),await this.initCommentSession(t)}catch(s){this.initialize_failed_message=s.message,console.error(s.toString())}}}},methods:{async initWatchSession(){let t;try{t=await n["default"].axios.get(`/channels/${this.channel.channel_id}/jikkyo`)}catch(e){throw new Error(e)}if(!1===t.data.is_success)throw"このチャンネルはニコニコ実況に対応していません。"!==t.data.detail&&"現在放送中のニコニコ実況がありません。"!==t.data.detail&&this.player.notice(t.data.detail),new Error(t.data.detail);return new Promise((e=>{this.watch_session=new WebSocket(t.data.audience_token),this.watch_session.addEventListener("open",(()=>{this.watch_session.send(JSON.stringify({type:"startWatching",data:{reconnect:!1}}))})),this.watch_session.addEventListener("message",(async t=>{const s=JSON.parse(t.data);switch(s.type){case"room":return e({message_server:s.data.messageServer.uri,thread_id:s.data.threadId,vpos_base_time:s.data.vposBaseTime,your_post_key:s.data.yourPostKey?s.data.yourPostKey:null});case"seat":this.keep_seat_interval_id=window.setInterval((()=>{1===this.watch_session.readyState?this.watch_session.send(JSON.stringify({type:"keepSeat"})):window.clearInterval(this.keep_seat_interval_id)}),1e3*s.data.keepIntervalSec);break;case"ping":this.watch_session.send(JSON.stringify({type:"pong"}));break;case"error":{let t;switch(s.data.code){case"CONNECT_ERROR":t="コメントサーバーに接続できません。";break;case"CONTENT_NOT_READY":t="ニコニコ実況が配信できない状態です。";break;case"NO_THREAD_AVAILABLE":t="コメントスレッドを取得できません。";break;case"NO_ROOM_AVAILABLE":t="コメント部屋を取得できません。";break;case"NO_PERMISSION":t="API にアクセスする権限がありません。";break;case"NOT_ON_AIR":t="ニコニコ実況が放送中ではありません。";break;case"BROADCAST_NOT_FOUND":t="ニコニコ実況の配信情報を取得できません。";break;case"INTERNAL_SERVERERROR":t="ニコニコ実況でサーバーエラーが発生しています。";break;default:t=`ニコニコ実況でエラーが発生しています。(${s.data.code})`;break}console.log(`error occurred. code: ${s.data.code}`),this.player.danmaku.showing&&this.player.notice(t);break}case"reconnect":{await F.sleep(s.data.waitTimeSec),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const t=await this.initWatchSession();await this.initCommentSession(t);break}case"disconnect":{let t;switch(this.watch_session&&(this.watch_session.onclose=null),s.data.reason){case"TAKEOVER":t="ニコニコ実況の番組から追い出されました。";break;case"NO_PERMISSION":t="ニコニコ実況の番組の座席を取得できませんでした。";break;case"END_PROGRAM":t="ニコニコ実況がリセットされたか、コミュニティの番組が終了しました。";break;case"PING_TIMEOUT":t="コメントサーバーとの接続生存確認に失敗しました。";break;case"TOO_MANY_CONNECTIONS":t="ニコニコ実況の同一ユーザからの接続数上限を越えています。";break;case"TOO_MANY_WATCHINGS":t="ニコニコ実況の同一ユーザからの視聴番組数上限を越えています。";break;case"CROWDED":t="ニコニコ実況の番組が満席です。";break;case"MAINTENANCE_IN":t="ニコニコ実況はメンテナンス中です。";break;case"SERVICE_TEMPORARILY_UNAVAILABLE":t="ニコニコ実況で一時的にサーバーエラーが発生しています。";break;default:t=`ニコニコ実況との接続が切断されました。(${s.data.reason})`;break}console.log(`disconnected. reason: ${s.data.reason}`),this.player.danmaku.showing&&this.player.notice(t),await F.sleep(5),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const e=await this.initWatchSession();await this.initCommentSession(e);break}}})),this.watch_session.onclose=async t=>{console.log(`disconnected. code: ${t.code}`),this.player.danmaku.showing&&this.player.notice(`ニコニコ実況との接続が切断されました。(code: ${t.code})`),await F.sleep(10),this.player.danmaku.showing&&this.player.notice("ニコニコ実況に再接続しています…"),this.destroy();const e=await this.initWatchSession();await this.initCommentSession(e)}}))},async initCommentSession(t){let e=[],s=!1;this.comment_session=new WebSocket(t.message_server),this.comment_session.addEventListener("open",(()=>{this.comment_session.send(JSON.stringify([{ping:{content:"rs:0"}},{ping:{content:"ps:0"}},{thread:{version:"20061206",thread:t.thread_id,threadkey:t.your_post_key,user_id:"",res_from:-50}},{ping:{content:"pf:0"}},{ping:{content:"rf:0"}}]))})),this.comment_session.addEventListener("message",(async t=>{const i=JSON.parse(t.data);if(void 0!==i.thread)if(0===i.thread.resultcode);else{const t="コメントサーバーに接続できませんでした。";console.error("Error: "+t)}void 0!==i.ping&&"rf:0"===i.ping.content&&(s=!0,this.scrollCommentList());const a=i.chat;if(void 0===a||void 0===a.content||a.content.match(/\/[a-z]+ /))return;if(a.yourpost&&1===a.yourpost)return;if(this.isMutedComment(a.content,a.user_id))return void console.log("Muted comment: "+a.content);let n="#FFEAEA",o="right",r="medium";if(void 0!==a.mail&&null!==a.mail){const t=a.mail.replace("184","").split(" ");for(const e of t)null!==this.getCommentColor(e)&&(n=this.getCommentColor(e)),null!==this.getCommentPosition(e)&&(o=this.getCommentPosition(e)),"big"!==e&&"medium"!==e&&"small"!==e||(r=e)}if(!0===F.getSettingsItem("mute_fixed_comments")&&("top"===o||"bottom"===o))return void console.log("Muted comment (Fixed): "+a.content);if(!0===F.getSettingsItem("mute_colored_comments")&&"#FFEAEA"!==n)return void console.log("Muted comment (Colored): "+a.content);if(!0===F.getSettingsItem("mute_big_size_comments")&&"big"===r)return void console.log("Muted comment (Big): "+a.content);if(s){const t=F.getSettingsItem("comment_delay_time");await F.sleep(t)}if(this.comment_list.length>=500&&!1===this.is_manual_scroll)while(this.comment_list.length>=500)this.comment_list.shift();const l={id:a.no,text:a.content,time:A()(1e3*a.date).format("HH:mm:ss"),user_id:a.user_id,my_post:!1};"hidden"!==document.visibilityState?(this.comment_list.push(l),s&&this.scrollCommentList(),s&&(this.player.video.paused||this.player.danmaku.draw({text:a.content,color:n,type:o,size:r}))):e.push(l)})),document.onvisibilitychange=()=>{"visible"===document.visibilityState&&(this.comment_list.push(...e),e=[],this.scrollCommentList())}},async sendComment(t){if(null!==this.initialize_failed_message)return void t.error(this.initialize_failed_message);if(null===this.user)return void t.error("コメントするには、KonomiTV アカウントにログインしてください。");if(null===this.user.niconico_user_id)return void t.error("コメントするには、ニコニコアカウントと連携してください。");if(!1===this.user.niconico_user_premium&&("top"===t.data.type||"bottom"===t.data.type))return void t.error("コメントを上下に固定するには、ニコニコアカウントのプレミアム会員登録が必要です。");if(!1===this.user.niconico_user_premium&&"big"===t.data.size)return void t.error("コメントサイズを大きめに設定するには、ニコニコアカウントのプレミアム会員登録が必要です。");const e={"#FFEAEA":"white","#F02840":"red","#FD7E80":"pink","#FDA708":"orange","#FFE133":"yellow","#64DD17":"green","#00D4F5":"cyan","#4763FF":"blue"},s={top:"ue",right:"naka",bottom:"shita"},i=Math.floor((new Date).getTime()/10)-this.vpos_base_timestamp;this.watch_session.send(JSON.stringify({type:"postComment",data:{text:t.data.text,color:e[t.data.color.toUpperCase()],position:s[t.data.type],size:t.data.size,vpos:i,isAnonymous:!0}})),this.comment_list.push({id:(new Date).getTime(),text:t.data.text,time:A()().format("HH:mm:ss"),user_id:`${this.user.niconico_user_id}`,my_post:!0}),this.watch_session.onmessage=e=>{const s=JSON.parse(e.data);switch(s.type){case"postCommentResult":t.success(),this.watch_session.onmessage=null;break;case"error":{let e=`コメントの送信に失敗しました。(${s.data.code})`;switch(s.data.code){case"COMMENT_POST_NOT_ALLOWED":e="コメントが許可されていません。";break;case"INVALID_MESSAGE":e="コメント内容が無効です。";break}t.error(e),this.watch_session.onmessage=null;break}}}},async initReserveObserver(){this.resize_observer_element=document.querySelector(".watch-player");let t=null;const e=()=>{const e=document.querySelector(".dplayer-video-wrap-aspect"),s=document.querySelector(".dplayer-danmaku");if(null===this.resize_observer_element||null===this.resize_observer_element.clientHeight)return;if(null===e||null===e.clientHeight)return;const i=(this.resize_observer_element.clientHeight-e.clientHeight)/2,a=window.matchMedia("(max-height: 450px)").matches?50:66;if(i0===e?t:l(e,t%e),c=l(o,r),_=`${o/c} / ${r/c}`;s.style.transition="none",s.style.setProperty("--comment-area-aspect-ratio",_),s.style.setProperty("--comment-area-vertical-margin",`${n}px`),window.clearTimeout(t),window.setTimeout((()=>{s.style.transition=""}),200)}else s.style.removeProperty("--comment-area-aspect-ratio"),s.style.removeProperty("--comment-area-vertical-margin")};this.resize_observer=new ResizeObserver(e),this.resize_observer.observe(this.resize_observer_element),window.setTimeout(e,600)},async scrollCommentList(t=!1){if(!0!==this.is_manual_scroll){this.is_auto_scrolling=!0;for(let e=0;e<3;e++)await F.sleep(.01),!0===t?this.comment_list_element.scrollTo({top:this.comment_list_element.scrollHeight,left:0,behavior:"smooth"}):this.comment_list_element.scrollTo(0,this.comment_list_element.scrollHeight);await F.sleep(.1),this.is_auto_scrolling=!1}},getCommentColor(t){const e={white:"#FFEAEA",red:"#F02840",pink:"#FD7E80",orange:"#FDA708",yellow:"#FFE133",green:"#64DD17",cyan:"#00D4F5",blue:"#4763FF",purple:"#D500F9",black:"#1E1310",white2:"#CCCC99",niconicowhite:"#CCCC99",red2:"#CC0033",truered:"#CC0033",pink2:"#FF33CC",orange2:"#FF6600",passionorange:"#FF6600",yellow2:"#999900",madyellow:"#999900",green2:"#00CC66",elementalgreen:"#00CC66",cyan2:"#00CCCC",blue2:"#3399FF",marineblue:"#3399FF",purple2:"#6633CC",nobleviolet:"#6633CC",black2:"#666666"};return void 0!==e[t]?e[t]:null},getCommentPosition(t){switch(t){case"ue":return"top";case"naka":return"right";case"shita":return"bottom";default:return null}},isMutedComment(t,e){const s=F.getSettingsItem("muted_comment_keywords");for(const a of s)if(""!==a.pattern)switch(a.match){case"partial":if(t.includes(a.pattern))return!0;break;case"forward":if(t.startsWith(a.pattern))return!0;break;case"backward":if(t.endsWith(a.pattern))return!0;break;case"exact":if(t===a.pattern)return!0;break;case"regex":if(new RegExp(a.pattern).test(t))return!0;break}if(!0===F.getSettingsItem("mute_vulgar_comments")&&Qt.test(t))return!0;if(!0===F.getSettingsItem("mute_abusive_discriminatory_prejudiced_comments")&&te.test(t))return!0;if(!0===F.getSettingsItem("mute_consecutive_same_characters_comments")&&/(.)\1{7,}/.test(t))return!0;if(/最高\d+米\/|計\d+ID|総\d+米/.test(t))return!0;const i=F.getSettingsItem("muted_niconico_user_ids");for(const a of i)if(e===a)return!0;return!1},addMutedKeywords(t){const e=F.getSettingsItem("muted_comment_keywords");e.push({match:"exact",pattern:t}),F.setSettingsItem("muted_comment_keywords",e)},addMutedNiconicoUserIDs(t){const e=F.getSettingsItem("muted_niconico_user_ids");e.push(t),F.setSettingsItem("muted_niconico_user_ids",e)},displayCommentListDropdown(t,e){const s=this.$refs.comment_list_wrapper.getBoundingClientRect(),i=76,a=t.currentTarget.getBoundingClientRect();this.comment_list_dropdown_top=a.top-s.top,this.comment_list_dropdown_top+i>s.height&&(this.comment_list_dropdown_top=this.comment_list_dropdown_top-i+a.height),this.comment_list_dropdown_comment=e,this.is_comment_list_dropdown_display=!0},destroy(){this.initialize_failed_message=null,this.comment_list=[],document.onvisibilitychange=null,null!==this.watch_session&&(this.watch_session.onclose=null,this.watch_session.close(),this.watch_session=null),null!==this.comment_session&&(this.comment_session.onclose=null,this.comment_session.close(),this.comment_session=null),window.clearInterval(this.keep_seat_interval_id)}}}),se=ee,ie=(0,p.Z)(se,Vt,Ft,!1,null,"23e84f03",null),ae=ie.exports,ne=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"program-container"},[e("section",{staticClass:"program-broadcaster"},[e("img",{staticClass:"program-broadcaster__icon",attrs:{src:`${t.Utils.api_base_url}/channels/${t.$route.params.channel_id}/logo`}}),e("div",{staticClass:"program-broadcaster__number"},[t._v("Ch: "+t._s(t.channel.channel_number))]),e("div",{staticClass:"program-broadcaster__name"},[t._v(t._s(t.channel.channel_name))])]),e("section",{staticClass:"program-info"},[e("h1",{staticClass:"program-info__title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"title"))}}),e("div",{staticClass:"program-info__time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_present)))]),e("div",{staticClass:"program-info__description",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_present,"description"))}}),e("div",{staticClass:"program-info__genre-container"},t._l(t.ProgramUtils.getAttribute(t.channel.program_present,"genre",[]),(function(s,i){return e("div",{key:i,staticClass:"program-info__genre"},[t._v(" "+t._s(s.major)+" / "+t._s(s.middle)+" ")])})),0),e("div",{staticClass:"program-info__next"},[e("span",{staticClass:"program-info__next-decorate"},[t._v("NEXT")]),e("Icon",{staticClass:"program-info__next-icon",attrs:{icon:"fluent:fast-forward-20-filled",width:"16px"}})],1),e("span",{staticClass:"program-info__next-title",domProps:{innerHTML:t._s(t.ProgramUtils.decorateProgramInfo(t.channel.program_following,"title"))}}),e("div",{staticClass:"program-info__next-time"},[t._v(t._s(t.ProgramUtils.getProgramTime(t.channel.program_following)))]),e("div",{staticClass:"program-info__status"},[e("div",{staticClass:"program-info__status-force",class:`program-info__status-force--${t.ChannelUtils.getChannelForceType(t.channel.channel_force)}`},[e("Icon",{attrs:{icon:"fa-solid:fire-alt",height:"14px"}}),e("span",{staticClass:"ml-2"},[t._v("勢い:")]),e("span",{staticClass:"ml-2"},[t._v(t._s(t.ProgramUtils.getAttribute(t.channel,"channel_force","--"))+" コメ/分")])],1),e("div",{staticClass:"program-info__status-viewers ml-5"},[e("Icon",{attrs:{icon:"fa-solid:eye",height:"14px"}}),e("span",{staticClass:"ml-2"},[t._v("視聴数:")]),e("span",{staticClass:"ml-1"},[t._v(t._s(t.channel.viewers))])],1)])]),e("section",{staticClass:"program-detail-container"},t._l(t.ProgramUtils.getAttribute(t.channel.program_present,"detail",{}),(function(s,i){return e("div",{key:i,staticClass:"program-detail"},[e("h2",{staticClass:"program-detail__heading"},[t._v(t._s(i))]),e("div",{staticClass:"program-detail__text",domProps:{innerHTML:t._s(t.Utils.URLtoLink(s))}})])})),0)])},oe=[],re=n["default"].extend({name:"Panel-ProgramTab",props:{channel:{type:Object,required:!0}},data(){return{Utils:F,ChannelUtils:T,ProgramUtils:V}}}),le=re,ce=(0,p.Z)(le,ne,oe,!1,null,"3c7f1e0c",null),_e=ce.exports,de=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"twitter-container"},[e(Ct.Z,{attrs:{"content-class":"zoom-capture-modal-container","max-width":"980",transition:"slide-y-transition"},model:{value:t.zoom_capture_modal,callback:function(e){t.zoom_capture_modal=e},expression:"zoom_capture_modal"}},[e("div",{staticClass:"zoom-capture-modal"},[e("img",{staticClass:"zoom-capture-modal__image",attrs:{src:t.zoom_capture?t.zoom_capture.image_url:""}}),e("a",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"zoom-capture-modal__download",attrs:{href:t.zoom_capture?t.zoom_capture.image_url:"",download:t.zoom_capture?t.zoom_capture.filename:""}},[e("Icon",{attrs:{icon:"fa6-solid:download",width:"45px"}})],1)])]),e("div",{staticClass:"tab-container"},[e("div",{staticClass:"tab-content",class:{"tab-content--active":"Capture"===t.twitter_active_tab}},[e("div",{staticClass:"captures"},t._l(t.captures,(function(s){return e("div",{key:s.image_url,staticClass:"capture",class:{"capture--selected":s.selected,"capture--focused":s.focused,"capture--disabled":!s.selected&&t.tweet_captures.length>=4},on:{click:function(e){return t.clickCapture(s)}}},[e("img",{staticClass:"capture__image",attrs:{src:s.image_url}}),e("div",{staticClass:"capture__disabled-cover"}),e("div",{staticClass:"capture__selected-number"},[t._v(t._s(t.tweet_captures.findIndex((t=>t===s.blob))+1))]),e("Icon",{staticClass:"capture__selected-checkmark",attrs:{icon:"fluent:checkmark-circle-16-filled"}}),e("div",{staticClass:"capture__selected-border"}),e("div",{staticClass:"capture__focused-border"}),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"capture__zoom",on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.zoom_capture_modal=!0,t.zoom_capture=s},mousedown:function(t){t.preventDefault(),t.stopPropagation()}}},[e("Icon",{attrs:{icon:"fluent:zoom-in-16-regular",width:"32px"}})],1)],1)})),0),e("div",{directives:[{name:"show",rawName:"v-show",value:0===t.captures.length,expression:"captures.length === 0"}],staticClass:"capture-announce"},[e("div",{staticClass:"capture-announce__heading"},[t._v("まだキャプチャがありません。")]),t._m(0)])])]),e("div",{staticClass:"tab-button-container"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Search"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Search"}}},[e("Icon",{attrs:{icon:"fluent:search-16-filled",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("ツイート検索")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Timeline"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Timeline"}}},[e("Icon",{attrs:{icon:"fluent:home-16-regular",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("タイムライン")])],1),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tab-button",class:{"tab-button--active":"Capture"===t.twitter_active_tab},on:{click:function(e){t.twitter_active_tab="Capture"}}},[e("Icon",{attrs:{icon:"fluent:image-copy-20-regular",height:"18px"}}),e("span",{staticClass:"tab-button__text"},[t._v("キャプチャ")])],1)]),e("div",{staticClass:"tweet-form",class:{"tweet-form--focused":t.is_tweet_hashtag_form_focused||t.is_tweet_text_form_focused,"tweet-form--virtual-keyboard-display":t.is_virtual_keyboard_display&&(t.Utils.hasActiveElementClass("tweet-form__hashtag-form")||t.Utils.hasActiveElementClass("tweet-form__textarea"))&&(()=>(t.is_hashtag_list_display=!1,!0))()}},[e("div",{staticClass:"tweet-form__hashtag"},[e("input",{directives:[{name:"model",rawName:"v-model",value:t.tweet_hashtag,expression:"tweet_hashtag"}],staticClass:"tweet-form__hashtag-form",attrs:{type:"search",placeholder:"#ハッシュタグ"},domProps:{value:t.tweet_hashtag},on:{input:[function(e){e.target.composing||(t.tweet_hashtag=e.target.value)},function(e){return t.updateTweetLetterCount()}],focus:function(e){t.is_tweet_hashtag_form_focused=!0},blur:function(e){t.is_tweet_hashtag_form_focused=!1},change:function(e){t.tweet_hashtag=t.formatHashtag(t.tweet_hashtag)}}}),e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tweet-form__hashtag-list-button",on:{click:function(e){t.is_hashtag_list_display=!t.is_hashtag_list_display}}},[e("Icon",{attrs:{icon:"fluent:clipboard-text-ltr-32-regular",height:"22px"}})],1)]),e("textarea",{directives:[{name:"model",rawName:"v-model",value:t.tweet_text,expression:"tweet_text"}],ref:"tweet_text",staticClass:"tweet-form__textarea",attrs:{placeholder:"ツイート"},domProps:{value:t.tweet_text},on:{input:[function(e){e.target.composing||(t.tweet_text=e.target.value)},function(e){return t.updateTweetLetterCount()}],paste:function(e){return t.pasteClipboardData(e)},focus:function(e){t.is_tweet_text_form_focused=!0},blur:function(e){t.is_tweet_text_form_focused=!1}}}),e("div",{staticClass:"tweet-form__control"},[e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"account-button",class:{"account-button--no-login":!t.is_logged_in_twitter},on:{click:function(e){return t.clickAccountButton()}}},[e("img",{staticClass:"account-button__icon",attrs:{src:t.is_logged_in_twitter?t.selected_twitter_account.icon_url:"/assets/images/account-icon-default.png"}}),e("span",{staticClass:"account-button__screen-name"},[t._v(" "+t._s(t.is_logged_in_twitter?`@${t.selected_twitter_account.screen_name}`:"連携されていません")+" ")]),e("Icon",{staticClass:"account-button__menu",attrs:{icon:"fluent:more-circle-20-regular",width:"22px"}})],1),e("div",{staticClass:"limit-meter"},[e("div",{staticClass:"limit-meter__content",class:{"limit-meter__content--yellow":t.tweet_letter_count<=20,"limit-meter__content--red":t.tweet_letter_count<=0}},[e("Icon",{staticStyle:{"margin-right":"-2px"},attrs:{icon:"fa-brands:twitter",width:"12px"}}),e("span",[t._v(t._s(t.tweet_letter_count))])],1),e("div",{staticClass:"limit-meter__content"},[e("Icon",{attrs:{icon:"fluent:image-16-filled",width:"14px"}}),e("span",[t._v(t._s(t.tweet_captures.length)+"/4")])],1)]),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"tweet-button",attrs:{disabled:!t.is_logged_in_twitter||t.tweet_letter_count<0||140===t.tweet_letter_count&&0===t.tweet_captures.length},on:{click:function(e){return t.sendTweet()}}},[e("Icon",{attrs:{icon:"fa-brands:twitter",height:"16px"}}),e("span",{staticClass:"ml-1"},[t._v("ツイート")])],1)])]),e("div",{staticClass:"twitter-account-list",class:{"twitter-account-list--display":t.is_twitter_account_list_display}},t._l(t.user.twitter_accounts,(function(s){return e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],key:s.id,staticClass:"twitter-account",on:{click:function(e){return t.updateSelectedTwitterAccount(s)}}},[e("img",{staticClass:"twitter-account__icon",attrs:{src:s.icon_url}}),e("div",{staticClass:"twitter-account__info"},[e("div",{staticClass:"twitter-account__name"},[t._v(t._s(s.name))]),e("div",{staticClass:"twitter-account__screen-name"},[t._v("@"+t._s(s.screen_name))])]),e("Icon",{directives:[{name:"show",rawName:"v-show",value:s.id===t.selected_twitter_account_id,expression:"twitter_account.id === selected_twitter_account_id"}],staticClass:"twitter-account__check",attrs:{icon:"fluent:checkmark-16-filled",width:"24px"}})],1)})),0),e("div",{staticClass:"hashtag-list",class:{"hashtag-list--display":t.is_hashtag_list_display,"hashtag-list--virtual-keyboard-display":t.is_virtual_keyboard_display&&t.Utils.hasActiveElementClass("hashtag__input")}},[e("div",{staticClass:"hashtag-heading"},[e("div",{staticClass:"hashtag-heading__text"},[e("Icon",{attrs:{icon:"charm:hash",width:"17px"}}),e("span",{staticClass:"ml-1"},[t._v("ハッシュタグリスト")])],1),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag-heading__add-button",on:{click:function(e){t.saved_twitter_hashtags.push({id:Date.now(),text:"#ここにハッシュタグを入力",editing:!1})}}},[e("Icon",{attrs:{icon:"fluent:add-12-filled",width:"17px"}}),e("span",{staticClass:"ml-1"},[t._v("追加")])],1)]),e("draggable",{staticClass:"hashtag-container",attrs:{handle:".hashtag__sort-handle"},model:{value:t.saved_twitter_hashtags,callback:function(e){t.saved_twitter_hashtags=e},expression:"saved_twitter_hashtags"}},t._l(t.saved_twitter_hashtags,(function(s){return e("div",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple",value:!s.editing,expression:"!hashtag.editing"}],key:s.id,staticClass:"hashtag",class:{"hashtag--editing":s.editing},on:{click:function(e){t.tweet_hashtag=s.text,t.tweet_hashtag=t.formatHashtag(t.tweet_hashtag),t.updateTweetLetterCount(),t.window.setTimeout((()=>t.is_hashtag_list_display=!1),150)}}},[e("input",{directives:[{name:"model",rawName:"v-model",value:s.text,expression:"hashtag.text"}],staticClass:"hashtag__input",attrs:{type:"search",disabled:!s.editing},domProps:{value:s.text},on:{click:function(t){t.stopPropagation()},input:function(e){e.target.composing||t.$set(s,"text",e.target.value)}}}),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag__edit-button",on:{click:function(e){e.preventDefault(),e.stopPropagation(),s.editing=!s.editing,s.text=t.formatHashtag(s.text,!0)}}},[e("Icon",{attrs:{icon:s.editing?"fluent:checkmark-16-filled":"fluent:edit-16-filled",width:"17px"}})],1),e("button",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"hashtag__delete-button",on:{click:function(e){e.preventDefault(),e.stopPropagation(),t.saved_twitter_hashtags.splice(t.saved_twitter_hashtags.indexOf(s),1)}}},[e("Icon",{attrs:{icon:"fluent:delete-16-filled",width:"17px"}})],1),e("div",{staticClass:"hashtag__sort-handle"},[e("Icon",{attrs:{icon:"material-symbols:drag-handle-rounded",width:"17px"}})],1)])})),0)],1)],1)},me=[function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"capture-announce__text"},[e("p",{staticClass:"mt-0 mb-0"},[t._v("プレイヤーのキャプチャボタンやショートカットキーでキャプチャを撮ると、ここに表示されます。")]),e("p",{staticClass:"mt-2 mb-0"},[t._v("表示されたキャプチャを選択してからツイートすると、キャプチャを付けてツイートできます。")])])}],ue=s(9980),pe=s.n(ue),he=n["default"].extend({name:"Panel-TwitterTab",components:{draggable:pe()},props:{channel:{type:Object,required:!0},player:{type:null,required:!0},is_virtual_keyboard_display:{type:Boolean,required:!0}},data(){return{Utils:F,window:window,is_logged_in:null!==F.getAccessToken(),is_logged_in_twitter:!1,user:null,selected_twitter_account:null,selected_twitter_account_id:F.getSettingsItem("selected_twitter_account_id"),is_twitter_account_list_display:!1,saved_twitter_hashtags:F.getSettingsItem("saved_twitter_hashtags").map(((t,e)=>({id:Date.now()+e,text:t,editing:!1}))),is_hashtag_list_display:!1,twitter_active_tab:F.getSettingsItem("twitter_active_tab"),zoom_capture_modal:!1,zoom_capture:null,captures:[],captures_element:null,is_tweet_hashtag_form_focused:!1,is_tweet_text_form_focused:!1,tweet_hashtag:"",tweet_text:"",tweet_captures:[],tweet_letter_count:140}},async created(){if(this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&(await this.syncAccountInfo(),this.user.twitter_accounts.length>0)){this.is_logged_in_twitter=!0,null!==this.selected_twitter_account_id&&this.user.twitter_accounts.some((t=>t.id===this.selected_twitter_account_id))||(this.selected_twitter_account_id=this.user.twitter_accounts[0].id,F.setSettingsItem("selected_twitter_account_id",this.selected_twitter_account_id));const t=this.user.twitter_accounts.findIndex((t=>t.id===this.selected_twitter_account_id));this.selected_twitter_account=this.user.twitter_accounts[t]}this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag)},beforeDestroy(){for(const t of this.captures)URL.revokeObjectURL(t.image_url)},watch:{async channel(t,e){var s;if(t.channel_id!==e.channel_id){const t=null!==(s=this.getChannelHashtag(e.channel_name))&&void 0!==s?s:"";this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag.replaceAll(t,""))}},saved_twitter_hashtags:{deep:!0,handler(){F.setSettingsItem("saved_twitter_hashtags",this.saved_twitter_hashtags.map((t=>t.text)))}}},methods:{async syncAccountInfo(){try{this.user=(await n["default"].axios.get("/users/me")).data}catch(t){R.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},updateTweetLetterCount(){this.tweet_letter_count=140-[...this.tweet_hashtag].length-[...this.tweet_text].length},clickAccountButton(){if(!this.is_logged_in_twitter)return document.fullscreenElement&&document.exitFullscreen(),void this.$router.push({path:"/settings/twitter"});this.is_twitter_account_list_display=!this.is_twitter_account_list_display,!0===this.is_twitter_account_list_display&&(this.is_hashtag_list_display=!1)},pasteClipboardData(t){for(const e of t.clipboardData.items)if(e.type.startsWith("image/")){const t=e.getAsFile();this.addCaptureList(t,t.name)}},updateSelectedTwitterAccount(t){this.selected_twitter_account_id=t.id,F.setSettingsItem("selected_twitter_account_id",this.selected_twitter_account_id),this.selected_twitter_account=t,window.setTimeout((()=>this.is_twitter_account_list_display=!1),150)},clickCapture(t){if(this.tweet_captures.length<4&&!1===t.selected)t.selected=!0,this.tweet_captures.push(t.blob);else{const e=this.tweet_captures.findIndex((e=>e===t.blob));e>-1&&this.tweet_captures.splice(e,1),t.selected=!1}},async addCaptureList(t,e){null===this.captures_element&&(this.captures_element=this.$el.querySelector(".tab-content")),this.captures.length>50&&(URL.revokeObjectURL(this.captures[0].image_url),this.captures.shift());const s=URL.createObjectURL(t);this.captures.push({blob:t,filename:e,image_url:s,selected:!1,focused:!1}),this.$nextTick((()=>{this.captures_element.scrollTo({top:this.captures_element.scrollHeight,behavior:"smooth"})}))},async drawProgramTitleOnCapture(t){const e=await createImageBitmap(t),s="OffscreenCanvas"in window?new OffscreenCanvas(e.width,e.height):document.createElement("canvas"),i=s.getContext("2d");switch(i.drawImage(e,0,0),e.close(),i.font="bold 22px 'YakuHanJPs', 'Open Sans', 'Hiragino Sans', 'Noto Sans JP', sans-serif",i.fillStyle="rgba(255, 255, 255, 70%)",i.shadowColor="rgba(0, 0, 0, 100%)",i.shadowBlur=4,i.shadowOffsetX=0,i.shadowOffsetY=0,F.getSettingsItem("tweet_capture_watermark_position")){case"TopLeft":i.textAlign="left",i.textBaseline="top",i.fillText(this.channel.program_present.title,16,12);break;case"TopRight":i.textAlign="right",i.textBaseline="top",i.fillText(this.channel.program_present.title,s.width-16,12);break;case"BottomLeft":i.textAlign="left",i.textBaseline="bottom",i.fillText(this.channel.program_present.title,16,s.height-12);break;case"BottomRight":i.textAlign="right",i.textBaseline="bottom",i.fillText(this.channel.program_present.title,s.width-16,s.height-12);break}return"OffscreenCanvas"in window?await s.convertToBlob({type:"image/jpeg",quality:1}):new Promise((t=>s.toBlob((e=>t(e)),"image/jpeg",1)))},getChannelHashtag(t){return t.startsWith("NHK総合")?"#nhk":t.startsWith("NHKEテレ")?"#etv":t.startsWith("日テレ")?"#ntv":t.startsWith("読売テレビ")?"#ytv":t.startsWith("中京テレビ")?"#chukyotv":t.startsWith("テレビ朝日")?"#tvasahi":t.startsWith("ABCテレビ")?"#abc":t.startsWith("メ~テレ")?"#nagoyatv":t.startsWith("TBS")&&!t.includes("TBSチャンネル")?"#tbs":t.startsWith("MBS")?"#mbs":t.startsWith("CBC")?"#cbc":t.startsWith("テレビ東京")?"#tvtokyo":t.startsWith("テレビ大阪")?"#tvo":t.startsWith("テレビ愛知")?"#tva":t.startsWith("フジテレビ")?"#fujitv":t.startsWith("関西テレビ")?"#kantele":t.startsWith("東海テレビ")?"#tokaitv":t.startsWith("TOKYO MX")?"#tokyomx":t.startsWith("tvk")?"#tvk":t.startsWith("チバテレ")?"#chibatv":t.startsWith("テレ玉")?"#teletama":t.startsWith("サンテレビ")?"#suntv":t.startsWith("KBS京都")?"#kbs":t.startsWith("NHKBS1")?"#nhkbs1":t.startsWith("NHKBSプレミアム")?"#nhkbsp":t.startsWith("BS日テレ")?"#bsntv":t.startsWith("BS朝日")?"#bsasahi":t.startsWith("BS-TBS")?"#bstbs":t.startsWith("BSテレ東")?"#bstvtokyo":t.startsWith("BSフジ")?"#bsfuji":t.startsWith("BS11イレブン")?"#bs11":t.startsWith("BS12トゥエルビ")?"#bs12":t.startsWith("AT-X")?"#at_x":null},formatHashtag(t,e=!1){const s=t.trim().replaceAll("♯","#").replaceAll("#","#").replace(/#{2,}/g,"#").replaceAll(" "," ").replaceAll(/ +/g," ").split(" ").filter((t=>""!==t));for(let i in s)s[i].startsWith("#")||(s[i]=`#${s[i]}`);if(!0===F.getSettingsItem("auto_add_watching_channel_hashtag")&&!1===e){const t=this.getChannelHashtag(this.channel.channel_name);null!==t&&!1===s.includes(t)&&s.push(t)}return s.join(" ")},async sendTweet(){this.tweet_hashtag=this.formatHashtag(this.tweet_hashtag);const t=this.tweet_hashtag;let e=this.tweet_text;if(""!==t)switch(F.getSettingsItem("tweet_hashtag_position")){case"Prepend":e=`${t} ${this.tweet_text}`;break;case"Append":e=`${this.tweet_text} ${t}`;break;case"PrependWithLineBreak":e=`${t}\n${this.tweet_text}`;break;case"AppendWithLineBreak":e=`${this.tweet_text}\n${t}`;break}const s=new FormData;s.append("tweet",e);for(let a of this.tweet_captures)"None"!==F.getSettingsItem("tweet_capture_watermark_position")&&(a=await this.drawProgramTitleOnCapture(a)),s.append("images",a);for(const a of this.captures)a.selected=!1,a.focused=!1;this.tweet_captures=[],this.tweet_text="",!0===F.getSettingsItem("fold_panel_after_sending_tweet")&&(this.$emit("panel_folding_requested"),this.$refs.tweet_text.blur());try{const t=await n["default"].axios.post(`/twitter/accounts/${this.selected_twitter_account.screen_name}/tweets`,s,{headers:{"Content-Type":"multipart/form-data"}});!0===t.data.is_success?this.player.notice(t.data.detail):this.player.notice("エラー: "+t.data.detail)}catch(i){console.error(i),this.player.notice("エラー: ツイートの送信に失敗しました。")}}}}),ge=he,ve=(0,p.Z)(ge,de,me,!1,null,"4a0651e4",null),fe=ve.exports;const we=1.5,ye=3;var be=n["default"].extend({name:"TV-Watch",components:{Channel:Kt,Comment:ae,Program:_e,Twitter:fe},data(){return{Utils:F,ProgramUtils:V,time:A()().format("YYYY/MM/DD HH:mm:ss"),tv_panel_active_tab:F.getSettingsItem("tv_panel_active_tab"),background_url:"",is_loading:!0,is_video_buffering:!0,is_background_display:!1,is_control_display:!0,is_panel_display:(()=>{switch(F.getSettingsItem("panel_display_state")){case"AlwaysDisplay":return!0;case"AlwaysFold":return!1;case"RestorePreviousState":return F.getSettingsItem("showed_panel_last_time")}})(),is_fullscreen:!1,is_ime_composing:!1,is_virtual_keyboard_display:!1,is_comment_send_just_did:!1,interval_ids:[],control_interval_id:0,channel_id:this.$route.params.channel_id,channel:At,channel_previous:At,channel_next:At,channels_list:new Map,player:null,romsounds_context:null,romsounds_buffers:[],eventsource:null,fullscreen_handler:null,capture_handler:null,shortcut_key_handler:null,shortcut_key_pressed_at:Date.now(),shortcut_key_modal:!1,shortcut_key_list:{left_column:[{name:"全般",icon:"fluent:home-20-filled",icon_height:"22px",shortcuts:[{name:"数字キー・テンキーに対応するリモコン番号 (1~12) の地デジチャンネルに切り替える",keys:[{name:"1~9, 0, -(=), ^(~)",icon:!1}]},{name:"数字キー・テンキーに対応するリモコン番号 (1~12) の BS チャンネルに切り替える",keys:[{name:"Shift",icon:!1},{name:"1~9, 0, -(=), ^(~)",icon:!1}]},{name:"前のチャンネルに切り替える",keys:[{name:"fluent:arrow-up-12-filled",icon:!0}]},{name:"次のチャンネルに切り替える",keys:[{name:"fluent:arrow-down-12-filled",icon:!0}]},{name:"キーボードショートカットの一覧を表示する",keys:[{name:"/(?)",icon:!1}]}]},{name:"プレイヤー",icon:"fluent:play-20-filled",icon_height:"20px",shortcuts:[{name:"再生 / 一時停止の切り替え",keys:[{name:"Space",icon:!1}]},{name:"再生 / 一時停止の切り替え (キャプチャタブ表示時)",keys:[{name:"Shift",icon:!1},{name:"Space",icon:!1}]},{name:"プレイヤーの音量を上げる",keys:[{name:F.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-up-12-filled",icon:!0}]},{name:"プレイヤーの音量を下げる",keys:[{name:F.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-down-12-filled",icon:!0}]},{name:"停止して0.5秒早戻し",keys:[{name:F.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-left-12-filled",icon:!0}]},{name:"停止して0.5秒早送り",keys:[{name:F.CtrlOrCmd(),icon:!1},{name:"fluent:arrow-right-12-filled",icon:!0}]},{name:"フルスクリーンの切り替え",keys:[{name:"F",icon:!1}]},{name:"ライブストリームの同期",keys:[{name:"W",icon:!1}]},{name:"Picture-in-Picture の表示切り替え",keys:[{name:"E",icon:!1}]},{name:"字幕の表示切り替え",keys:[{name:"S",icon:!1}]},{name:"コメントの表示切り替え",keys:[{name:"D",icon:!1}]},{name:"映像をキャプチャする",keys:[{name:"C",icon:!1}]},{name:"映像をコメントを付けてキャプチャする",keys:[{name:"V",icon:!1}]},{name:"コメント入力フォームにフォーカスする",keys:[{name:"M",icon:!1}]},{name:"コメント入力フォームを閉じる",keys:[{name:F.CtrlOrCmd(),icon:!1},{name:"M",icon:!1}]}]}],right_column:[{name:"パネル",icon:"fluent:panel-right-20-filled",icon_height:"24px",shortcuts:[{name:"パネルの表示切り替え",keys:[{name:"P",icon:!1}]},{name:"番組情報タブを表示する",keys:[{name:"K",icon:!1}]},{name:"チャンネルタブを表示する",keys:[{name:"L",icon:!1}]},{name:"コメントタブを表示する",keys:[{name:";(+)",icon:!1}]},{name:"Twitter タブを表示する",keys:[{name:":(*)",icon:!1}]}]},{name:"Twitter",icon:"fa-brands:twitter",icon_height:"22px",shortcuts:[{name:"ツイート検索タブを表示する",keys:[{name:"[ (「)",icon:!1}]},{name:"タイムラインタブを表示する",keys:[{name:"] (」)",icon:!1}]},{name:"キャプチャタブを表示する",keys:[{name:"\(¥)",icon:!1}]},{name:"ツイート入力フォームにフォーカスを当てる/フォーカスを外す",keys:[{name:"Tab",icon:!1}]},{name:"キャプチャにフォーカスする",keys:[{name:"キャプチャタブを表示",icon:!1},{name:"fluent:arrow-up-12-filled;fluent:arrow-down-12-filled;fluent:arrow-left-12-filled;fluent:arrow-right-12-filled",icon:!0}]},{name:"キャプチャを拡大表示する/
キャプチャの拡大表示を閉じる",keys:[{name:"キャプチャにフォーカス",icon:!1},{name:"Enter",icon:!1}]},{name:"キャプチャを選択する/
キャプチャの選択を解除する",keys:[{name:"キャプチャにフォーカス",icon:!1},{name:"Space",icon:!1}]},{name:"クリップボード内の画像を
キャプチャとして取り込む",keys:[{name:"ツイート入力
フォームにフォーカス",icon:!1},{name:F.CtrlOrCmd(),icon:!1},{name:"V",icon:!1}]},{name:"ツイートを送信する",keys:[{name:"Twitter タブを表示",icon:!1},{name:F.CtrlOrCmd(),icon:!1},{name:"Enter",icon:!1}]}]}]}}},async created(){"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!0,navigator.virtualKeyboard.ongeometrychange=t=>{0===t.target.boundingRect.width&&0===t.target.boundingRect.height?this.is_virtual_keyboard_display=!1:this.is_virtual_keyboard_display=!0}),this.init(),this.romsounds_context=new AudioContext;for(let t=1;t<=14;t++){const e=`/assets/romsounds/${t.toString().padStart(2,"0")}.wav`,s=await n["default"].axios.get(e,{baseURL:"",responseType:"arraybuffer"});this.romsounds_buffers.push(await this.romsounds_context.decodeAudioData(s.data))}},beforeDestroy(){"virtualKeyboard"in navigator&&(navigator.virtualKeyboard.overlaysContent=!1),this.destroy(!0),this.romsounds_context.close()},beforeRouteUpdate(t,e,s){this.destroy(),this.channel_id=t.params.channel_id,[this.channel_previous,this.channel,this.channel_next]=T.getPreviousAndCurrentAndNextChannel(this.channels_list,this.channel_id),!0===F.getSettingsItem("reset_hashtag_when_program_switches")&&(this.$refs.Twitter.tweet_hashtag=""),this.interval_ids.push(window.setTimeout((()=>this.init()),500)),s()},watch:{is_panel_display(){F.setSettingsItem("showed_panel_last_time",this.is_panel_display)}},methods:{init(){this.background_url=z.generatePlayerBackgroundURL(),this.controlDisplayTimer(),this.update(),this.interval_ids.push(window.setInterval((()=>{this.time=A()().format("YYYY/MM/DD HH:mm:ss")}),1e3));const t=60-Math.floor((new Date).getTime()/1e3)%60;this.interval_ids.push(window.setTimeout((()=>{this.update(),this.interval_ids.push(window.setInterval((()=>{this.update()}),3e4))}),1e3*t))},async update(){if(void 0===this.$route.params.channel_id)return;let t;try{t=await n["default"].axios.get(`/channels/${this.channel_id}`)}catch(r){return console.error(r),void(r.response&&422===r.response.status&&"Specified channel_id was not found"===r.response.data.detail&&await this.$router.push({path:"/not-found/"}))}const e=t.data;if((this.channel.id!==e.id||null!==this.channel.program_present&&null===e.program_present||null===this.channel.program_present&&null!==e.program_present||null!==this.channel.program_present&&null!==e.program_present&&this.channel.program_present.id!==e.program_present.id)&&!0===F.getSettingsItem("reset_hashtag_when_program_switches")){const t=this.$refs.Twitter;t.tweet_hashtag=t.formatHashtag("")}let s;this.channel=e,null!==this.player&&!0!==this.player.KonomiTVCanDestroy||(this.initPlayer(),this.initEventHandler(),this.initCaptureHandler(),document.removeEventListener("keydown",this.shortcut_key_handler),this.initShortcutKeyHandler()),null===this.channel.program_present||"1/0+1/0モード(デュアルモノ)"!==this.channel.program_present.primary_audio_type&&null===this.channel.program_present.secondary_audio_type?(this.player.template.audioItem[1].classList.add("dplayer-setting-audio-item--disabled"),this.player.plugins.mpegts&&window.setTimeout((()=>{this.player.template.audioItem[0].classList.add("dplayer-setting-audio-current"),this.player.template.audioItem[1].classList.remove("dplayer-setting-audio-current"),this.player.template.audioValue.textContent=this.player.tran("Primary audio");try{this.player.plugins.mpegts.switchPrimaryAudio()}catch(r){}}),300)):this.player.template.audioItem[1].classList.remove("dplayer-setting-audio-item--disabled");try{s=await n["default"].axios.get("/channels")}catch(r){return void console.error(r)}const i=t=>t.is_display||this.channel_id===t.channel_id;this.channels_list=new Map,this.channels_list.set("ピン留め",[]),s.data.GR.length>0&&this.channels_list.set("地デジ",s.data.GR.filter(i)),s.data.BS.length>0&&this.channels_list.set("BS",s.data.BS.filter(i)),s.data.CS.length>0&&this.channels_list.set("CS",s.data.CS.filter(i)),s.data.CATV.length>0&&this.channels_list.set("CATV",s.data.CATV.filter(i)),s.data.SKY.length>0&&this.channels_list.set("SKY",s.data.SKY.filter(i)),s.data.STARDIGIO.length>0&&this.channels_list.set("StarDigio",s.data.STARDIGIO.filter(i));const a=F.getSettingsItem("pinned_channel_ids"),o=[];for(const n of a){const t=T.getChannelType(n,!0),e=this.channels_list.get(t).find((t=>t.channel_id===n));void 0!==e&&o.push(e)}if(o.length>0?this.channels_list.set("ピン留め",o):this.channels_list.delete("ピン留め"),[this.channel_previous,,this.channel_next]=T.getPreviousAndCurrentAndNextChannel(this.channels_list,this.channel_id),"mediaSession"in navigator){const t=[{src:"/assets/images/icons/icon-maskable-192px.png",sizes:"192x192",type:"image/png"},{src:"/assets/images/icons/icon-maskable-512px.png",sizes:"512x512",type:"image/png"}];navigator.mediaSession.metadata=new MediaMetadata({title:this.channel.program_present?this.channel.program_present.title:"放送休止",artist:this.channel.channel_name,artwork:t}),"setPositionState"in navigator.mediaSession&&navigator.mediaSession.setPositionState({duration:0,playbackRate:1}),navigator.mediaSession.setActionHandler("play",(()=>{this.player.play()})),navigator.mediaSession.setActionHandler("pause",(()=>{this.player.pause()})),navigator.mediaSession.setActionHandler("previoustrack",(async()=>{navigator.mediaSession.metadata=new MediaMetadata({title:this.channel_previous.program_present?this.channel_previous.program_present.title:"放送休止",artist:this.channel_previous.channel_name,artwork:t}),await this.$router.push({path:`/tv/watch/${this.channel_previous.channel_id}`})})),navigator.mediaSession.setActionHandler("nexttrack",(async()=>{navigator.mediaSession.metadata=new MediaMetadata({title:this.channel_next.program_present?this.channel_next.program_present.title:"放送休止",artist:this.channel_next.channel_name,artwork:t}),await this.$router.push({path:`/tv/watch/${this.channel_next.channel_id}`})}))}},controlDisplayTimer(t=null,e=!1){const s=/iPhone|iPad|iPod|Windows|Macintosh|Android|Mobile/i.test(navigator.userAgent)&&"ontouchend"in document;if(1==s&&null!==t&&"mousemove"===t.type)return;if(0==s&&null!==t&&("touchmove"===t.type||"click"===t.type))return;window.clearTimeout(this.control_interval_id);const i=()=>{null!==this.player&&this.player.template.controller.classList.contains("dplayer-controller-comment")?this.control_interval_id=window.setTimeout(i,3e3):(this.is_control_display=!1,null!==this.player&&(this.player.controller.hide(),this.player.setting.hide()))};!0===s&&!0===e?this.player.controller.isShow()?(this.is_control_display=!0,this.player.controller.show(),this.control_interval_id=window.setTimeout(i,3e3)):(this.is_control_display=!1,this.player.controller.hide(),this.player.setting.hide()):(this.is_control_display=!0,null!==this.player&&this.player.controller.show(),this.control_interval_id=window.setTimeout(i,3e3))},initPlayer(){if(window.mpegts=Pt(),null!==this.player&&!0===this.player.KonomiTVCanDestroy){try{this.player.destroy()}catch(a){void 0!==this.player.plugins.mpegts&&this.player.plugins.mpegts.destroy()}this.player=null}const t=F.getSettingsItem("tv_low_latency_mode")?we:ye;this.player=new(Tt())({container:this.$el.querySelector(".watch-player__dplayer"),theme:"#E64F97",lang:"ja-jp",live:!0,liveSyncMinBufferSize:t,loop:!1,airplay:!1,autoplay:!0,hotkey:!1,screenshot:!1,volume:1,video:{defaultQuality:this.channel.is_radiochannel?"48kHz/192kbps":F.getSettingsItem("tv_streaming_quality"),quality:(()=>{const t=[];if(this.channel.is_radiochannel)t.push({name:"48kHz/192kbps",type:"mpegts",url:`${F.api_base_url}/streams/live/${this.channel_id}/1080p/mpegts`});else{let e="";z.isHEVCVideoSupported()&&!0===F.getSettingsItem("tv_data_saver_mode")&&(e="-hevc");for(const s of["1080p-60fps","1080p","810p","720p","540p","480p","360p","240p"])t.push({name:"1080p-60fps"===s?"1080p (60fps)":s,type:"mpegts",url:`${F.api_base_url}/streams/live/${this.channel_id}/${s}${e}/mpegts`})}return t})()},danmaku:{user:"KonomiTV",speedRate:F.getSettingsItem("comment_speed_rate"),fontSize:F.getSettingsItem("comment_font_size")},apiBackend:{read:t=>{t.success([{}])},send:async t=>{await this.$refs.Comment.sendComment(t)}},pluginOptions:{mpegts:{config:{enableWorker:!0,liveSync:F.getSettingsItem("tv_low_latency_mode"),liveSyncMaxLatency:3,liveSyncTargetLatency:t,liveSyncPlaybackRate:1.1}},aribb24:{normalFont:`"${F.getSettingsItem("caption_font")}", sans-serif`,forceStrokeColor:!!F.getSettingsItem("always_border_caption_text"),forceBackgroundColor:F.getSettingsItem("specify_caption_background_color")?F.getSettingsItem("caption_background_color"):null,drcsReplacement:!0,enableRawCanvas:!0,useStrokeText:!0,usePUA:(()=>{const t=F.getSettingsItem("caption_font"),e=document.createElement("canvas").getContext("2d");return e.font=`10px ${t}`,e.fillText("Test",0,0),!!t.startsWith("Windows TV")})(),PRACallback:async t=>{if(!1===F.getSettingsItem("tv_show_superimpose"))return;"suspended"===this.romsounds_context.state&&await this.romsounds_context.resume();const e=this.romsounds_context.createBufferSource();e.buffer=this.romsounds_buffers[t];const s=this.romsounds_context.createGain();e.connect(s),s.connect(this.romsounds_context.destination),s.gain.value=3,e.start(0)}}},subtitle:{type:"aribb24"}}),window.player=this.player,this.player.controller.setAutoHide=t=>{},this.player.template.commentInput.addEventListener("keydown",(t=>{"Enter"===t.code&&(this.is_comment_send_just_did=!0,setTimeout((()=>this.is_comment_send_just_did=!1),100))})),this.player.comment.send=()=>{!0===F.getSettingsItem("close_comment_form_after_sending")&&this.player.template.commentInput.blur(),this.player.template.commentInput.value.replace(/^\s+|\s+$/g,"")?(this.player.danmaku.send({text:this.player.template.commentInput.value,color:this.player.container.querySelector(".dplayer-comment-setting-color input:checked").value,type:this.player.container.querySelector(".dplayer-comment-setting-type input:checked").value,size:this.player.container.querySelector(".dplayer-comment-setting-size input:checked").value},(()=>{!0===F.getSettingsItem("close_comment_form_after_sending")&&this.player.comment.hide()}),!0),this.player.template.commentInput.value=""):this.player.notice(this.player.tran("Please input danmaku content!"))};const e=/iPhone|iPad|iPod|Macintosh|Android|Mobile/i.test(navigator.userAgent)&&"ontouchend"in document;if(!1===e){this.player.template.settingOriginPanel.insertAdjacentHTML("beforeend",'\n
\n キーボードショートカット\n
\n \n \n \n
\n
');const t=this.player.template.settingOriginPanel.scrollHeight;this.player.template.settingBox.style.clipPath=`inset(calc(100% - ${t}px) 0 0 round 7px)`,this.$el.querySelector(".dplayer-setting-keyboard-shortcut").addEventListener("click",(()=>{this.player.setting.hide(),this.shortcut_key_modal=!0}))}const s=document.querySelector(".v-application");this.fullscreen_handler=()=>this.is_fullscreen=this.player.fullScreen.isFullScreen(),void 0!==s.onfullscreenchange?s.addEventListener("fullscreenchange",this.fullscreen_handler):s.addEventListener("webkitfullscreenchange",this.fullscreen_handler),this.player.fullScreen.isFullScreen=t=>!(!document.fullscreenElement&&!document.webkitFullscreenElement),this.player.fullScreen.request=t=>{this.player.fullScreen.isFullScreen()?this.player.fullScreen.cancel():(s.requestFullscreen=s.requestFullscreen||s.webkitRequestFullscreen,s.requestFullscreen&&s.requestFullscreen(),screen.orientation&&screen.orientation.lock("landscape").catch((()=>{})))},this.player.fullScreen.cancel=t=>{document.exitFullscreen=document.exitFullscreen||document.webkitExitFullscreen,document.exitFullscreen&&document.exitFullscreen(),screen.orientation&&screen.orientation.unlock()};const i=()=>{this.player.setting.hide(),this.controlDisplayTimer()};this.player.on("play",i),this.player.on("pause",i),this.player.on("quality_start",(()=>{this.background_url=z.generatePlayerBackgroundURL(),null!==this.eventsource&&(this.eventsource.close(),this.eventsource=null),this.initEventHandler()})),this.interval_ids.push(window.setInterval((()=>{this.player.video.paused&&this.player.video.buffered.end(0)-this.player.video.currentTime>30&&this.player.sync()}),6e4)),!0===F.getSettingsItem("tv_show_superimpose")?(this.player.plugins.aribb24Superimpose.show(),this.player.on("subtitle_hide",(()=>{this.player.plugins.aribb24Superimpose.show()}))):(this.player.plugins.aribb24Superimpose.hide(),this.player.on("subtitle_show",(()=>{this.player.plugins.aribb24Superimpose.hide()})))},initEventHandler(){this.is_loading=!0,this.player.video.volume=0;const t=()=>{this.player.video.oncanplay=null,this.player.video.oncanplaythrough=null,this.player.video.playbackRate=0,window.setTimeout((async()=>{const t=()=>{try{return Math.round(1e3*(this.player.video.buffered.end(0)-this.player.video.currentTime))/1e3}catch(t){return 0}},e=F.getSettingsItem("tv_low_latency_mode")?we:ye;let s=t();while(sthis.is_video_buffering=!0)),this.player.video.addEventListener("playing",(()=>this.is_video_buffering=!1)),this.is_loading=!1,this.is_video_buffering=!1,this.channel.is_radiochannel?this.is_background_display=!0:this.is_background_display=!1;const i=this.player.user.get("volume");while(this.player.video.volume+.05{const e=JSON.parse(t.data);"Standby"===e.status&&(this.is_video_buffering=!0,this.is_background_display=!0)})),this.eventsource.addEventListener("status_update",(t=>{const e=JSON.parse(t.data);switch(console.log(`Status: ${e.status} / Detail: ${e.detail}`),this.channel.viewers=e.clients_count,e.status){case"Standby":this.player.template.notice.textContent.includes("画質を")||this.player.notice(e.detail,-1),this.is_video_buffering=!0,this.is_background_display=!0;break;case"ONAir":this.player.template.notice.textContent.includes("画質を")||this.player.notice(this.player.template.notice.textContent,1e-6),document.pictureInPictureElement&&(document.exitPictureInPicture(),this.player.video.requestPictureInPicture());break;case"Restart":this.player.notice(e.detail,-1),this.player.switchVideo({url:this.player.quality.url,type:this.player.quality.type}),this.player.play(),this.is_video_buffering=!0,this.is_background_display=!0;break;case"Offline":this.player.notice(e.detail,-1),this.player.video.onerror=()=>{this.player.notice(e.detail,-1),this.player.video.onerror=null},this.player.danmaku.clear(),this.player.video.pause(),this.eventsource.close(),this.is_background_display=!0,this.is_loading=!1,this.is_video_buffering=!1;break}})),this.eventsource.addEventListener("detail_update",(t=>{const e=JSON.parse(t.data);console.log(`Status: ${e.status} Detail:${e.detail}`),this.channel.viewers=e.clients_count,"Standby"===e.status&&(this.player.notice(e.detail,-1),this.is_background_display||(this.is_background_display=!0))})),this.eventsource.addEventListener("clients_update",(t=>{const e=JSON.parse(t.data);this.channel.viewers=e.clients_count}))},initShortcutKeyHandler(){const t=this.$refs.Twitter,e=t.$el.querySelector(".tweet-form__textarea");for(const s of document.querySelectorAll("input[type=text],input[type=search],textarea"))s.addEventListener("compositionstart",(()=>this.is_ime_composing=!0)),s.addEventListener("compositionend",(()=>this.is_ime_composing=!1));this.shortcut_key_handler=async s=>{const i=document.activeElement.tagName.toUpperCase(),a=document.activeElement.getAttribute("contenteditable");["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(s.code)&&"INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a&&s.preventDefault();let n=!1;s.repeat&&(n=!0);const o=Date.now();if(o-this.shortcut_key_pressed_at<50)return;this.shortcut_key_pressed_at=o;const r=await(async()=>{if(("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a||document.activeElement===e)&&!1===this.is_ime_composing&&"Tab"===s.code)return document.activeElement===e?(e.blur(),!0):(this.is_panel_display=!0,this.tv_panel_active_tab="Twitter",e.focus(),this.$el.scrollLeft=0,window.setTimeout((()=>{e.focus(),this.$el.scrollLeft=0}),100),!0);if(("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a||document.activeElement===e)&&"Twitter"===this.tv_panel_active_tab&&!1===this.is_ime_composing&&(s.ctrlKey||s.metaKey||s.shiftKey)&&"Enter"===s.code)return t.$el.querySelector(".tweet-button").click(),!0;if(null!==this.player&&!s.shiftKey&&!s.altKey&&this.player.template.controller.classList.contains("dplayer-controller-comment")&&(s.ctrlKey||s.metaKey)&&"KeyM"===s.code)return this.player.comment.hide(),!0;if("INPUT"!==i&&"TEXTAREA"!==i&&""!==a&&"true"!==a){if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.altKey){const t=s.shiftKey?"BS":"GR";let e=null;if("Digit1"!==s.code&&"Digit2"!==s.code&&"Digit3"!==s.code&&"Digit4"!==s.code&&"Digit5"!==s.code&&"Digit6"!==s.code&&"Digit7"!==s.code&&"Digit8"!==s.code&&"Digit9"!==s.code||(e=Number(s.code.replace("Digit",""))),"Digit0"===s.code&&(e=10),"Minus"===s.code&&(e=11),"Equal"===s.code&&(e=12),"Numpad1"!==s.code&&"Numpad2"!==s.code&&"Numpad3"!==s.code&&"Numpad4"!==s.code&&"Numpad5"!==s.code&&"Numpad6"!==s.code&&"Numpad7"!==s.code&&"Numpad8"!==s.code&&"Numpad9"!==s.code||(e=Number(s.code.replace("Numpad",""))),"Numpad0"===s.code&&(e=10),null!==e){const s=T.getChannelFromRemoconID(this.channels_list,t,e);if(null!==s&&s.channel_id!==this.channel_id)return await this.$router.push({path:`/tv/watch/${s.channel_id}`}),!0}}if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("Slash"===s.code)return this.shortcut_key_modal=!this.shortcut_key_modal,!0;if("KeyP"===s.code)return this.is_panel_display=!this.is_panel_display,!0;if("KeyK"===s.code)return this.tv_panel_active_tab="Program",!0;if("KeyL"===s.code)return this.tv_panel_active_tab="Channel",!0;if("Semicolon"===s.code)return this.tv_panel_active_tab="Comment",!0;if("Quote"===s.code)return this.tv_panel_active_tab="Twitter",!0;if("BracketRight"===s.code)return t.twitter_active_tab="Search",!0;if("Backslash"===s.code)return t.twitter_active_tab="Timeline",!0;if("IntlRo"===s.code)return t.twitter_active_tab="Capture",!0}if("Twitter"===this.tv_panel_active_tab&&"Capture"===t.twitter_active_tab&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if(["ArrowUp","ArrowDown","ArrowLeft","ArrowRight"].includes(s.code)){if(0===t.captures.length)return!1;if(!1===t.captures.some((t=>!0===t.focused)))return t.captures[t.captures.length-1].focused=!0,!0;const e=t.captures.findIndex((t=>!0===t.focused));if("ArrowUp"===s.code){if(e-2<0)return!1;t.captures[e-2].focused=!0}if("ArrowDown"===s.code){if(e+2>t.captures.length-1)return!1;t.captures[e+2].focused=!0}if("ArrowLeft"===s.code){if(e-1<0)return!1;t.captures[e-1].focused=!0}if("ArrowRight"===s.code){if(e+1>t.captures.length-1)return!1;t.captures[e+1].focused=!0}t.captures[e].focused=!1;const i=t.captures.find((t=>!0===t.focused));!0===t.zoom_capture_modal&&(t.zoom_capture=i);const a=t.$el.querySelector(`img[src="${i.image_url}"]`).parentElement;return n?a.scrollIntoView({block:"nearest",inline:"nearest",behavior:"auto"}):a.scrollIntoView({block:"nearest",inline:"nearest",behavior:"smooth"}),!0}if("Enter"===s.code){if(this.is_comment_send_just_did)return!1;if(!0===t.zoom_capture_modal)return t.zoom_capture_modal=!1,!0;const e=t.captures.find((t=>!0===t.focused));return void 0!==e&&(t.zoom_capture=e,t.zoom_capture_modal=!0,!0)}if("Space"===s.code){const e=t.captures.find((t=>!0===t.focused));return void 0!==e&&(t.clickCapture(e),!0)}}if(!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("ArrowUp"===s.code)return await this.$router.push({path:`/tv/watch/${this.channel_previous.channel_id}`}),!0;if("ArrowDown"===s.code)return await this.$router.push({path:`/tv/watch/${this.channel_next.channel_id}`}),!0}if(null!==this.player&&!s.shiftKey&&!s.altKey){if((s.ctrlKey||s.metaKey)&&"ArrowUp"===s.code)return this.player.volume(this.player.volume()+.05),!0;if((s.ctrlKey||s.metaKey)&&"ArrowDown"===s.code)return this.player.volume(this.player.volume()-.05),!0;if((s.ctrlKey||s.metaKey)&&"ArrowLeft"===s.code)return!1===this.player.video.paused&&this.player.video.pause(),this.player.video.currentTime=this.player.video.currentTime-.5,!0;if((s.ctrlKey||s.metaKey)&&"ArrowRight"===s.code)return!1===this.player.video.paused&&this.player.video.pause(),this.player.video.currentTime=this.player.video.currentTime+.5,!0}if(null!==this.player&&!s.ctrlKey&&!s.metaKey&&!s.altKey&&!0===s.shiftKey&&"Space"===s.code&&!1===n&&"Twitter"===this.tv_panel_active_tab&&"Capture"===t.twitter_active_tab)return this.player.toggle(),!0;if(null!==this.player&&!1===n&&!s.ctrlKey&&!s.metaKey&&!s.shiftKey&&!s.altKey){if("Space"===s.code)return this.player.toggle(),!0;if("KeyF"===s.code)return this.player.fullScreen.toggle(),!0;if("KeyW"===s.code)return this.player.sync(),!0;if("KeyE"===s.code)return document.pictureInPictureEnabled&&this.player.template.pipButton.click(),!0;if("KeyS"===s.code)return this.player.subtitle.toggle(),this.player.subtitle.container.classList.contains("dplayer-subtitle-hide")?this.player.notice(`${this.player.tran("Hide subtitle")}`):this.player.notice(`${this.player.tran("Show subtitle")}`),!0;if("KeyD"===s.code)return this.player.template.showDanmaku.click(),this.player.template.showDanmakuToggle.checked?this.player.notice(`${this.player.tran("Show comment")}`):this.player.notice(`${this.player.tran("Hide comment")}`),!0;if("KeyC"===s.code)return await this.capture_handler.captureAndSave(this.channel,!1),!0;if("KeyV"===s.code)return await this.capture_handler.captureAndSave(this.channel,!0),!0;if("KeyM"===s.code)return this.player.controller.show(),this.player.comment.show(),this.controlDisplayTimer(),window.setTimeout((()=>this.player.template.commentInput.focus()),100),!0}}return!1})();!0===r&&s.preventDefault()},document.addEventListener("keydown",this.shortcut_key_handler)},initCaptureHandler(){this.capture_handler=new D(this.player,((t,e)=>{this.$refs.Twitter.addCaptureList(t,e)}));const t=this.$el.querySelector(".dplayer-icon.dplayer-capture-icon");t.addEventListener("click",(async()=>{await this.capture_handler.captureAndSave(this.channel,!1)}));const e=this.$el.querySelector(".dplayer-icon.dplayer-comment-capture-icon");e.addEventListener("click",(async()=>{await this.capture_handler.captureAndSave(this.channel,!0)}))},destroy(t=!1){for(const e of this.interval_ids)window.clearInterval(e);window.clearTimeout(this.control_interval_id),this.interval_ids=[],this.is_loading=!0,this.is_background_display=!1,this.player.KonomiTVCanDestroy=!0,null!==this.eventsource&&(this.eventsource.close(),this.eventsource=null),this.interval_ids.push(window.setTimeout((()=>{if(this.player.video.pause(),!0===t&&null!==this.player){try{this.player.destroy()}catch(e){void 0!==this.player.plugins.mpegts&&this.player.plugins.mpegts.destroy()}this.player=null}}),400))}}}),Ce=be,xe=(0,p.Z)(Ce,Ot,St,!1,null,"49a8f767",null),ke=xe.exports,Oe=s(9223),Se=s(4611),Ie=s(4650),Te=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e(wt.Z,{staticClass:"settings-container d-flex px-5 py-5 mx-auto background",attrs:{elevation:"0",width:"100%","max-width":"1000"}},[e(Ie.Z,{staticClass:"settings-navigation flex-shrink-0 background",attrs:{permanent:"",width:"100%",height:"auto"}},[e(Ht.Z,{staticClass:"px-4"},[e(Lt.km,[e("h1",[t._v("設定")])])],1),e(Et.Z,{staticClass:"mt-2 px-0",attrs:{nav:""}},[e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/general"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 3px"},attrs:{icon:"fa-solid:sliders-h",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("全般")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/account"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("アカウント")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/jikkyo"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 2px"},attrs:{icon:"bi:chat-left-text-fill",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("ニコニコ実況")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/twitter"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 1px"},attrs:{icon:"fa-brands:twitter",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("Twitter")])],1)],1),e(Oe.Z),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/environment"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("環境設定")])],1)],1)],1)],1)],1)],1)],1)},je=[],Pe=n["default"].extend({name:"Settings-Index",components:{Header:lt,Navigation:pt}}),Ze=Pe,Ae=(0,p.Z)(Ze,Te,je,!1,null,"a67629d4",null),$e=Ae.exports,De=s(3422),ze=s(1625),Ne=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fa-solid:sliders-h",width:"19px"}}),e("span",{staticClass:"ml-3"},[t._v("全般")])],1),e("div",{staticClass:"settings__content"},[e("div",{staticClass:"settings__item settings__item--sync-disabled"},[e("div",{staticClass:"settings__item-heading"},[t._v("テレビのストリーミング画質")]),e("div",{staticClass:"settings__item-label"},[t._v(" テレビをライブストリーミングするときの既定の画質を設定します。"),e("br"),t._v(" ストリーミング画質はプレイヤーの設定からいつでも切り替えられます。"),e("br")]),e("div",{staticClass:"settings__item-label"},[t._v(" [1080p (60fps)] は、通常 30fps (60i) の映像を補間することで、ほかの画質よりも滑らか(ぬるぬる)な映像で再生できます。ただし、再生負荷が少し高くなります。"),e("br"),t._v(" [1080p (60fps)] で視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーでは CPU 使用率が高くなり、再生に支障が出ることがあります。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tv_streaming_quality},model:{value:t.settings.tv_streaming_quality,callback:function(e){t.$set(t.settings,"tv_streaming_quality",e)},expression:"settings.tv_streaming_quality"}})],1),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled",class:{"settings__item--disabled":!1===t.PlayerUtils.isHEVCVideoSupported()}},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_data_saver_mode"}},[t._v("テレビを通信節約モードで視聴する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_data_saver_mode"}},[t._v(" テレビをライブストリーミングするときに、通信節約モードで視聴するかを設定します。"),e("br"),t._v(" 通信節約モードでは、H.265 / HEVC という圧縮率の高いコーデックを使い、画質はほぼそのまま、通信量を通常の 2/3 程度に抑えながら視聴できます。ただし、再生負荷が高くなります。"),e("br"),t._v(" 通信節約モードで視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーではまともに再生できない可能性が高いです。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_data_saver_mode",inset:"","hide-details":"",disabled:!1===t.PlayerUtils.isHEVCVideoSupported()},model:{value:t.settings.tv_data_saver_mode,callback:function(e){t.$set(t.settings,"tv_data_saver_mode",e)},expression:"settings.tv_data_saver_mode"}})],1),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_low_latency_mode"}},[t._v("テレビを低遅延で視聴する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_low_latency_mode"}},[t._v(" テレビをライブストリーミングするときに、低遅延で視聴するかを設定します。"),e("br"),t._v(" 低遅延ストリーミングがオンのときは、放送波との遅延を最短 1.9 秒に抑えて視聴できます。"),e("br"),t._v(" また、約 3 秒以上遅延したときに少しだけ再生速度を早める (1.1x) ことで、滑らかにストリーミングの遅れを取り戻します。"),e("br"),t._v(" 宅外視聴などのネットワークが不安定になりがちな環境では、一度低遅延ストリーミングをオフにしてみると、映像のカクつきを改善できるかもしれません。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_low_latency_mode",inset:"","hide-details":""},model:{value:t.settings.tv_low_latency_mode,callback:function(e){t.$set(t.settings,"tv_low_latency_mode",e)},expression:"settings.tv_low_latency_mode"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"tv_show_superimpose"}},[t._v("テレビをみるときに文字スーパーを表示する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"tv_show_superimpose"}},[t._v(" テレビをライブストリーミングするときに、文字スーパーを表示するかを設定します。"),e("br"),t._v(" 文字スーパーは、緊急地震速報の赤テロップや、NHK BS のニュース速報のテロップなどで利用されています。とくに理由がなければ、オンのままにしておくことをおすすめします。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"tv_show_superimpose",inset:"","hide-details":""},model:{value:t.settings.tv_show_superimpose,callback:function(e){t.$set(t.settings,"tv_show_superimpose",e)},expression:"settings.tv_show_superimpose"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("既定のパネルの表示状態")]),e("div",{staticClass:"settings__item-label"},[t._v(" 視聴画面を開いたときに、右側のパネルをどう表示するかを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.panel_display_state},model:{value:t.settings.panel_display_state,callback:function(e){t.$set(t.settings,"panel_display_state",e)},expression:"settings.panel_display_state"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("テレビをみるときに既定で表示されるパネルのタブ")]),e("div",{staticClass:"settings__item-label"},[t._v(" テレビの視聴画面を開いたときに、右側のパネルで最初に表示されるタブを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tv_panel_active_tab},model:{value:t.settings.tv_panel_active_tab,callback:function(e){t.$set(t.settings,"tv_panel_active_tab",e)},expression:"settings.tv_panel_active_tab"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("label",{staticClass:"settings__item-heading"},[t._v("字幕のフォント")]),e("label",{staticClass:"settings__item-label"},[t._v(" プレイヤーで字幕表示をオンにしているときの、字幕のフォントを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.caption_font},model:{value:t.settings.caption_font,callback:function(e){t.$set(t.settings,"caption_font",e)},expression:"settings.caption_font"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"always_border_caption_text"}},[t._v("字幕の文字を常に縁取って描画する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"always_border_caption_text"}},[t._v(" プレイヤーで字幕表示をオンにしているときに、字幕の文字を常に縁取って描画するかを設定します。"),e("br"),t._v(" 字幕は縁取られていた方が視認性が良く、見た目的にもきれいです。とくに理由がなければ、オンのままにしておくことをおすすめします。"),e("br"),t._v(" この設定をオフにしているときも、字幕データ側で明示的に縁取りするように指定されていれば、オンにしているとき同様に文字が縁取られて描画されます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"always_border_caption_text",inset:"","hide-details":""},model:{value:t.settings.always_border_caption_text,callback:function(e){t.$set(t.settings,"always_border_caption_text",e)},expression:"settings.always_border_caption_text"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"specify_caption_background_color"}},[t._v("字幕の背景色を指定する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"specify_caption_background_color"}},[t._v(" プレイヤーで字幕表示をオンにしているときに、字幕の背景色を明示的に指定するかを設定します。"),e("br"),t._v(" この設定をオフにしているときは、字幕データ側で指定されている背景色で描画します。とくに理由がなければ、オフのままにしておくことをおすすめします。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"specify_caption_background_color",inset:"","hide-details":""},model:{value:t.settings.specify_caption_background_color,callback:function(e){t.$set(t.settings,"specify_caption_background_color",e)},expression:"settings.specify_caption_background_color"}})],1),e("div",{staticClass:"settings__item",class:{"settings__item--disabled":!1===t.settings.specify_caption_background_color}},[e("label",{staticClass:"settings__item-heading"},[t._v("字幕の背景色")]),e("label",{staticClass:"settings__item-label"},[t._v(" プレイヤーで字幕表示をオンにしているときの、字幕の背景色を設定します。"),e("br"),t._v(" 上の [字幕の背景色を指定する] をオンにしているときのみ有効です。透明度 (アルファチャンネル) を 0 に設定すれば、字幕の背景を非表示にできます。"),e("br")]),e("div",{ref:"caption_background_color",staticClass:"settings__item-label"},[e(De.Z,{staticClass:"settings__item-form",attrs:{"hide-details":"",flat:!0,"show-alpha":!0,"show-swatches":!1,"hide-inputs":!1,width:690,"canvas-height":80,disabled:!1===t.settings.specify_caption_background_color},model:{value:t.settings.caption_background_color,callback:function(e){t.$set(t.settings,"caption_background_color",e)},expression:"settings.caption_background_color"}})],1)]),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item settings__item--switch settings__item--sync-disabled"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"capture_copy_to_clipboard"}},[t._v("キャプチャをクリップボードにコピーする")]),e("label",{staticClass:"settings__item-label",attrs:{for:"capture_copy_to_clipboard"}},[t._v(" プレイヤーでキャプチャを撮ったときに、撮ったキャプチャをクリップボードにもコピーするかを設定します。"),e("br"),t._v(" クリップボードの履歴をサポートしていない OS では、この設定をオンにした状態でキャプチャを撮ると、以前のクリップボードが上書きされます。注意してください。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"capture_copy_to_clipboard",inset:"","hide-details":""},model:{value:t.settings.capture_copy_to_clipboard,callback:function(e){t.$set(t.settings,"capture_copy_to_clipboard",e)},expression:"settings.capture_copy_to_clipboard"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("キャプチャの保存先")]),e("div",{staticClass:"settings__item-label"},[e("p",[t._v(" キャプチャした画像をブラウザでダウンロードするか、KonomiTV サーバーにアップロードするかを設定します。"),e("br"),t._v(" ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方同時に行うこともできます。"),e("br")]),e("p",[t._v(" ブラウザでダウンロードすると、視聴中のデバイスのダウンロードフォルダに保存されます。"),e("br"),t._v(" 視聴中のデバイスにそのまま保存されるためシンプルですが、保存先のフォルダを変更できないこと、PC 版 Chrome では毎回ダウンロードバーが表示されてしまうことがデメリットです。"),e("br")]),e("p",[t._v(" KonomiTV サーバーにアップロードすると、環境設定で指定されたキャプチャ保存フォルダに保存されます。視聴したデバイスにかかわらず、今までに撮ったキャプチャをひとつのフォルダにまとめて保存できます。"),e("br"),t._v(" 他のデバイスでキャプチャを見るにはキャプチャ保存フォルダをネットワークに共有する必要があること、スマホ・タブレットではネットワーク上のフォルダへのアクセスがやや面倒なことがデメリットです。"),e("br")])]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.capture_save_mode},model:{value:t.settings.capture_save_mode,callback:function(e){t.$set(t.settings,"capture_save_mode",e)},expression:"settings.capture_save_mode"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("字幕が表示されているときのキャプチャの保存モード")]),e("div",{staticClass:"settings__item-label"},[t._v(" 字幕が表示されているときに、キャプチャした画像に字幕を合成するかを設定します。"),e("br"),t._v(" 映像のみのキャプチャと、字幕を合成したキャプチャを両方同時に保存することもできます。"),e("br"),t._v(" なお、字幕が表示されていない場合は、常に映像のみ (+コメント付きキャプチャではコメントを合成して) 保存されます。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.capture_caption_mode},model:{value:t.settings.capture_caption_mode,callback:function(e){t.$set(t.settings,"capture_caption_mode",e)},expression:"settings.capture_caption_mode"}})],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("設定をエクスポート")]),e("div",{staticClass:"settings__item-label"},[t._v(" このデバイス(ブラウザ)に保存されている設定データをエクスポート(ダウンロード)できます。"),e("br"),t._v(" ダウンロードした設定データ (KonomiTV-Settings.json) は、[設定をインポート] からインポートできます。異なるサーバーの KonomiTV を同じ設定で使いたいときなどに使ってください。"),e("br")])]),e(W.Z,{staticClass:"settings__save-button mt-4",attrs:{depressed:""},on:{click:function(e){return t.exportSettings()}}},[e("Icon",{staticClass:"mr-3",attrs:{icon:"fa6-solid:download",height:"19px"}}),t._v("設定をエクスポート ")],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading error--text text--lighten-1"},[t._v("設定をインポート")]),e("div",{staticClass:"settings__item-label"},[t._v(" [設定をエクスポート] でダウンロードした設定データを、このデバイス(ブラウザ)にインポートできます。"),e("br"),t._v(" 設定をインポートすると、それまでこのデバイス(ブラウザ)に保存されていた設定が、すべてインポート先の設定データで上書きされます。元に戻すことはできません。 ")]),e(ze.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",placeholder:"設定データ (KonomiTV-Settings.json) を選択",dense:t.is_form_dense,accept:"application/json","prepend-icon":"","prepend-inner-icon":"mdi-paperclip"},model:{value:t.import_settings_file,callback:function(e){t.import_settings_file=e},expression:"import_settings_file"}})],1),e(W.Z,{staticClass:"settings__save-button error mt-5",attrs:{depressed:""},on:{click:function(e){return t.importSettings()}}},[e("Icon",{staticClass:"mr-3",attrs:{icon:"fa6-solid:upload",height:"19px"}}),t._v("設定をインポート ")],1)],1)])},Be=[],Ke=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e(wt.Z,{staticClass:"settings-container d-flex px-5 py-5 mx-auto background",attrs:{elevation:"0",width:"100%","max-width":"1000"}},[e("div",[e(Ie.Z,{staticClass:"settings-navigation flex-shrink-0 background",attrs:{permanent:"",width:"195",height:"auto"}},[e(Ht.Z,{staticClass:"px-4"},[e(Lt.km,[e("h1",[t._v("設定")])])],1),e(Et.Z,{staticClass:"mt-2 px-0",attrs:{nav:""}},[e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/general"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 3px"},attrs:{icon:"fa-solid:sliders-h",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("全般")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/account"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("アカウント")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/jikkyo"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 2px"},attrs:{icon:"bi:chat-left-text-fill",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("ニコニコ実況")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/twitter"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{staticStyle:{padding:"0 1px"},attrs:{icon:"fa-brands:twitter",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("Twitter")])],1)],1),e(Ht.Z,{staticClass:"px-4",attrs:{link:"",color:"primary",to:"/settings/environment"}},[e(Se.Z,{staticClass:"mr-4"},[e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"26px"}})],1),e(Lt.km,[e(Lt.V9,[t._v("環境設定")])],1)],1)],1)],1)],1),e(wt.Z,{staticClass:"settings ml-5 px-7 py-7 background lighten-1",attrs:{width:"100%"}},[t._t("default")],2)],1)],1)],1)},Ee=[],He=n["default"].extend({name:"Settings-Base",components:{Header:lt,Navigation:pt}}),Le=He,Ve=(0,p.Z)(Le,Ke,Ee,!1,null,"03345d7e",null),Fe=Ve.exports,Re=n["default"].extend({name:"Settings-General",components:{Base:Fe},data(){return{PlayerUtils:z,is_form_dense:F.isSmartphoneHorizontal(),tv_streaming_quality:[{text:"1080p (60fps) (1時間あたり約3.24GB / 7.2Mbps)",value:"1080p-60fps"},{text:"1080p (1時間あたり約2.31GB / 5.1Mbps)",value:"1080p"},{text:"810p (1時間あたり約1.92GB / 4.2Mbps)",value:"810p"},{text:"720p (1時間あたり約1.33GB / 3.0Mbps)",value:"720p"},{text:"540p (1時間あたり約1.00GB / 2.2Mbps)",value:"540p"},{text:"480p (1時間あたり約0.74GB / 1.6Mbps)",value:"480p"},{text:"360p (1時間あたり約0.40GB / 0.9Mbps)",value:"360p"},{text:"240p (1時間あたり約0.23GB / 0.5Mbps)",value:"240p"}],panel_display_state:[{text:"前回の状態を復元する",value:"RestorePreviousState"},{text:"常に表示する",value:"AlwaysDisplay"},{text:"常に折りたたむ",value:"AlwaysFold"}],tv_panel_active_tab:[{text:"番組情報タブ",value:"Program"},{text:"チャンネルタブ",value:"Channel"},{text:"コメントタブ",value:"Comment"},{text:"Twitter タブ",value:"Twitter"}],caption_font:[{text:"Windows TV ゴシック",value:"Windows TV Gothic"},{text:"Windows TV 丸ゴシック",value:"Windows TV MaruGothic"},{text:"Windows TV 太丸ゴシック",value:"Windows TV FutoMaruGothic"},{text:"ヒラギノTV丸ゴ",value:"Hiragino TV Sans Rd S"},{text:"新丸ゴ ARIB",value:"TT-ShinMGo-regular"},{text:"Rounded M+ 1m for ARIB",value:"Rounded M+ 1m for ARIB"},{text:"Noto Sans JP",value:"Noto Sans JP Caption"},{text:"デフォルトのフォント",value:"sans-serif"}],capture_save_mode:[{text:"ブラウザでダウンロード",value:"Browser"},{text:"KonomiTV サーバーにアップロード",value:"UploadServer"},{text:"ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方行う",value:"Both"}],capture_caption_mode:[{text:"映像のみのキャプチャを保存する",value:"VideoOnly"},{text:"字幕を合成したキャプチャを保存する",value:"CompositingCaption"},{text:"映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する",value:"Both"}],import_settings_file:null,settings:(()=>{const t={},e=["tv_streaming_quality","tv_data_saver_mode","tv_low_latency_mode","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_copy_to_clipboard","capture_save_mode","capture_caption_mode"];for(const s of e)t[s]=F.getSettingsItem(s);return t})()}},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))F.setSettingsItem(t,e)}}},methods:{exportSettings(){const t=localStorage.getItem("KonomiTV-Settings")||JSON.stringify(F.default_settings),e=new Blob([t],{type:"application/json"});F.downloadBlobData(e,"KonomiTV-Settings.json"),this.$message.success("設定をエクスポートしました。")},async importSettings(){if(null!==this.import_settings_file)try{const t=JSON.parse(await this.import_settings_file.text());localStorage.setItem("KonomiTV-Settings",JSON.stringify(t)),await F.syncClientSettingsToServer(),this.$message.success("設定をインポートしました。"),window.setTimeout((()=>this.$router.go(0)),300)}catch(t){return void this.$message.error("設定データが不正なため、インポートできませんでした。")}else this.$message.error("インポートする設定データを選択してください!")}}}),Me=Re,Ue=(0,p.Z)(Me,Ne,Be,!1,null,null,null),qe=Ue.exports,Ge=s(5125),We=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fluent:person-20-filled",width:"25px"}}),e("span",{staticClass:"ml-2"},[t._v("アカウント")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[null===t.user?e("div",{staticClass:"account"},[e("div",{staticClass:"account-wrapper"},[e("img",{staticClass:"account__icon",attrs:{src:"/assets/images/account-icon-default.png"}}),e("div",{staticClass:"account__info"},[e("div",{staticClass:"account__info-name"},[e("span",{staticClass:"account__info-name-text"},[t._v("ログインしていません")])]),e("span",{staticClass:"account__info-id"},[t._v("Not logged in")])])]),e(W.Z,{staticClass:"account__login ml-auto",attrs:{color:"secondary",width:"140",height:"56",depressed:"",to:"/login/"}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-in"}}),t._v("ログイン ")],1)],1):t._e(),null!==t.user?e("div",{staticClass:"account"},[e("div",{staticClass:"account-wrapper"},[e("img",{staticClass:"account__icon",attrs:{src:t.user_icon_blob}}),e("div",{staticClass:"account__info"},[e("div",{staticClass:"account__info-name"},[e("span",{staticClass:"account__info-name-text"},[t._v(t._s(t.user.name))]),t.user.is_admin?e("span",{staticClass:"account__info-admin"},[t._v("管理者")]):t._e()]),e("span",{staticClass:"account__info-id"},[t._v("User ID: "+t._s(t.user.id))])])]),e(W.Z,{staticClass:"account__login ml-auto",attrs:{color:"secondary",width:"140",height:"56",depressed:""},on:{click:function(e){return t.logout()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-out"}}),t._v("ログアウト ")],1)],1):t._e(),!1===t.is_logged_in?e("div",{staticClass:"account-register"},[e("div",{staticClass:"account-register__heading"},[t._v(" KonomiTV アカウントにログインすると、"),e("br"),t._v("より便利な機能が使えます! ")]),e("div",{staticClass:"account-register__feature"},[e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"bi:chat-left-text-fill"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("ニコニコ実況にコメントする")]),e("span",{staticClass:"account-feature__info-text"},[t._v("テレビを見ながらニコニコ実況にコメントできます。別途、ニコニコアカウントとの連携が必要です。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fa-brands:twitter"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("Twitter 連携機能")]),e("span",{staticClass:"account-feature__info-text"},[t._v("テレビを見ながら Twitter にツイートしたり、検索したツイートをリアルタイムで表示できます。別途、Twitter アカウントとの連携が必要です。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fluent:arrow-sync-20-filled"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("設定をデバイス間で同期")]),e("span",{staticClass:"account-feature__info-text"},[t._v("ピン留めしたチャンネルなど、ブラウザに保存されている各種設定をブラウザやデバイスをまたいで同期できます。")])])],1),e("div",{staticClass:"account-feature"},[e("Icon",{staticClass:"account-feature__icon",attrs:{icon:"fa-solid:sliders-h"}}),e("div",{staticClass:"account-feature__info"},[e("span",{staticClass:"account-feature__info-heading"},[t._v("環境設定をブラウザから変更")]),e("span",{staticClass:"account-feature__info-text"},[t._v("管理者権限があれば、環境設定をブラウザから変更できます。一番最初に作成されたアカウントには、自動で管理者権限が付与されます。")])])],1)]),e("div",{staticClass:"account-register__description"},[t._v(" KonomiTV アカウントの作成に必要なものはユーザー名とパスワードだけです。"),e("br"),t._v(" アカウントはローカルにインストールした KonomiTV サーバーごとに保存されます。"),e("br"),t._v(" 外部のサービスには保存されませんので、ご安心ください。"),e("br")]),e(W.Z,{staticClass:"account-register__button",attrs:{color:"secondary",width:"100%","max-width":"250",height:"50",depressed:"",to:"/register/"}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-add-20-filled",height:"24"}}),t._v("アカウントを作成 ")],1)],1):t._e(),!0===t.is_logged_in?e("div",[e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"sync_settings"}},[t._v("設定をデバイス間で同期する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"sync_settings"}},[t._v(" KonomiTV の設定を、同じアカウントにログインしているデバイス同士で同期するかを設定します。"),e("br"),t._v(" 同期を有効にすると、同期が有効なデバイスすべてで同じ設定が使えます。ピン留めしたチャンネルやハッシュタグリストなども同期されます。"),e("br"),t._v(" ストリーミング画質やコメントの遅延時間など、デバイスごとに最適な設定が異なるものは、同期を有効にしたあとも引き続きこのデバイス(ブラウザ)のみに反映されます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"sync_settings",inset:"","hide-details":""},model:{value:t.sync_settings,callback:function(e){t.sync_settings=e},expression:"sync_settings"}})],1),e(Ct.Z,{attrs:{"max-width":"530"},model:{value:t.sync_settings_dialog,callback:function(e){t.sync_settings_dialog=e},expression:"sync_settings_dialog"}},[e(wt.Z,[e(yt.EB,{staticClass:"justify-center"},[t._v("設定データの競合")]),e(yt.ZB,[t._v(" このデバイスの設定と、サーバーに保存されている設定が競合しています。"),e("br"),t._v(" 一度上書きすると、元に戻すことはできません。慎重に選択してください。"),e("br")]),e("div",{staticClass:"d-flex flex-column px-4 pb-4"},[e(W.Z,{staticClass:"settings__save-button error--text text--lighten-1",attrs:{depressed:""},on:{click:function(e){return t.overrideServerSettingsFromClient()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:document-arrow-up-16-filled",height:"22px"}}),t._v(" サーバーに保存されている設定を、このデバイスの設定で上書きする ")],1),e(W.Z,{staticClass:"settings__save-button error--text text--lighten-1 mt-3",attrs:{depressed:""},on:{click:function(e){return t.overrideClientSettingsFromServer()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:document-arrow-down-16-filled",height:"22px"}}),t._v(" このデバイスの設定を、サーバーに保存されている設定で上書きする ")],1),e(W.Z,{staticClass:"settings__save-button mt-3",attrs:{depressed:""},on:{click:function(e){t.sync_settings_dialog=!1}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:dismiss-16-filled",height:"22px"}}),t._v(" キャンセル ")],1)],1)],1)],1),e(Ge.Z,{ref:"settings_username",staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("ユーザー名")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントのユーザー名を設定します。アルファベットだけでなく日本語や記号も使えます。"),e("br"),t._v(" 同じ KonomiTV サーバー上の他のアカウントと同じユーザー名には変更できません。"),e("br")]),e(Ut.Z,{staticClass:"settings__item-form",attrs:{outlined:"",placeholder:"ユーザー名",dense:t.is_form_dense,rules:[t.settings_username_validation]},model:{value:t.settings_username,callback:function(e){t.settings_username=e},expression:"settings_username"}})],1),e(W.Z,{staticClass:"settings__save-button",attrs:{depressed:""},on:{click:function(e){return t.updateAccountInfo("username")}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("ユーザー名を更新 ")],1),e(Ge.Z,{staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("アイコン画像")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントのアイコン画像を設定します。"),e("br"),t._v(" アップロードされた画像は自動的に 400×400 の正方形にリサイズされます。"),e("br")]),e(ze.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",placeholder:"アイコン画像を選択",dense:t.is_form_dense,accept:"image/jpeg, image/png","prepend-icon":"","prepend-inner-icon":"mdi-paperclip"},model:{value:t.settings_icon,callback:function(e){t.settings_icon=e},expression:"settings_icon"}})],1),e(W.Z,{staticClass:"settings__save-button mt-5",attrs:{depressed:""},on:{click:function(e){return t.updateAccountIcon()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("アイコン画像を更新 ")],1),e(Ge.Z,{ref:"settings_password",staticClass:"settings__item",on:{submit:function(t){t.preventDefault()}}},[e("div",{staticClass:"settings__item-heading"},[t._v("新しいパスワード")]),e("div",{staticClass:"settings__item-label"},[t._v(" KonomiTV アカウントの新しいパスワードを設定します。"),e("br")]),e(Ut.Z,{staticClass:"settings__item-form",attrs:{outlined:"",placeholder:"新しいパスワード",dense:t.is_form_dense,type:t.settings_password_showing?"text":"password","append-icon":t.settings_password_showing?"mdi-eye":"mdi-eye-off",rules:[t.settings_password_validation]},on:{"click:append":function(e){t.settings_password_showing=!t.settings_password_showing}},model:{value:t.settings_password,callback:function(e){t.settings_password=e},expression:"settings_password"}})],1),e(W.Z,{staticClass:"settings__save-button",attrs:{depressed:""},on:{click:function(e){return t.updateAccountInfo("password")}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:save-16-filled",height:"24px"}}),t._v("パスワードを更新 ")],1),e(Oe.Z,{staticClass:"mt-6"}),e("div",{staticClass:"settings__item mt-6"},[e("div",{staticClass:"settings__item-heading error--text text--lighten-1"},[t._v("アカウントを削除")]),e("div",{staticClass:"settings__item-label"},[t._v(" 現在ログインしている KonomiTV アカウントを削除します。"),e("br"),t._v(" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。"),e("br")])]),e(Ct.Z,{attrs:{"max-width":"385"},scopedSlots:t._u([{key:"activator",fn:function({on:s,attrs:i}){return[e(W.Z,t._g(t._b({staticClass:"settings__save-button error mt-5",attrs:{depressed:""}},"v-btn",i,!1),s),[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:delete-16-filled",height:"24px"}}),t._v("アカウントを削除 ")],1)]}}],null,!1,974850237),model:{value:t.account_delete_confirm_dialog,callback:function(e){t.account_delete_confirm_dialog=e},expression:"account_delete_confirm_dialog"}},[e(wt.Z,[e(yt.EB,{staticClass:"justify-center"},[t._v("本当にアカウントを削除しますか?")]),e(yt.ZB,[t._v(" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。"),e("br"),t._v(" 本当にアカウントを削除しますか? ")]),e(yt.h7,[e(Q.Z),e(W.Z,{attrs:{color:"text",text:""},on:{click:function(e){t.account_delete_confirm_dialog=!1}}},[t._v("キャンセル")]),e(W.Z,{attrs:{color:"error",text:""},on:{click:function(e){return t.deleteAccount()}}},[t._v("削除")])],1)],1)],1)],1):t._e()])])},Ye=[],Xe=n["default"].extend({name:"Settings-Account",components:{Base:Fe},data(){return{Utils:F,is_form_dense:F.isSmartphoneHorizontal(),is_loading:!0,is_logged_in:null!==F.getAccessToken(),user:null,user_icon_blob:"",settings_username:null,settings_username_validation:t=>""===t||null===t?"ユーザー名を入力してください。":!1!==/^.{2,}$/.test(t)||"ユーザー名は2文字以上で入力してください。",settings_password:null,settings_password_showing:!0,settings_password_validation:t=>""===t||null===t?"パスワードを入力してください。":!1!==/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(t)||"パスワードは4文字以上の半角英数記号を入力してください。",settings_icon:null,account_delete_confirm_dialog:null,sync_settings:F.getSettingsItem("sync_settings"),sync_settings_dialog:!1}},async created(){await this.syncAccountInfo(),this.is_loading=!1},watch:{async sync_settings(){if(!0===this.sync_settings&&!1===this.sync_settings_dialog)try{null===localStorage.getItem("KonomiTV-Settings")&&localStorage.setItem("KonomiTV-Settings",JSON.stringify(F.default_settings));const t=JSON.parse(localStorage.getItem("KonomiTV-Settings")),e={};for(const a of F.sync_settings_keys)e[a]=a in t?t[a]:F.default_settings[a];const s=JSON.stringify(e),i=JSON.stringify((await n["default"].axios.get("/settings/client")).data);s!==i?(this.sync_settings_dialog=!0,this.sync_settings=!1):F.setSettingsItem("sync_settings",!0)}catch(t){this.$message.error(`サーバーから設定データを取得できませんでした。(HTTP Error ${t.response.status})`)}else!1===this.sync_settings&&!1===this.sync_settings_dialog&&F.setSettingsItem("sync_settings",!1)}},methods:{async overrideServerSettingsFromClient(){await F.syncClientSettingsToServer(!0),this.sync_settings=!0,F.setSettingsItem("sync_settings",!0),this.sync_settings_dialog=!1},async overrideClientSettingsFromServer(){await F.syncServerSettingsToClient(!0),this.sync_settings=!0,F.setSettingsItem("sync_settings",!0),this.sync_settings_dialog=!1},async syncAccountInfo(){try{const t=await n["default"].axios.get("/users/me");this.user=t.data,this.settings_username=this.user.name,await this.syncAccountIcon()}catch(t){R.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(console.log("Not logged in."),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",F.deleteAccessToken())}},async syncAccountIcon(){const t=await n["default"].axios.get("/users/me/icon",{responseType:"arraybuffer"});this.user_icon_blob=URL.createObjectURL(new Blob([t.data],{type:"image/png"}))},async updateAccountInfo(t){if("username"===t){if(!1===this.$refs.settings_username.validate())return}else if(!1===this.$refs.settings_password.validate())return;try{"username"===t?(await n["default"].axios.put("/users/me",{username:this.settings_username}),this.$message.show("ユーザー名を更新しました。")):(await n["default"].axios.put("/users/me",{password:this.settings_password}),this.$message.show("パスワードを更新しました。")),await this.syncAccountInfo()}catch(e){if(R.ZP.isAxiosError(e)&&e.response&&422===e.response.status)switch(e.response.data.detail){case"Specified username is duplicated":this.$message.error("ユーザー名が重複しています。");break;case"Specified username is not accepted due to system limitations":this.$message.error("ユーザー名に token と me は使えません。");break;default:this.$message.error(`アカウント情報を更新できませんでした。(HTTP Error ${e.response.status})`);break}}},async updateAccountIcon(){if(null===this.settings_icon)return void this.$message.error("アップロードする画像を選択してください!");const t=new FormData;t.append("image",this.settings_icon);try{await n["default"].axios.put("/users/me/icon",t,{headers:{"Content-Type":"multipart/form-data"}}),await this.syncAccountIcon()}catch(e){if(R.ZP.isAxiosError(e)&&e.response&&422===e.response.status)switch(e.response.data.detail){case"Please upload JPEG or PNG image":this.$message.error("JPEG または PNG 画像をアップロードしてください。");break;default:this.$message.error(`アイコン画像を更新できませんでした。(HTTP Error ${e.response.status})`);break}}},async deleteAccount(){this.account_delete_confirm_dialog=!1,await n["default"].axios["delete"]("/users/me"),F.setSettingsItem("sync_settings",!1),F.deleteAccessToken(),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",this.$message.show("アカウントを削除しました。")},logout(){F.setSettingsItem("sync_settings",!1),F.deleteAccessToken(),this.is_logged_in=!1,this.user=null,this.user_icon_blob="",this.$message.success("ログアウトしました。")}}}),Je=Xe,Qe=(0,p.Z)(Je,We,Ye,!1,null,"12036e32",null),ts=Qe.exports,es=s(7414),ss=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"bi:chat-left-text-fill",width:"19px"}}),e("span",{staticClass:"ml-3"},[t._v("ニコニコ実況")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[null===t.user.niconico_user_id?e("div",{staticClass:"niconico-account niconico-account--anonymous"},[e("div",{staticClass:"niconico-account-wrapper"},[e("Icon",{staticClass:"flex-shrink-0",attrs:{icon:"bi:chat-left-text-fill",width:"45px"}}),e("div",{staticClass:"niconico-account__info ml-4"},[e("div",{staticClass:"niconico-account__info-name"},[e("span",{staticClass:"niconico-account__info-name-text"},[t._v("ニコニコアカウントと連携していません")])]),e("span",{staticClass:"niconico-account__info-description"},[t._v(" ニコニコアカウントと連携すると、テレビを見ながらニコニコ実況にコメントできるようになります。 ")])])],1),e(W.Z,{staticClass:"niconico-account__login ml-auto",attrs:{color:"secondary",width:"130",height:"56",depressed:""},on:{click:function(e){return t.loginNiconicoAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-connected-20-filled",height:"26"}}),t._v("連携する ")],1)],1):t._e(),null!==t.user.niconico_user_id?e("div",{staticClass:"niconico-account"},[e("div",{staticClass:"niconico-account-wrapper"},[e("img",{staticClass:"niconico-account__icon",attrs:{src:this.niconico_user_icon_url}}),e("div",{staticClass:"niconico-account__info"},[e("div",{staticClass:"niconico-account__info-name"},[e("span",{staticClass:"niconico-account__info-name-text"},[t._v(t._s(t.user.niconico_user_name)+" と連携しています")])]),e("span",{staticClass:"niconico-account__info-description"},[e("span",{staticClass:"mr-2"},[t._v("Niconico User ID:")]),e("a",{staticClass:"mr-2",attrs:{href:`https://www.nicovideo.jp/user/${t.user.niconico_user_id}`,target:"_blank"}},[t._v(t._s(t.user.niconico_user_id))]),1==t.user.niconico_user_premium?e("span",{staticClass:"secondary--text"},[t._v("(Premium)")]):t._e()])])]),e(W.Z,{staticClass:"niconico-account__login ml-auto",attrs:{color:"secondary",width:"130",height:"56",depressed:""},on:{click:function(e){return t.logoutNiconicoAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-disconnected-20-filled",height:"26"}}),t._v("連携解除 ")],1)],1):t._e(),e("div",{staticClass:"settings__item mt-7"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントのミュート設定")]),e("div",{staticClass:"settings__item-label"},[t._v(" 表示したくないコメントを、画面やコメントリストに表示しないようにミュートできます。"),e("br")])]),e(W.Z,{staticClass:"settings__save-button mt-4",attrs:{depressed:""},on:{click:function(e){t.comment_mute_settings_modal=!t.comment_mute_settings_modal}}},[e("Icon",{attrs:{icon:"heroicons-solid:filter",height:"19px"}}),e("span",{staticClass:"ml-1"},[t._v("コメントのミュート設定を開く")])],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの速さ")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーに流れるコメントの速さを設定します。"),e("br"),t._v(" たとえば 1.2 に設定すると、コメントが 1.2 倍速く流れます。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",step:.1,min:.5,max:2},model:{value:t.settings.comment_speed_rate,callback:function(e){t.$set(t.settings,"comment_speed_rate",e)},expression:"settings.comment_speed_rate"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの文字サイズ")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーに流れるコメントの文字サイズの基準値を設定します。"),e("br"),t._v(" 実際の文字サイズは画面の大きさに合わせて調整されます。既定の文字サイズは 34px です。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",min:20,max:60},model:{value:t.settings.comment_font_size,callback:function(e){t.$set(t.settings,"comment_font_size",e)},expression:"settings.comment_font_size"}})],1),e("div",{staticClass:"settings__item settings__item--sync-disabled"},[e("div",{staticClass:"settings__item-heading"},[t._v("コメントの遅延時間")]),e("div",{staticClass:"settings__item-label"},[t._v(" プレイヤーやコメントリストに表示されるコメントを何秒遅らせて反映するかを設定します。"),e("br"),t._v(" 通常は 1.75 秒程度で大丈夫です。ネットワークが遅いなどでタイムラグが大きいときだけ、映像の遅延に合わせて調整してください。"),e("br")]),e(es.Z,{staticClass:"settings__item-form",attrs:{ticks:"always","thumb-label":"","hide-details":"",step:.25,min:0,max:10},model:{value:t.settings.comment_delay_time,callback:function(e){t.$set(t.settings,"comment_delay_time",e)},expression:"settings.comment_delay_time"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"close_comment_form_after_sending"}},[t._v("コメント送信後にコメント入力フォームを閉じる")]),e("label",{staticClass:"settings__item-label",attrs:{for:"close_comment_form_after_sending"}},[t._v(" コメントを送信したあとに、コメント入力フォームを自動的に閉じるかを設定します。"),e("br"),t._v(" 基本的にはオンのままにしておくことをおすすめします。コメント入力フォームが表示されたままだと、大部分のショートカットキーが文字入力と競合して使えないためです。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"close_comment_form_after_sending",inset:"","hide-details":""},model:{value:t.settings.close_comment_form_after_sending,callback:function(e){t.$set(t.settings,"close_comment_form_after_sending",e)},expression:"settings.close_comment_form_after_sending"}})],1)],1),e("CommentMuteSettings",{model:{value:t.comment_mute_settings_modal,callback:function(e){t.comment_mute_settings_modal=e},expression:"comment_mute_settings_modal"}})],1)},is=[],as=n["default"].extend({name:"Settings-Jikkyo",components:{Base:Fe,CommentMuteSettings:Jt},data(){return{Utils:F,comment_mute_settings_modal:!1,is_loading:!0,is_logged_in:null!==F.getAccessToken(),user:null,niconico_user_icon_url:"",settings:(()=>{const t={},e=["comment_speed_rate","comment_font_size","comment_delay_time","close_comment_form_after_sending"];for(const s of e)t[s]=F.getSettingsItem(s);return t})()}},async created(){this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&await this.syncAccountInfo(),this.is_loading=!1},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))F.setSettingsItem(t,e)}}},methods:{async syncAccountInfo(){try{const t=await n["default"].axios.get("/users/me");if(this.user=t.data,null!==this.user.niconico_user_id){const t=this.user.niconico_user_id.toString().slice(0,4);this.niconico_user_icon_url=`https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/${t}/${this.user.niconico_user_id}.jpg`}}catch(t){R.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},async loginNiconicoAccount(){if(!1===this.is_logged_in)return void this.$message.warning("連携をはじめるには、KonomiTV アカウントにログインしてください。");const t=(await n["default"].axios.get("/niconico/auth")).data.authorization_url,e=window.open(t,"KonomiTV-OAuthPopup",F.getWindowFeatures()),s=async t=>{if(e.closed)return;if("object"!==F["typeof"](t.data))return;if("KonomiTV-OAuthPopup"in t.data===!1)return;e&&e.close(),window.removeEventListener("message",s);const i=t.data["KonomiTV-OAuthPopup"]["status"],a=t.data["KonomiTV-OAuthPopup"]["detail"];if(console.log(`NiconicoAuthCallbackAPI: Status: ${i} / Detail: ${a}`),200===i)await this.syncAccountInfo(),this.$message.success("ニコニコアカウントと連携しました。");else if(a.startsWith("Authorization was denied (access_denied)"))this.$message.error("ニコニコアカウントとの連携がキャンセルされました。");else if(a.startsWith("Failed to get access token (HTTP Error ")){const t=a.replace("Failed to get access token ","");this.$message.error(`アクセストークンの取得に失敗しました。${t}`)}else if(a.startsWith("Failed to get access token (Connection Timeout)"))this.$message.error("アクセストークンの取得に失敗しました。ニコニコで障害が発生している可能性があります。");else if(a.startsWith("Failed to get user information (HTTP Error ")){const t=a.replace("Failed to get user information ","");this.$message.error(`ニコニコアカウントのユーザー情報の取得に失敗しました。${t}`)}else a.startsWith("Failed to get user information (Connection Timeout)")?this.$message.error("ニコニコアカウントのユーザー情報の取得に失敗しました。ニコニコで障害が発生している可能性があります。"):this.$message.error(`ニコニコアカウントとの連携に失敗しました。(${a})`)};window.addEventListener("message",s)},async logoutNiconicoAccount(){await n["default"].axios["delete"]("/niconico/logout"),await this.syncAccountInfo(),this.$message.success("ニコニコアカウントとの連携を解除しました。")}}}),ns=as,os=(0,p.Z)(ns,ss,is,!1,null,"786083d5",null),rs=os.exports,ls=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fa-brands:twitter",width:"22px"}}),e("span",{staticClass:"ml-3"},[t._v("Twitter")])],1),e("div",{staticClass:"settings__content",class:{"settings__content--loading":t.is_loading}},[e("div",{staticClass:"twitter-accounts"},[t.user.twitter_accounts.length>0?e("div",{staticClass:"twitter-accounts__heading"},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-board-20-filled",height:"30"}}),t._v("連携中のアカウント ")],1):t._e(),0===t.user.twitter_accounts.length?e("div",{staticClass:"twitter-accounts__guide"},[e("Icon",{staticClass:"flex-shrink-0",attrs:{icon:"fa-brands:twitter",width:"45px"}}),e("div",{staticClass:"ml-4"},[e("div",{staticClass:"font-weight-bold text-h6"},[t._v("Twitter アカウントと連携していません")]),e("div",{staticClass:"text--text text--darken-1 text-subtitle-2 mt-1"},[t._v(" Twitter アカウントと連携すると、テレビを見ながら Twitter にツイートしたり、ほかの実況ツイートをリアルタイムで表示できるようになります。 ")])])],1):t._e(),t._l(t.user.twitter_accounts,(function(s){return e("div",{key:s.id,staticClass:"twitter-account"},[e("img",{staticClass:"twitter-account__icon",attrs:{src:s.icon_url}}),e("div",{staticClass:"twitter-account__info"},[e("div",{staticClass:"twitter-account__info-name"},[e("span",{staticClass:"twitter-account__info-name-text"},[t._v(t._s(s.name))])]),e("span",{staticClass:"twitter-account__info-screen-name"},[t._v("@"+t._s(s.screen_name))])]),e(W.Z,{staticClass:"twitter-account__logout ml-auto",attrs:{width:"124",height:"52",depressed:""},on:{click:function(e){return t.logoutTwitterAccount(s.screen_name)}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-disconnected-20-filled",height:"24"}}),t._v("連携解除 ")],1)],1)})),e(W.Z,{staticClass:"twitter-account__login",attrs:{color:"secondary","max-width":"250",height:"50",depressed:""},on:{click:function(e){return t.loginTwitterAccount()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:plug-connected-20-filled",height:"24"}}),t._v("連携するアカウントを追加 ")],1)],2),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"fold_panel_after_sending_tweet"}},[t._v("ツイート送信後にパネルを閉じる")]),e("label",{staticClass:"settings__item-label",attrs:{for:"fold_panel_after_sending_tweet"}},[t._v(" ツイートを送信した後に、表示中のパネルを閉じる(折りたたむ)かを設定します。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"fold_panel_after_sending_tweet",inset:"","hide-details":""},model:{value:t.settings.fold_panel_after_sending_tweet,callback:function(e){t.$set(t.settings,"fold_panel_after_sending_tweet",e)},expression:"settings.fold_panel_after_sending_tweet"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"reset_hashtag_when_program_switches"}},[t._v("番組が切り替わったときにハッシュタグフォームをリセットする")]),e("label",{staticClass:"settings__item-label",attrs:{for:"reset_hashtag_when_program_switches"}},[t._v(" チャンネルを切り替えたときや、視聴中の番組が終了し次の番組の放送が開始されたときに、ハッシュタグフォームをリセットするかを設定します。"),e("br"),t._v(" この設定をオンにしておけば、「誤って前番組のハッシュタグをつけたまま次番組の実況ツイートをしてしまう」といったミスを回避できます。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"reset_hashtag_when_program_switches",inset:"","hide-details":""},model:{value:t.settings.reset_hashtag_when_program_switches,callback:function(e){t.$set(t.settings,"reset_hashtag_when_program_switches",e)},expression:"settings.reset_hashtag_when_program_switches"}})],1),e("div",{staticClass:"settings__item settings__item--switch"},[e("label",{staticClass:"settings__item-heading",attrs:{for:"auto_add_watching_channel_hashtag"}},[t._v("視聴中のチャンネルに対応する局タグを自動的に追加する")]),e("label",{staticClass:"settings__item-label",attrs:{for:"auto_add_watching_channel_hashtag"}},[t._v(" ハッシュタグフォームに、視聴中のチャンネルに対応する局タグ (#nhk, #tokyomx など) を自動的に追加するかを設定します。"),e("br"),t._v(" 現時点で、局タグは三大首都圏の地上波・BS の一部チャンネル・AT-X にのみ対応しています。"),e("br")]),e(Mt.Z,{staticClass:"settings__item-switch",attrs:{id:"auto_add_watching_channel_hashtag",inset:"","hide-details":""},model:{value:t.settings.auto_add_watching_channel_hashtag,callback:function(e){t.$set(t.settings,"auto_add_watching_channel_hashtag",e)},expression:"settings.auto_add_watching_channel_hashtag"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("既定で表示される Twitter タブ内のタブ")]),e("div",{staticClass:"settings__item-label"},[t._v(" 視聴画面を開いたときに、パネルの Twitter タブの中で最初に表示されるタブを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.twitter_active_tab},model:{value:t.settings.twitter_active_tab,callback:function(e){t.$set(t.settings,"twitter_active_tab",e)},expression:"settings.twitter_active_tab"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("ツイートにつけるハッシュタグの位置")]),e("div",{staticClass:"settings__item-label"},[t._v(" ツイート本文から見て、ハッシュタグをどの位置につけてツイートするかを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tweet_hashtag_position},model:{value:t.settings.tweet_hashtag_position,callback:function(e){t.$set(t.settings,"tweet_hashtag_position",e)},expression:"settings.tweet_hashtag_position"}})],1),e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("ツイートするキャプチャに番組タイトルの透かしを描画する")]),e("div",{staticClass:"settings__item-label"},[t._v(" ツイートするキャプチャに、視聴中の番組タイトルの透かしを描画するかを設定します。"),e("br")]),e(Rt.Z,{staticClass:"settings__item-form",attrs:{outlined:"","hide-details":"",dense:t.is_form_dense,items:t.tweet_capture_watermark_position},model:{value:t.settings.tweet_capture_watermark_position,callback:function(e){t.$set(t.settings,"tweet_capture_watermark_position",e)},expression:"settings.tweet_capture_watermark_position"}})],1)])])},cs=[],_s=n["default"].extend({name:"Settings-Twitter",components:{Base:Fe},data(){return{is_form_dense:F.isSmartphoneHorizontal(),twitter_active_tab:[{text:"ツイート検索タブ",value:"Search"},{text:"タイムラインタブ",value:"Timeline"},{text:"キャプチャタブ",value:"Capture"}],tweet_hashtag_position:[{text:"ツイート本文の前に追加する",value:"Prepend"},{text:"ツイート本文の後に追加する",value:"Append"},{text:"ツイート本文の前に追加してから改行する",value:"PrependWithLineBreak"},{text:"ツイート本文の後に改行してから追加する",value:"AppendWithLineBreak"}],tweet_capture_watermark_position:[{text:"透かしを描画しない",value:"None"},{text:"透かしをキャプチャの左上に描画する",value:"TopLeft"},{text:"透かしをキャプチャの右上に描画する",value:"TopRight"},{text:"透かしをキャプチャの左下に描画する",value:"BottomLeft"},{text:"透かしをキャプチャの右下に描画する",value:"BottomRight"}],is_loading:!0,is_logged_in:null!==F.getAccessToken(),user:null,settings:(()=>{const t={},e=["fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position"];for(const s of e)t[s]=F.getSettingsItem(s);return t})()}},async created(){this.user={id:0,name:"",is_admin:!0,niconico_user_id:null,niconico_user_name:null,niconico_user_premium:null,twitter_accounts:[],created_at:"",updated_at:""},!0===this.is_logged_in&&await this.syncAccountInfo(),this.is_loading=!1},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))F.setSettingsItem(t,e)}}},methods:{async syncAccountInfo(){try{this.user=(await n["default"].axios.get("/users/me")).data}catch(t){R.ZP.isAxiosError(t)&&t.response&&401===t.response.status&&(this.is_logged_in=!1,this.user=null)}},async loginTwitterAccount(){if(!1===this.is_logged_in)return void this.$message.warning("連携をはじめるには、KonomiTV アカウントにログインしてください。");const t=(await n["default"].axios.get("/twitter/auth")).data.authorization_url,e=window.open(t,"KonomiTV-OAuthPopup",F.getWindowFeatures()),s=async t=>{if(e.closed)return;if("object"!==F["typeof"](t.data))return;if("KonomiTV-OAuthPopup"in t.data===!1)return;e&&e.close(),window.removeEventListener("message",s);const i=t.data["KonomiTV-OAuthPopup"]["status"],a=t.data["KonomiTV-OAuthPopup"]["detail"];if(console.log(`TwitterAuthCallbackAPI: Status: ${i} / Detail: ${a}`),200!==i)return void(a.startsWith("Authorization was denied by user")?this.$message.error("Twitter アカウントとの連携がキャンセルされました。"):a.startsWith("Failed to get access token")?this.$message.error("アクセストークンの取得に失敗しました。"):a.startsWith("Failed to get user information")?this.$message.error("Twitter アカウントのユーザー情報の取得に失敗しました。"):this.$message.error(`Twitter アカウントとの連携に失敗しました。(${a})`));await this.syncAccountInfo();const n=[...this.user.twitter_accounts].sort(((t,e)=>t.updated_ate.updated_at?-1:0))[0];this.$message.success(`Twitter @${n.screen_name} と連携しました。`)};window.addEventListener("message",s)},async logoutTwitterAccount(t){await n["default"].axios["delete"](`/twitter/accounts/${t}`),await this.syncAccountInfo(),this.$message.success(`Twitter @${t} との連携を解除しました。`)}}}),ds=_s,ms=(0,p.Z)(ds,ls,cs,!1,null,"1970b264",null),us=ms.exports,ps=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("Base",[e("h2",{staticClass:"settings__heading"},[e("router-link",{directives:[{def:et.Z,name:"ripple",rawName:"v-ripple"}],staticClass:"settings__back-button",attrs:{to:"/settings/"}},[e("Icon",{attrs:{icon:"fluent:arrow-left-12-filled",width:"25px"}})],1),e("Icon",{attrs:{icon:"fluent:toolbox-20-filled",width:"22px"}}),e("span",{staticClass:"ml-3"},[t._v("環境設定")])],1),e("div",{staticClass:"settings__content"},[e("div",{staticClass:"settings__item"},[e("div",{staticClass:"settings__item-heading"},[t._v("鋭意開発中…")])])])])},hs=[],gs=n["default"].extend({name:"Settings-Environment",components:{Base:Fe},data(){return{settings:(()=>{const t={},e=[];for(const s of e)t[s]=F.getSettingsItem(s);return t})()}},watch:{settings:{deep:!0,handler(){for(const[t,e]of Object.entries(this.settings))F.setSettingsItem(t,e)}}}}),vs=gs,fs=(0,p.Z)(vs,ps,hs,!1,null,null,null),ws=fs.exports,ys=s(5495),bs=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"login-container-wrapper d-flex align-center w-100 mb-13"},[e(wt.Z,{staticClass:"login-container px-10 pt-8 pb-11 mx-auto background lighten-1",attrs:{elevation:"10",width:"100%","max-width":"450"}},[e(yt.EB,{staticClass:"login__logo justify-center pb-7"},[e(ys.Z,{attrs:{"max-width":"250",src:"/assets/images/logo.svg"}})],1),e(Oe.Z),e(Ge.Z,{ref:"login",on:{submit:function(t){t.preventDefault()}}},[e(Ut.Z,{staticClass:"mt-12",attrs:{outlined:"",placeholder:"ユーザー名","hide-details":"",autofocus:"",dense:t.is_form_dense},model:{value:t.username,callback:function(e){t.username=e},expression:"username"}}),e(Ut.Z,{staticClass:"mt-8",attrs:{outlined:"",placeholder:"パスワード","hide-details":"",dense:t.is_form_dense,type:t.password_showing?"text":"password","append-icon":t.password_showing?"mdi-eye":"mdi-eye-off"},on:{"click:append":function(e){t.password_showing=!t.password_showing}},model:{value:t.password,callback:function(e){t.password=e},expression:"password"}}),e(W.Z,{staticClass:"login-button mt-5",attrs:{color:"secondary",depressed:"",width:"100%",height:"56"},on:{click:function(e){return t.login()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fa:sign-in"}}),t._v("ログイン ")],1)],1)],1)],1)],1)],1)},Cs=[],xs=n["default"].extend({name:"Login",components:{Header:lt,Navigation:pt},data(){return{is_form_dense:F.isSmartphoneHorizontal(),username:"",password:"",password_showing:!1}},methods:{async login(){if(""!==this.username&&""!==this.password)try{const t=await n["default"].axios.post("/users/token",new URLSearchParams({username:this.username,password:this.password}));console.log("Login successful."),console.log(t.data),F.saveAccessToken(t.data.access_token),this.$message.success("ログインしました。"),await this.$router.push({path:"/settings/account"})}catch(t){if(R.ZP.isAxiosError(t)&&t.response&&401===t.response.status)switch(console.log("Failed to login."),console.log(t.response.data),t.response.data.detail){case"Incorrect username":this.$message.error("ログインできませんでした。そのユーザー名のアカウントは存在しません。");break;case"Incorrect password":this.$message.error("ログインできませんでした。パスワードを間違えていませんか?");break;default:this.$message.error(`ログインできませんでした。(HTTP Error ${t.response.status})`);break}}}}}),ks=xs,Os=(0,p.Z)(ks,bs,Cs,!1,null,"0c2bb32a",null),Ss=Os.exports,Is=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),e("div",{staticClass:"register-container-wrapper d-flex align-center w-100 mb-13"},[e(wt.Z,{staticClass:"register-container px-10 pt-8 pb-11 mx-auto background lighten-1",attrs:{elevation:"10",width:"100%","max-width":"450"}},[e(yt.EB,{staticClass:"register__logo flex-column justify-center"},[e(ys.Z,{attrs:{"max-width":"250",src:"/assets/images/logo.svg"}}),e("h4",{staticClass:"mt-10"},[t._v("アカウントを作成")])],1),e(Oe.Z),e(Ge.Z,{ref:"register",on:{submit:function(t){t.preventDefault()}}},[e(Ut.Z,{staticClass:"mt-10",attrs:{outlined:"",placeholder:"ユーザー名",autofocus:"",dense:t.is_form_dense,rules:[t.username_validation]},model:{value:t.username,callback:function(e){t.username=e},expression:"username"}}),e(Ut.Z,{staticClass:"mt-2",attrs:{outlined:"",placeholder:"パスワード",dense:t.is_form_dense,type:t.password_showing?"text":"password","append-icon":t.password_showing?"mdi-eye":"mdi-eye-off",rules:[t.password_validation]},on:{"click:append":function(e){t.password_showing=!t.password_showing}},model:{value:t.password,callback:function(e){t.password=e},expression:"password"}}),e(W.Z,{staticClass:"register-button mt-5",attrs:{color:"secondary",depressed:"",width:"100%",height:"56"},on:{click:function(e){return t.register()}}},[e("Icon",{staticClass:"mr-2",attrs:{icon:"fluent:person-add-20-filled",height:"24"}}),t._v("アカウントを作成 ")],1)],1)],1)],1)],1)],1)},Ts=[],js=n["default"].extend({name:"Register",components:{Header:lt,Navigation:pt},data(){return{is_form_dense:F.isSmartphoneHorizontal(),username:null,username_validation:t=>""===t||null===t?"ユーザー名を入力してください。":!1!==/^.{2,}$/.test(t)||"ユーザー名は2文字以上で入力してください。",password:null,password_showing:!0,password_validation:t=>""===t||null===t?"パスワードを入力してください。":!1!==/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(t)||"パスワードは4文字以上の半角英数記号を入力してください。"}},methods:{async register(){if(!1!==this.$refs.register.validate()){try{const t=await n["default"].axios.post("/users",{username:this.username,password:this.password});console.log("Account created."),console.log(t.data)}catch(t){if(R.ZP.isAxiosError(t)&&t.response&&422===t.response.status)switch(console.log("Failed to create account."),console.log(t.response.data),t.response.data.detail){case"Specified username is duplicated":this.$message.error("ユーザー名が重複しています。");break;case"Specified username is not accepted due to system limitations":this.$message.error("ユーザー名に token と me は使えません。");break;default:this.$message.error(`アカウントを作成できませんでした。(HTTP Error ${t.response.status})`);break}return}try{const t=await n["default"].axios.post("/users/token",new URLSearchParams({username:this.username,password:this.password}));console.log("Login successful."),console.log(t.data),F.saveAccessToken(t.data.access_token),this.$message.success("アカウントを作成しました。"),await this.$router.push({path:"/settings/account"})}catch(t){if(R.ZP.isAxiosError(t)&&t.response&&401===t.response.status)switch(console.log("Failed to login."),console.log(t.response.data),t.response.data.detail){case"Incorrect username":this.$message.error("ログインできませんでした。そのユーザー名のアカウントは存在しません。");break;case"Incorrect password":this.$message.error("ログインできませんでした。パスワードを間違えていませんか?");break;default:this.$message.error(`ログインできませんでした。(HTTP Error ${t.response.status})`);break}}}}}}),Ps=js,Zs=(0,p.Z)(Ps,Is,Ts,!1,null,"d0eaf0ae",null),As=Zs.exports,$s=function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"route-container"},[e("Header"),e("main",[e("Navigation"),t._m(0)],1)],1)},Ds=[function(){var t=this,e=t._self._c;t._self._setupProxy;return e("div",{staticClass:"d-flex justify-center align-center w-100"},[e("div",{staticClass:"d-flex justify-center align-center flex-column"},[e("h1",[t._v("Not Found, or Under Development...")]),e("span",{staticClass:"mt-4 text--text text--darken-1"},[t._v("お探しのページは存在しないか、鋭意開発中です。")])])])}],zs=n["default"].extend({name:"NotFound",components:{Header:lt,Navigation:pt}}),Ns=zs,Bs=(0,p.Z)(Ns,$s,Ds,!1,null,"daa4530a",null),Ks=Bs.exports;n["default"].use(J.ZP);const Es=new J.ZP({mode:"history",base:"/",routes:[{path:"/",redirect:"/tv/"},{path:"/tv/",name:"TV Home",component:ft},{path:"/tv/watch/:channel_id",name:"TV Watch",component:ke},{path:"/settings/",name:"Settings Index",component:$e,beforeEnter:(t,e,s)=>{F.isSmartphoneVertical()||F.isSmartphoneHorizontal()||F.isTabletVertical()?s():s({path:"/settings/general/"})}},{path:"/settings/general",name:"Settings General",component:qe},{path:"/settings/account",name:"Settings Account",component:ts},{path:"/settings/jikkyo",name:"Settings Jikkyo",component:rs},{path:"/settings/twitter",name:"Settings Twitter",component:us},{path:"/settings/environment",name:"Settings Environment",component:ws},{path:"/login/",name:"Login",component:Ss},{path:"/register/",name:"Register",component:As},{path:"*",name:"NotFound",component:Ks}],scrollBehavior(t,e,s){return s||{x:0,y:0}}});var Hs=Es,Ls=s(5205);(0,Ls.z)("/service-worker.js",{ready(){console.log("App is being served from cache by a service worker.\nFor more details, visit https://goo.gl/AFskqB")},registered(){console.log("Service worker has been registered.")},cached(){console.log("Content has been cached for offline use.")},updatefound(){console.log("New content is downloading.")},updated(){console.log("New content is available; please refresh.")},offline(){console.log("No internet connection found. App is running in offline mode.")},error(t){console.error("Error during service worker registration:",t)}}),(0,a.OK)(),n["default"].config.productionTip=!1,n["default"].config.devtools=!0,n["default"].use(o.Z,U),n["default"].use(r.ZP),n["default"].use(c(),{top:!1,bottom:!0,color:"#433532",dark:!0,elevation:8,timeout:2500,autoRemove:!0,closeButtonContent:"閉じる",vuetifyInstance:X});const Vs=F.isTouchDevice()?[]:["hover","focus","touch"];_.ZP.options.themes.tooltip.showTriggers=Vs,_.ZP.options.themes.tooltip.hideTriggers=Vs,_.ZP.options.themes.tooltip.delay.show=0,_.ZP.options.offset=[0,7],n["default"].use(_.ZP),n["default"].component("Icon",i.JO),n["default"].component("v-tab-item-fix",w),n["default"].component("v-tabs-fix",k),n["default"].component("v-tabs-items-fix",S),new n["default"]({router:Hs,vuetify:X,render:t=>t(v)}).$mount("#app"),window.setInterval((async()=>{null!==F.getAccessToken()&&!0===F.getSettingsItem("sync_settings")&&F.syncServerSettingsToClient()}),3e3)}},e={};function s(i){var a=e[i];if(void 0!==a)return a.exports;var n=e[i]={id:i,loaded:!1,exports:{}};return t[i].call(n.exports,n,n.exports,s),n.loaded=!0,n.exports}s.m=t,function(){s.amdO={}}(),function(){var t=[];s.O=function(e,i,a,n){if(!i){var o=1/0;for(_=0;_=n)&&Object.keys(s.O).every((function(t){return s.O[t](i[l])}))?i.splice(l--,1):(r=!1,n0&&t[_-1][2]>n;_--)t[_]=t[_-1];t[_]=[i,a,n]}}(),function(){s.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return s.d(e,{a:e}),e}}(),function(){s.d=function(t,e){for(var i in e)s.o(e,i)&&!s.o(t,i)&&Object.defineProperty(t,i,{enumerable:!0,get:e[i]})}}(),function(){s.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"===typeof window)return window}}()}(),function(){s.hmd=function(t){return t=Object.create(t),t.children||(t.children=[]),Object.defineProperty(t,"exports",{enumerable:!0,set:function(){throw new Error("ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: "+t.id)}}),t}}(),function(){s.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)}}(),function(){s.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}}(),function(){var t={143:0};s.O.j=function(e){return 0===t[e]};var e=function(e,i){var a,n,o=i[0],r=i[1],l=i[2],c=0;if(o.some((function(e){return 0!==t[e]}))){for(a in r)s.o(r,a)&&(s.m[a]=r[a]);if(l)var _=l(s)}for(e&&e(i);c {\n\n // VueComponent の key が一致する this.$slots.default 内の VNode を探す\n const index_a = this.$slots.default.findIndex((element) => {\n return a.$vnode.key === element.key;\n });\n const index_b = this.$slots.default.findIndex((element) => {\n return b.$vnode.key === element.key;\n });\n\n // index 順で並び替え\n return index_a - index_b;\n });\n\n item.$on('change', () => (this as any).onClick(item));\n if ((this as any).mandatory && !(this as any).selectedValues.length) {\n (this as any).updateMandatory();\n }\n\n // 追加された要素のソート後のインデックスを取得して更新する\n (this as any).updateItem(item, this.items.indexOf(item));\n\n // ソート後の現在アクティブなタブのインデックスを取得し直し、設定する\n // 配列の末尾以外に追加された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n },\n\n unregister(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 継承元の unregister() の処理を呼び出す(いわゆる super() )\n // ref: https://github.com/vuejs/vue/issues/2977\n (this.constructor as any).super.options.methods.unregister.call(this, item);\n\n // 配列の末尾以外から削除された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n }\n }\n});\n","\nimport { VueConstructor, VNode } from 'vue';\n\nimport { convertToUnit } from 'vuetify/lib/util/helpers'\nimport VTabs from 'vuetify/lib/components/VTabs/VTabs';\nimport VTabsBar from '@/components/VTabsBar';\n\nexport default (VTabs as VueConstructor).extend({\n methods: {\n\n // VTabsBar は VTabs から暗黙的に生成されるコンポーネントのため、直接上書きすることができない\n // そこで VTabs 自体も上書きし、VTabs で $createElement() される時の VTabsBar を自前でオーバーライドしたものに差し替える\n // ビルド済みのファイルには型定義が入っていないので any を多用せざるを得ない…\n genBar(items: VNode[], slider: VNode | null) {\n const data = {\n style: {\n height: convertToUnit((this as any).height),\n },\n props: {\n activeClass: (this as any).activeClass,\n centerActive: (this as any).centerActive,\n dark: (this as any).dark,\n light: (this as any).light,\n mandatory: !(this as any).optional,\n mobileBreakpoint: (this as any).mobileBreakpoint,\n nextIcon: (this as any).nextIcon,\n prevIcon: (this as any).prevIcon,\n showArrows: (this as any).showArrows,\n value: (this as any).internalValue,\n },\n on: {\n 'call:slider': (this as any).callSlider,\n change: (val: any) => {\n (this as any).internalValue = val;\n },\n },\n ref: 'items',\n };\n\n (this as any).setTextColor((this as any).computedColor, data);\n (this as any).setBackgroundColor((this as any).backgroundColor, data);\n\n // ここでオーバーライドした VTabsBar を使うのが最重要\n // これをやるためだけにわざわざ VTabs に関してもオーバーライドする羽目になってる…\n return (this as any).$createElement(VTabsBar, data, [\n (this as any).genSlider(slider),\n items,\n ]);\n }\n }\n});\n","\nimport { VueConstructor } from 'vue';\n\nimport { GroupableInstance } from 'vuetify/lib/components/VItemGroup/VItemGroup';\nimport VTabsItems from 'vuetify/lib/components/VTabs/VTabsItems';\n\n// VTabsItems は VItemGroup と VWindow を extend() して実装されている\nexport default (VTabsItems as VueConstructor).extend({\n data() {\n return {\n // 一応型定義をしておく\n items: [] as GroupableInstance[],\n }\n },\n methods: {\n\n // タブのデータ配列の先頭に新しい要素が追加されるとそのタブのアニメーションの向きが逆になるバグがあるので、VItemGroup 側の挙動をオーバーライドする\n // DOM 上も VNode 上も正しい順序で並んでいるが、this.items に関しては追加された順になっていてしまっていて齟齬が発生するのが原因\n // ref: https://github.com/vuetifyjs/vuetify/issues/13862\n register(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 要素を items に追加\n this.items.push(item);\n\n // this.$slots.default に VNode が、items には単に VueComponent が入っているので、事前に VNode の順番に合わせて並べ替える\n // こうすることで、追加された順ではなく元のデータ配列通りの順番になる\n this.items.sort((a, b) => {\n\n // VueComponent の key が一致する this.$slots.default 内の VNode を探す\n const index_a = this.$slots.default.findIndex((element) => {\n return a.$vnode.key === element.key;\n });\n const index_b = this.$slots.default.findIndex((element) => {\n return b.$vnode.key === element.key;\n });\n\n // index 順で並び替え\n return index_a - index_b;\n });\n\n item.$on('change', () => (this as any).onClick(item));\n if ((this as any).mandatory && !(this as any).selectedValues.length) {\n (this as any).updateMandatory();\n }\n\n // 追加された要素のソート後のインデックスを取得して更新する\n (this as any).updateItem(item, this.items.indexOf(item));\n\n // ソート後の現在アクティブなタブのインデックスを取得し直し、設定する\n // 配列の末尾以外に追加された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n // 値が異なるときだけ更新する\n // こうしないと、Safari で変なアニメーションがついてしまう\n if (this.items.indexOf(activeItem) !== (this as any).internalValue) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n }\n },\n\n unregister(item: GroupableInstance) {\n\n // 現在アクティブなタブの VueComponent を取得\n const activeItem = this.items[(this as any).internalIndex];\n\n // 継承元の unregister() の処理を呼び出す(いわゆる super() )\n // ref: https://github.com/vuejs/vue/issues/2977\n (this.constructor as any).super.options.methods.unregister.call(this, item);\n\n // 配列の末尾以外から削除された場合はインデックスが1つずつずれてしまうため、インデックスを設定し直す必要がある\n if (activeItem !== undefined) {\n (this as any).updateInternalValue(this.items.indexOf(activeItem));\n }\n },\n\n // 最初のタブから最後のタブに遷移するとアニメーションの向きが逆になるバグがあるので、VWindow 側の挙動をオーバーライドする\n // 本来は VCarousel 用の動作だが、VTabsItems も VWindow を継承しているので、それが適用されてしまっているらしい\n // ref: https://github.com/yuwu9145/vuetify/blob/master/packages/vuetify/src/components/VWindow/VWindow.ts#L239-L252\n updateReverse(val: number, oldVal: number) {\n\n const itemsLength = this.items.length;\n const lastIndex = itemsLength - 1;\n\n if (itemsLength <= 2) return val < oldVal;\n\n // continuous が false の時、常に val < oldVal の結果を返す\n if (!(this as any).continuous) return val < oldVal;\n\n if (val === lastIndex && oldVal === 0) {\n return true;\n } else if (val === 0 && oldVal === lastIndex) {\n return false;\n } else {\n return val < oldVal;\n }\n }\n }\n});\n","\nimport Vue from 'vue';\n\nimport { IMutedCommentKeywords } from '@/interface';\n\n/**\n * 共通ユーティリティ\n */\nexport default class Utils {\n\n // バージョン情報\n // ビルド時の環境変数 (vue.config.js に記載) から取得\n static readonly version: string = process.env.VUE_APP_VERSION;\n\n // バックエンドの API のベース URL\n static readonly api_base_url = (() => {\n if (process.env.NODE_ENV === 'development') {\n // デバッグ時はポートを 7000 に強制する\n return `${window.location.protocol}//${window.location.hostname}:7000/api`;\n } else {\n // ビルド後は同じポートを使う\n return `${window.location.protocol}//${window.location.host}/api`;\n }\n })();\n\n // デフォルトの設定値\n // (同期無効) とある項目は、デバイス間で同期するとかえって面倒なことになりそうなため同期されない設定\n // ここを変えたときはサーバー側の app.schemas.ClientSettings も変更すること\n static readonly default_settings = {\n\n // ***** 設定画面から直接変更できない設定値 *****\n\n // ピン留めしているチャンネルの ID (ex: gr011) が入るリスト\n pinned_channel_ids: [] as string[],\n\n // 前回視聴画面を開いた際にパネルが表示されていたかどうか (同期無効)\n showed_panel_last_time: true as boolean,\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID (同期無効)\n selected_twitter_account_id: null as number | null,\n\n // 保存している Twitter のハッシュタグが入るリスト\n saved_twitter_hashtags: [] as string[],\n\n // ***** 設定 → 全般 *****\n\n // テレビのストリーミング画質 (Default: 1080p) (同期無効)\n tv_streaming_quality: '1080p' as ('1080p-60fps' | '1080p' | '810p' | '720p' | '540p' | '480p' | '360p' | '240p'),\n\n // テレビを通信節約モードで視聴する (Default: オフ) (同期無効)\n tv_data_saver_mode: false as boolean,\n\n // テレビを低遅延で視聴する (Default: 低遅延で視聴する) (同期無効)\n tv_low_latency_mode: true as boolean,\n\n // テレビをみるときに文字スーパーを表示する (Default: 表示する)\n tv_show_superimpose: true as boolean,\n\n // 既定のパネルの表示状態 (Default: 前回の状態を復元する)\n panel_display_state: 'RestorePreviousState' as ('RestorePreviousState' | 'AlwaysDisplay' | 'AlwaysFold'),\n\n // テレビをみるときに既定で表示されるパネルのタブ (Default: 番組情報タブ)\n tv_panel_active_tab: 'Program' as ('Program' | 'Channel' | 'Comment' | 'Twitter'),\n\n // 字幕のフォント (Default: Windows TV 丸ゴシック)\n caption_font: 'Windows TV MaruGothic' as string,\n\n // 字幕の文字を常に縁取って描画する (Default: 常に縁取る)\n always_border_caption_text: true as boolean,\n\n // 字幕の背景色を指定する (Default: 指定しない)\n specify_caption_background_color: false as boolean,\n\n // 字幕の背景色 (Default: 不透明度が 50% の黒)\n caption_background_color: '#00000080' as string,\n\n // キャプチャをクリップボードにコピーする (Default: 無効) (同期無効)\n capture_copy_to_clipboard: false as boolean,\n\n // キャプチャの保存先 (Default: ブラウザでダウンロード)\n capture_save_mode: 'Browser' as ('Browser' | 'UploadServer' | 'Both'),\n\n // 字幕が表示されているときのキャプチャの保存モード (Default: 映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する)\n capture_caption_mode: 'Both' as ('VideoOnly' | 'CompositingCaption' | 'Both'),\n\n // ***** 設定 → アカウント *****\n\n // 設定を同期する (Default: 同期しない) (同期無効)\n sync_settings: false as boolean,\n\n // ***** 設定 → ニコニコ実況 *****\n\n // コメントの速さ (Default: 1倍)\n comment_speed_rate: 1 as number,\n\n // コメントのフォントサイズ (Default: 34px)\n comment_font_size: 34 as number,\n\n // コメントの遅延時間 (Default: 1.75秒) (同期無効)\n comment_delay_time: 1.75 as number,\n\n // コメント送信後にコメント入力フォームを閉じる (Default: オン)\n close_comment_form_after_sending: true as boolean,\n\n // ***** 設定 → ニコニコ実況 (ミュート設定) *****\n\n // ミュート済みのコメントのキーワードが入るリスト\n muted_comment_keywords: [] as IMutedCommentKeywords[],\n\n // ミュート済みのニコニコユーザー ID が入るリスト\n muted_niconico_user_ids: [] as string[],\n\n // 露骨な表現を含むコメントをミュートする (Default: ミュートする)\n mute_vulgar_comments: true as boolean,\n\n // 罵倒や誹謗中傷、差別的な表現、政治的に偏った表現を含むコメントをミュートする (Default: ミュートする)\n mute_abusive_discriminatory_prejudiced_comments: true as boolean,\n\n // 文字サイズが大きいコメントをミュートする (Default: ミュートする)\n mute_big_size_comments: true as boolean,\n\n // 映像の上下に固定表示されるコメントをミュートする (Default: ミュートしない)\n mute_fixed_comments: false as boolean,\n\n // 色付きのコメントをミュートする (Default: ミュートしない)\n mute_colored_comments: false as boolean,\n\n // 8文字以上同じ文字が連続しているコメントをミュートする (Default: ミュートしない)\n mute_consecutive_same_characters_comments: false as boolean,\n\n // ***** 設定 → Twitter *****\n\n // ツイート送信後にパネルを閉じる (Default: オフ)\n fold_panel_after_sending_tweet: false as boolean,\n\n // 番組が切り替わったときにハッシュタグフォームをリセットする (Default: オン)\n reset_hashtag_when_program_switches: true as boolean,\n\n // 視聴中のチャンネルに対応する局タグを自動的に追加する (Default: オン)\n auto_add_watching_channel_hashtag: true as boolean,\n\n // 既定で表示される Twitter タブ内のタブ (Default: キャプチャタブ)\n twitter_active_tab: 'Capture' as ('Search' | 'Timeline' | 'Capture'),\n\n // ツイートにつけるハッシュタグの位置 (Default: ツイート本文の後に追加する)\n tweet_hashtag_position: 'Append' as ('Prepend' | 'Append' | 'PrependWithLineBreak' | 'AppendWithLineBreak'),\n\n // ツイートするキャプチャに番組名の透かしを描画する (Default: 透かしを描画しない)\n tweet_capture_watermark_position: 'None' as ('None' | 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight'),\n };\n\n // 同期対象の設定キー\n // サーバー側の app.schemas.ClientSettings に定義されているものと同じ\n static readonly sync_settings_keys = [\n 'pinned_channel_ids',\n 'saved_twitter_hashtags',\n 'tv_show_superimpose',\n 'panel_display_state',\n 'tv_panel_active_tab',\n 'caption_font',\n 'always_border_caption_text',\n 'specify_caption_background_color',\n 'caption_background_color',\n 'capture_save_mode',\n 'capture_caption_mode',\n 'comment_speed_rate',\n 'comment_font_size',\n 'close_comment_form_after_sending',\n 'muted_comment_keywords',\n 'muted_niconico_user_ids',\n 'mute_vulgar_comments',\n 'mute_abusive_discriminatory_prejudiced_comments',\n 'mute_big_size_comments',\n 'mute_fixed_comments',\n 'mute_colored_comments',\n 'mute_consecutive_same_characters_comments',\n 'fold_panel_after_sending_tweet',\n 'reset_hashtag_when_program_switches',\n 'auto_add_watching_channel_hashtag',\n 'twitter_active_tab',\n 'tweet_hashtag_position',\n 'tweet_capture_watermark_position',\n ];\n\n // 設定をサーバーにアップロード中かどうか\n // これが true のときは、定期的なサーバーからの設定ダウンロードを行わない\n static uploading_settings: boolean = false;\n\n\n /**\n * 設定を LocalStorage から取得する\n * @param key 設定のキー名\n * @returns 設定されている値\n */\n static getSettingsItem(key: string): any | null {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n // データは JSON で管理し、LocalStorage 上の一つのキーにまとめる\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // そのキーが保存されているときだけ、設定値を返す\n if (key in settings) {\n return settings[key];\n } else {\n // デフォルトの設定値にあればそれを使う\n if (key in Utils.default_settings) {\n return Utils.default_settings[key];\n } else {\n return null;\n }\n }\n }\n\n\n /**\n * 設定を LocalStorage に保存する\n * @param key 設定のキー名\n * @param value 設定する値\n */\n static setSettingsItem(key: string, value: any): void {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // 設定値を新しい値で置き換え\n settings[key] = value;\n\n // (名前が変わった、廃止されたなどの理由で) 現在の default_settings に存在しない設定キーを排除した上で並び替え\n // 並び替えられていないと設定データの比較がうまくいかない\n const new_settings: {[key: string]: any} = {};\n for (const default_settings_key of Object.keys(Utils.default_settings)) {\n if (default_settings_key in settings) {\n new_settings[default_settings_key] = settings[default_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n new_settings[default_settings_key] = Utils.default_settings[default_settings_key];\n }\n }\n\n // LocalStorage に保存\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(new_settings));\n\n // 更新された設定をサーバーに同期 (同期有効時のみ)\n Utils.syncClientSettingsToServer();\n }\n\n\n /**\n * ログイン時かつ同期が有効な場合、サーバーに保存されている設定データをこのクライアントに同期する\n * @param force ログイン中なら同期が有効かに関わらず実行する (デフォルト: false)\n */\n static async syncServerSettingsToClient(force = false): Promise {\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // ログインしていない時、同期が無効なときは実行しない\n if (Utils.getAccessToken() === null || (settings.sync_settings === false && force === false)) {\n return;\n }\n\n // 設定データをアップロード中のときは、動作が競合しないように終わるまで待つ\n while (Utils.uploading_settings === true) {\n await Utils.sleep(0.1);\n }\n\n try {\n\n // サーバーから設定データをダウンロード\n const server_settings: {[key: string]: any} = (await Vue.axios.get('/settings/client')).data;\n\n // クライアントの設定値をサーバーからの設定値で上書き\n for (const [server_settings_key, server_settings_value] of Object.entries(server_settings)) {\n settings[server_settings_key] = server_settings_value;\n }\n\n // LocalStorage に保存\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(settings));\n\n } catch (error) {\n // 何らかの理由でエラーになったときは何もしない\n }\n }\n\n\n /**\n * ログイン時かつ同期が有効な場合、このクライアントの設定をサーバーに同期する\n * @param force ログイン中なら同期が有効かに関わらず実行する (デフォルト: false)\n */\n static async syncClientSettingsToServer(force = false): Promise {\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // ログインしていない時、同期が無効なときは実行しない\n if (Utils.getAccessToken() === null || (settings.sync_settings === false && force === false)) {\n return;\n }\n\n // 設定データのアップロード開始\n Utils.uploading_settings = true;\n\n // 同期対象の設定キーのみで設定データをまとめ直す\n // sync_settings には同期対象外の設定は含まれない\n const sync_settings: {[key: string]: any} = {};\n for (const sync_settings_key of Utils.sync_settings_keys) {\n if (sync_settings_key in settings) {\n sync_settings[sync_settings_key] = settings[sync_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n sync_settings[sync_settings_key] = Utils.default_settings[sync_settings_key];\n }\n }\n\n // サーバーに設定データをアップロード\n try {\n await Vue.axios.put('/settings/client', sync_settings);\n } catch (error) {\n // 何もしない\n }\n\n // 設定データのアップロード終了\n Utils.uploading_settings = false;\n }\n\n\n /**\n * アクセストークンを LocalStorage から取得する\n * @returns JWT アクセストークン(ログインしていない場合は null が返る)\n */\n static getAccessToken(): string | null {\n\n // LocalStorage の取得結果をそのまま返す\n // LocalStorage.getItem() はキーが存在しなければ(=ログインしていなければ)null を返す\n return localStorage.getItem('KonomiTV-AccessToken');\n }\n\n\n /**\n * アクセストークンを LocalStorage に保存する\n * @param access_token 発行された JWT アクセストークン\n */\n static saveAccessToken(access_token: string): void {\n\n // そのまま LocalStorage に保存\n localStorage.setItem('KonomiTV-AccessToken', access_token);\n }\n\n\n /**\n * アクセストークンを LocalStorage から削除する\n * アクセストークンを削除することで、ログアウト相当になる\n */\n static deleteAccessToken(): void {\n\n // LocalStorage に KonomiTV-AccessToken キーが存在しない\n if (localStorage.getItem('KonomiTV-AccessToken') === null) return;\n\n // KonomiTV-AccessToken キーを削除\n localStorage.removeItem('KonomiTV-AccessToken');\n }\n\n\n /**\n * ブラウザが実行されている OS に応じて、\"Ctrl\" または \"Cmd\" を返す\n * キーボードショートカットのコンビネーションキーの説明を OS によって分けるために使う\n * @returns ブラウザが実行されている OS が Mac なら Cmd を、それ以外なら Ctrl を返す\n */\n static CtrlOrCmd(): 'Ctrl' | 'Cmd' {\n // iPhone・iPad で純正キーボードを接続した場合も一応想定して、iPhone・iPad も含める(動くかは未検証)\n return /iPhone|iPad|Macintosh/i.test(navigator.userAgent) ? 'Cmd' : 'Ctrl';\n }\n\n\n /**\n * Blob に格納されているデータをブラウザにダウンロードさせる\n * @param blob Blob オブジェクト\n * @param filename 保存するファイル名\n */\n static downloadBlobData(blob: Blob, filename: string): void {\n\n // Blob URL を発行\n const blob_url = URL.createObjectURL(blob);\n\n // 画像をダウンロード\n const link = document.createElement('a');\n link.download = filename;\n link.href = blob_url;\n link.click();\n\n // Blob URL を破棄\n URL.revokeObjectURL(blob_url);\n }\n\n\n /**\n * innerHTML しても問題ないように HTML の特殊文字をエスケープする\n * PHP の htmlspecialchars() と似たようなもの\n * @param content HTML エスケープされてないテキスト\n * @returns HTML エスケープされたテキスト\n */\n static escapeHTML(content: string): string {\n\n // HTML エスケープが必要な文字\n // ref: https://www.php.net/manual/ja/function.htmlspecialchars.php\n const html_escape_table = {\n '&': '&',\n '\"': '"',\n '\\'': ''',\n '<': '<',\n '>': '>',\n };\n\n // ref: https://qiita.com/noriaki/items/4bfef8d7cf85dc1035b3\n return content.replace(/[&\"'<>]/g, (match) => {\n return html_escape_table[match];\n });\n }\n\n\n /**\n * OAuth 連携時のポップアップを画面中央に表示するための windowFeatures 文字列を取得する\n * ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1\n * @returns window.open() で使う windowFeatures 文字列\n */\n static getWindowFeatures(): string {\n\n // ポップアップウインドウのサイズ\n const popupSizeWidth = 650;\n const popupSizeHeight = window.screen.height >= 800 ? 800 : window.screen.height - 100;\n\n // ポップアップウインドウの位置\n const posTop = (window.screen.height - popupSizeHeight) / 2;\n const posLeft = (window.screen.width - popupSizeWidth) / 2;\n\n return `toolbar=0,status=0,top=${posTop},left=${posLeft},width=${popupSizeWidth},height=${popupSizeHeight},modal=yes,alwaysRaised=yes`;\n }\n\n\n /**\n * 現在フォーカスを持っている要素に指定された CSS クラスが付与されているか\n * @param class_name 存在を確認する CSS クラス名\n * @returns document.activeElement が class_name で指定したクラスを持っているかどうか\n */\n static hasActiveElementClass(class_name: string): boolean {\n if (document.activeElement === null) return false;\n return document.activeElement.classList.contains(class_name);\n }\n\n\n /**\n * 表示画面がスマホ横画面かどうか\n * @returns スマホ横画面なら true を返す\n */\n static isSmartphoneHorizontal(): boolean {\n return window.matchMedia('(max-width: 1000px) and (max-height: 450px)').matches;\n }\n\n\n /**\n * 表示画面がスマホ縦画面かどうか\n * @returns スマホ縦画面なら true を返す\n */\n static isSmartphoneVertical(): boolean {\n return window.matchMedia('(max-width: 600px) and (min-height: 450.01px)').matches;\n }\n\n\n /**\n * 表示画面がタブレット横画面かどうか\n * @returns タブレット横画面なら true を返す\n */\n static isTabletHorizontal(): boolean {\n return window.matchMedia('(max-width: 1264px) and (max-height: 850px)').matches;\n }\n\n\n /**\n * 表示画面がタブレット縦画面かどうか\n * @returns タブレット縦画面なら true を返す\n */\n static isTabletVertical(): boolean {\n return window.matchMedia('(max-width: 850px) and (min-height: 850.01px)').matches;\n }\n\n\n /**\n * 表示端末がタッチデバイスかどうか\n * @returns タッチデバイスなら true を返す\n */\n static isTouchDevice(): boolean {\n return window.matchMedia('(hover: none)').matches;\n }\n\n\n /**\n * 任意の桁で切り捨てする\n * ref: https://qiita.com/nagito25/items/0293bc317067d9e6c560#comment-87f0855f388953843037\n * @param value 切り捨てする数値\n * @param base どの桁で切り捨てするか (-1 → 10の位 / 3 → 小数第3位)\n * @return 切り捨てした値\n */\n static mathFloor(value: number, base: number = 0): number {\n return Math.floor(value * (10**base)) / (10**base);\n }\n\n\n /**\n * async/await でスリープ的なもの\n * @param seconds 待機する秒数\n * @returns Promise を返すので、await sleep(1); のように使う\n */\n static async sleep(seconds: number): Promise {\n return await new Promise(resolve => setTimeout(resolve, seconds * 1000));\n }\n\n\n /**\n * 現在時刻の UNIX タイムスタンプを取得する (デバッグ用)\n * @returns 現在時刻の UNIX タイムスタンプ\n */\n static time(): number {\n return Date.now() / 1000;\n }\n\n\n /**\n * 指定された値の型の名前を取得する\n * ref: https://qiita.com/amamamaou/items/ef0b797156b324bb4ef3\n * @returns 指定された値の型の名前\n */\n static typeof(value: any): string {\n return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();\n }\n\n\n /**\n * 文字列中に含まれる URL をリンクの HTML に置き換える\n * @param text 置換対象の文字列\n * @returns URL をリンクに置換した文字列\n */\n static URLtoLink(text: string): string {\n\n // HTML の特殊文字で表示がバグらないように、事前に HTML エスケープしておく\n text = Utils.escapeHTML(text);\n\n // ref: https://www.softel.co.jp/blogs/tech/archives/6099\n const pattern = /(https?:\\/\\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig;\n return text.replace(pattern, '$1');\n }\n}\n","\nimport { ChannelType, ChannelTypePretty, IChannel } from '@/interface';\n\n/**\n * チャンネル周りのユーティリティ\n */\nexport class ChannelUtils {\n\n /**\n * チャンネル ID からチャンネルタイプを取得する\n * @param channel_id チャンネル ID\n * @param is_pretty ChannelTypePretty 型で返すかどうか\n * @returns チャンネルタイプ\n */\n static getChannelType(channel_id: string, is_pretty: boolean = false): ChannelType | ChannelTypePretty {\n const result = channel_id.match('(?[a-z]+)[0-9]+').groups.channel_type.toUpperCase();\n if (is_pretty === true) {\n switch (result) {\n case 'GR':\n return '地デジ';\n case 'STARDIGIO':\n return 'StarDigio';\n default:\n return result as ChannelTypePretty;\n }\n } else {\n return result as ChannelType;\n }\n }\n\n\n /**\n * チャンネルの実況勢いから「多」「激多」「祭」を取得する\n * ref: https://ja.wikipedia.org/wiki/%E3%83%8B%E3%82%B3%E3%83%8B%E3%82%B3%E5%AE%9F%E6%B3%81\n * @param channel_force チャンネルの実況勢い\n * @returns normal(普通)or many(多)or so-many(激多)or festival(祭)\n */\n static getChannelForceType(channel_force: number | null): 'normal' | 'many' | 'so-many' | 'festival' {\n\n // 実況勢いが null(=対応する実況チャンネルがない)\n if (channel_force === null) return 'normal';\n\n // 実況勢いが 1000 コメント以上(祭)\n if (channel_force >= 1000) return 'festival';\n // 実況勢いが 200 コメント以上(激多)\n if (channel_force >= 200) return 'so-many';\n // 実況勢いが 100 コメント以上(多)\n if (channel_force >= 100) return 'many';\n\n // それ以外\n return 'normal';\n }\n\n\n /**\n * チャンネルタイプとリモコン番号からチャンネル情報を取得する\n * @param channels_list チャンネルリスト\n * @param channel_type チャンネルタイプ\n * @param remocon_id リモコン番号\n * @returns チャンネル情報\n */\n static getChannelFromRemoconID(channels_list: Map, channel_type: ChannelType, remocon_id: number): IChannel | null {\n\n // ChannelTypePretty 型に変換する\n const channel_type_pretty = channel_type.replace('GR', '地デジ').replace('STARDIGIO', 'StarDigio') as ChannelTypePretty;\n\n // 指定されたチャンネルタイプのチャンネルを取得\n const channels = channels_list.get(channel_type_pretty); //「GR」は「地デジ」に置換してから取得\n\n // リモコン番号が一致するチャンネルを見つけ、一番最初に見つかったものを返す\n for (let index = 0; index < channels.length; index++) {\n const channel = channels[index];\n if (channel.remocon_id === remocon_id) {\n return channel;\n }\n }\n\n // リモコン番号が一致するチャンネルを見つけられなかった\n return null;\n }\n\n\n /**\n * 前・現在・次のチャンネル情報を取得する\n * @param channels_list チャンネルリスト\n * @param channel_id 起点にする現在のチャンネル ID\n * @returns 前・現在・次のチャンネル情報\n */\n static getPreviousAndCurrentAndNextChannel(channels_list: Map, channel_id: string): IChannel[] {\n\n // 前後のチャンネルを取得\n const channels = channels_list.get(this.getChannelType(channel_id, true) as ChannelTypePretty);\n for (let index = 0; index < channels.length; index++) {\n const element = channels[index];\n\n // チャンネル ID が一致したときだけ\n if (element.channel_id === channel_id) {\n\n // インデックスが最初か最後の時はそれぞれ最後と最初にインデックスを一周させる\n let previous_index = index - 1;\n if (previous_index === -1) previous_index = channels.length - 1;\n let next_index = index + 1;\n if (next_index === channels.length) next_index = 0;\n\n // 前・現在・次のチャンネル情報を返す\n return [channels[previous_index], channels[index], channels[next_index]];\n }\n }\n }\n}\n","\nimport { Buffer } from 'buffer';\nimport { convertBlobToPng, copyBlobToClipboard } from 'copy-image-clipboard';\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/ja';\nimport * as piexif from 'piexifjs';\nimport Vue from 'vue';\n\nimport { IChannel, ICaptureExifData, IProgram } from '@/interface';\nimport Utils from './Utils';\n\nexport class PlayerCaptureHandler {\n\n private player: any;\n private player_container: HTMLDivElement;\n private captured_callback: (blob: Blob, filename: string) => void;\n private capture_button: HTMLDivElement;\n private comment_capture_button: HTMLDivElement;\n private canvas: OffscreenCanvas | HTMLCanvasElement;\n private canvas_context: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;\n\n constructor(player: any, captured_callback: (blob: Blob, filename: string) => void) {\n\n this.player = player;\n this.player_container = this.player.container;\n this.captured_callback = captured_callback;\n\n // コメント付きキャプチャボタンの HTML を追加\n // insertAdjacentHTML で .dplayer-icons-right の一番左側に配置する\n // この後に通常のキャプチャボタンが insert されるので、実際は左から2番目\n // TODO: ボタンのデザインをコメント付きだと分かるようなものに変更する\n this.player_container.querySelector('.dplayer-icons.dplayer-icons-right').insertAdjacentHTML('afterbegin', `\n
\n \n \n \n
\n `);\n\n // キャプチャボタンの HTML を追加\n // 標準のスクリーンショット機能は貧弱なので、あえて独自に実装している(そのほうが自由度も高くてやりやすい)\n // insertAdjacentHTML で .dplayer-icons-right の一番左側に配置する\n this.player_container.querySelector('.dplayer-icons.dplayer-icons-right').insertAdjacentHTML('afterbegin', `\n
\n \n \n \n
\n `);\n\n this.comment_capture_button = this.player_container.querySelector('.dplayer-comment-capture-icon');\n this.capture_button = this.player_container.querySelector('.dplayer-capture-icon');\n\n // キャプチャ用の Canvas を初期化\n // パフォーマンス向上のため、一度作成した Canvas は使い回す\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n this.canvas = ('OffscreenCanvas' in window) ? new OffscreenCanvas(0, 0) : document.createElement('canvas');\n this.canvas_context = this.canvas.getContext('2d', {alpha: false, desynchronized: true, willReadFrequently: false});\n\n // 映像の解像度を Canvas サイズとして設定\n // 映像が読み込まれた / 画質が変わった際に Canvas のサイズを映像のサイズに合わせる\n this.canvas.width = 0;\n this.canvas.height = 0;\n player.on('loadedmetadata', async () => {\n this.canvas.width = player.video.videoWidth;\n this.canvas.height = player.video.videoHeight;\n // 映像サイズがちゃんと設定されるまで繰り返す (Safari 対策)\n while (this.canvas.width === 0 && this.canvas.height === 0) {\n await Utils.sleep(0.1);\n this.canvas.width = player.video.videoWidth;\n this.canvas.height = player.video.videoHeight;\n }\n });\n }\n\n\n /**\n * 映像をキャプチャして保存する\n * 映像のみと字幕付き (字幕表示時のみ) の両方のキャプチャを生成できる\n * @param channel チャンネル情報 (キャプチャの EXIF メタデータに番組情報を書き込むのに必要)\n * @param with_comments キャプチャにコメントを合成するかどうか\n */\n public async captureAndSave(channel: IChannel, with_comments: boolean): Promise {\n\n const total_time = Utils.time();\n\n // ***** バリデーション *****\n\n // ラジオチャンネルを視聴している場合 (当然映像がないのでキャプチャできない)\n if (channel.is_radiochannel === true) {\n this.player.notice('ラジオチャンネルはキャプチャできません。');\n return;\n }\n\n // まだ映像の表示準備が終わっていない (Canvas の幅/高さが 0 のまま)\n if (this.canvas.width === 0 && this.canvas.height === 0) {\n this.player.notice('読み込み中はキャプチャできません。');\n return;\n }\n\n // コメントが表示されていないのにコメント付きキャプチャしようとした\n if (with_comments === true && this.player.danmaku.showing === false) {\n this.player.notice('コメントを付けてキャプチャするには、コメント表示をオンにしてください。');\n return;\n }\n\n // ***** キャプチャの下準備 *****\n\n // キャプチャ中はキャプチャボタンをハイライトする\n this.addHighlight(with_comments);\n\n // ファイル名(拡張子なし)\n // TODO: ファイル名パターンを変更できるようにする\n const filename_base = `Capture_${dayjs().format('YYYYMMDD-HHmmss')}`;\n const filename = `${filename_base}.jpg`; // 字幕なしキャプチャ\n const filename_caption = `${filename_base}_caption.jpg`; // 字幕ありキャプチャ\n\n // 字幕・文字スーパーの Canvas を取得\n // getRawCanvas() で映像と同じ解像度の Canvas が取得できる\n const caption_canvas: HTMLCanvasElement = this.player.plugins.aribb24Caption.getRawCanvas();\n const superimpose_canvas: HTMLCanvasElement = this.player.plugins.aribb24Superimpose.getRawCanvas();\n\n // 字幕が表示されているか\n const is_caption_showing = (this.player.plugins.aribb24Caption.isShowing === true &&\n this.player.plugins.aribb24Caption.isPresent());\n\n // 文字スーパーが表示されているか\n const is_superimpose_showing = (this.player.plugins.aribb24Superimpose.isShowing === true &&\n this.player.plugins.aribb24Superimpose.isPresent());\n\n // 字幕が表示されている場合、表示中の字幕のテキストを取得\n // 取得した字幕のテキストは、キャプチャに字幕が合成されているかに関わらず、常に EXIF メタデータに書き込まれる\n // 字幕が表示されていない場合は null を入れ、キャプチャしたシーンで字幕が表示されていなかったことを明示する\n const caption_text = is_caption_showing ? (this.player.plugins.aribb24Caption.getTextContent() as string) : null;\n\n // エクスポートして保存する共通処理\n const export_and_save = async (\n canvas: OffscreenCanvas | HTMLCanvasElement,\n filename: string,\n program: IProgram,\n caption_text: string | null,\n is_caption_composited: boolean,\n is_comment_composited: boolean,\n ): Promise => {\n\n // Canvas を Blob にエクスポート\n const time = Utils.time();\n let blob: Blob;\n try {\n blob = await this.exportToBlob(canvas);\n } catch (error) {\n this.player.notice('キャプチャの保存に失敗しました…');\n return false;\n }\n console.log('[PlayerCaptureHandler] Export to Blob:', Utils.mathFloor(Utils.time() - time, 3), 'sec');\n\n // キャプチャに番組情報などのメタデータ (EXIF) をセット\n blob = await this.setEXIFDataToCapture(blob, program, caption_text, is_caption_composited, is_comment_composited);\n\n // キャプチャの保存先: ブラウザでダウンロード or 両方\n if (['Browser', 'Both'].includes(Utils.getSettingsItem('capture_save_mode'))) {\n Utils.downloadBlobData(blob, filename);\n }\n\n // キャプチャの保存先: KonomiTV サーバーにアップロード or 両方\n // 時間がかかるし完了を待つ必要がないので非同期\n if (['UploadServer', 'Both'].includes(Utils.getSettingsItem('capture_save_mode'))) {\n this.uploadCaptureToServer(blob, filename);\n }\n\n return blob;\n }\n\n // ***** 映像のキャプチャ *****\n\n // null はまだキャプチャしていないことを、false はキャプチャに失敗したことを表す\n let capture_normal: {blob: Blob, filename: string} | null | false = null;\n let capture_caption: {blob: Blob, filename: string} | null | false = null;\n\n // 映像の ImageBitmap を取得\n const image_bitmap = await createImageBitmap(this.player.video);\n\n // もし映像以外に追加で合成するものがないなら、処理の高速化のために ImageBitmap をそのまま Canvas に転送して Blob 化する\n // コメントキャプチャではない & 文字スーパーが表示されていない (=合成処理を行う必要がない) &\n // (字幕が表示されていない or 字幕が表示されているが合成しないように設定されている) 場合\n // コメント付きキャプチャではなく、かつ字幕のない番組では大半がここの処理を通ることになる\n if (with_comments === false && is_superimpose_showing === false &&\n (is_caption_showing === false || Utils.getSettingsItem('capture_caption_mode') === 'VideoOnly')) {\n\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n const bitmap_canvas = ('OffscreenCanvas' in window) ?\n new OffscreenCanvas(image_bitmap.width, image_bitmap.height) : document.createElement('canvas');\n bitmap_canvas.width = image_bitmap.width;\n bitmap_canvas.height = image_bitmap.height;\n const canvas_context = bitmap_canvas.getContext('bitmaprenderer', {alpha: false});\n\n // Canvas に映像がキャプチャされた ImageBitmap を転送\n // 描画ではなくゼロコピーで転送しているらしい…?\n canvas_context.transferFromImageBitmap(image_bitmap);\n image_bitmap.close(); // 今後使うことはないので明示的に閉じる\n\n // ファイル名\n // 保存モードが「字幕キャプチャのみ」のとき (=字幕キャプチャのみをキャプチャする設定にしていたが、字幕がそもそもないとき) は、\n // 便宜上字幕ありキャプチャと同じファイル名で保存する\n const filename_real =\n (Utils.getSettingsItem('capture_caption_mode') === 'CompositingCaption') ? filename_caption : filename;\n\n // Blob にエクスポートして保存\n // false が返ってきた場合は失敗を意味する\n const blob = await export_and_save(\n bitmap_canvas, filename_real, channel.program_present, caption_text, false, with_comments);\n if (blob !== false) {\n capture_normal = {blob: blob, filename: filename_real};\n } else {\n capture_normal = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_normal !== false) {\n this.captured_callback(capture_normal.blob, capture_normal.filename);\n }\n\n // ***** 通常実行 (Canvas にキャプチャ以外のデータを重ねて描画する必要があるケース) *****\n\n } else {\n\n const promises: Promise[] = [];\n\n // Canvas に映像がキャプチャされた ImageBitmap を描画\n this.canvas_context.drawImage(image_bitmap, 0, 0, this.canvas.width, this.canvas.height);\n\n // 文字スーパーを描画 (表示されている場合)\n // 文字スーパー自体が稀だし、文字スーパーなしでキャプチャ撮りたいユースケースはない…はず\n if (is_superimpose_showing === true) {\n this.canvas_context.drawImage(superimpose_canvas, 0, 0, this.canvas.width, this.canvas.height);\n }\n\n // コメント付きキャプチャ: 追加でニコニコ実況のコメントを描画\n let comments_image: HTMLImageElement | null = null;\n if (with_comments === true) {\n comments_image = await this.createCommentsImage();\n await this.drawComments(comments_image);\n }\n\n // ***** 映像のみのキャプチャを保存 *****\n\n // 字幕表示時のキャプチャの保存モード: 映像のみ or 両方\n // 保存モードが「字幕キャプチャのみ」になっているが字幕が表示されていない場合も実行する\n if (['VideoOnly', 'Both'].includes(Utils.getSettingsItem('capture_caption_mode')) || is_caption_showing === false) {\n\n promises.push((async () => {\n\n // ファイル名\n // 保存モードが「字幕キャプチャのみ」のとき (=字幕キャプチャのみをキャプチャする設定にしていたが、字幕がそもそもないとき) は、\n // 便宜上字幕ありキャプチャと同じファイル名で保存する\n const filename_real =\n (Utils.getSettingsItem('capture_caption_mode') === 'CompositingCaption') ? filename_caption : filename;\n\n // Blob にエクスポートして保存\n const blob = await export_and_save(\n this.canvas, filename_real, channel.program_present, caption_text, false, with_comments);\n if (blob !== false) {\n capture_normal = {blob: blob, filename: filename_real};\n } else {\n capture_normal = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_normal !== false) {\n this.captured_callback(capture_normal.blob, capture_normal.filename);\n }\n\n })());\n }\n\n // ***** 字幕付きのキャプチャを保存 *****\n\n // 字幕表示時のキャプチャの保存モード: 字幕キャプチャのみ or 両方\n // 字幕が表示されているときのみ実行(字幕が表示されていないのにやっても意味がない)\n if (['CompositingCaption', 'Both'].includes(Utils.getSettingsItem('capture_caption_mode')) && is_caption_showing === true) {\n\n promises.push((async () => {\n\n // コメント付きキャプチャ: 映像と文字スーパーの描画をやり直す\n // すでに字幕なしキャプチャを生成する過程でコメントを描画してしまっているため、映像描画からやり直す必要がある\n if (with_comments === true) {\n this.canvas_context.drawImage(image_bitmap, 0, 0, this.canvas.width, this.canvas.height);\n if (is_superimpose_showing === true) {\n this.canvas_context.drawImage(superimpose_canvas, 0, 0, this.canvas.width, this.canvas.height);\n }\n }\n image_bitmap.close(); // 今後使うことはないので明示的に閉じる\n\n // 字幕を重ねて描画\n this.canvas_context.drawImage(caption_canvas, 0, 0, this.canvas.width, this.canvas.height);\n\n // コメント付きキャプチャ: 追加でニコニコ実況のコメントを描画\n if (with_comments === true) {\n await this.drawComments(comments_image);\n }\n\n // Blob にエクスポートして保存\n const blob = await export_and_save(\n this.canvas, filename_caption, channel.program_present, caption_text, true, with_comments,\n );\n if (blob !== false) {\n capture_caption = {blob: blob, filename: filename_caption};\n } else {\n capture_caption = false; // キャプチャのエクスポートに失敗\n }\n\n // キャプチャの Blob をコールバック関数に渡す\n // ここでコールバック関数に渡した Blob が Twitter タブのキャプチャリストに送られる\n if (capture_caption !== false) {\n // 字幕表示時のキャプチャの保存モードが「両方 (Both)」のときのみ、映像のみのキャプチャの生成が終わるまで待ってから実行\n // 必ずキャプチャリストへの追加が [映像のみ] → [字幕付き] の順序で行われるようにする\n if (Utils.getSettingsItem('capture_caption_mode') === 'Both') {\n while (capture_normal === null) {\n // Blob (成功) か false (失敗) が capture_normal に入るまでループ\n await Utils.sleep(0.01);\n }\n }\n this.captured_callback(capture_caption.blob, capture_caption.filename);\n }\n\n })());\n }\n\n // すべてのキャプチャ処理が終わるまで待つ\n await Promise.all(promises);\n }\n\n console.log('[PlayerCaptureHandler] Total:', Utils.mathFloor(Utils.time() - total_time, 3), 'sec');\n\n // キャプチャボタンのハイライトを削除する\n this.removeHighlight(with_comments);\n\n // Twitter タブのキャプチャリストに送る処理が最優先なので、コールバックを実行しきった後に時間のかかるクリップボードへのコピーを行う\n for (const capture of [capture_normal, capture_caption]) {\n\n // クリップボードへのコピーが有効なら、キャプチャの Blob をクリップボードにコピー\n // PNG 以外は受け付けないそうなので、JPEG を PNG に変換してからコピーしている\n if (Utils.getSettingsItem('capture_copy_to_clipboard') && capture !== null && capture !== false) {\n try {\n await copyBlobToClipboard(await convertBlobToPng(capture.blob));\n } catch (error) {\n this.player.notice('クリップボードへのキャプチャのコピーに失敗しました…');\n console.error(error);\n }\n }\n }\n }\n\n\n /**\n * キャプチャボタンをハイライトする\n * @param with_comments コメント付きキャプチャボタンをハイライトするか\n */\n private addHighlight(with_comments: boolean = false): void {\n if (with_comments) {\n this.comment_capture_button.classList.add('dplayer-capturing');\n } else {\n this.capture_button.classList.add('dplayer-capturing');\n }\n }\n\n\n /**\n * キャプチャボタンのハイライトを外す\n * @param with_comments コメント付きキャプチャボタンのハイライトを外すか\n */\n private removeHighlight(with_comments: boolean = false): void {\n if (with_comments) {\n this.comment_capture_button.classList.remove('dplayer-capturing');\n } else {\n this.capture_button.classList.remove('dplayer-capturing');\n }\n }\n\n\n /**\n * DPlayer から取得したコメント HTML を SVG 画像の HTMLImageElement に変換する\n * ZenzaWatch のコードを参考にしている\n * ref: https://github.com/segabito/ZenzaWatch/blob/master/packages/lib/src/dom/VideoCaptureUtil.js\n * ref: https://web.archive.org/web/2/https://developer.mozilla.org/ja/docs/Web/HTML/Canvas/Drawing_DOM_objects_into_a_canvas\n * @param html DPlayer から取得したコメント HTML\n * @param width SVG 画像の幅\n * @param height SVG 画像の高さ\n * @returns SVG 画像の HTMLImageElement\n */\n private async commentsHTMLtoSVGImage(html: string, width: number, height: number): Promise {\n\n // SVG の foreignObject を使い、HTML をそのまま SVG に埋め込む\n // SVG なので、CSS はインラインでないと適用されない…\n // DPlayer の danmaku.scss の内容のうち、描画に必要なプロパティのみを列挙 (追加変更したものもある)\n // ref: https://github.com/tsukumijima/DPlayer/blob/master/src/css/danmaku.scss\n const svg = (`\n \n \n
\n \n ${html}\n
\n
\n
\n `).trim();\n\n // Data URL 化して Image オブジェクトにする\n // わざわざ Blob にするよりこっちのほうが楽\n const image = new Image();\n image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;\n\n // Image は onload を使わなくても await Image.decode() でロードできる\n await image.decode();\n return image;\n }\n\n\n /**\n * DPlayer から表示中のコメントを取得し、SVG 画像の HTMLImageElement を作成する\n * @returns 表示されているコメントが描画された HTMLImageElement\n */\n private async createCommentsImage(): Promise {\n\n // コメントが表示されている要素の HTML を取得する\n let comments_html = (this.player.template.danmaku as HTMLDivElement).outerHTML;\n\n // HTML を取得するだけではスクロール中コメントの表示位置が特定できないため、HTML を修正する\n for (const comment of this.player_container.querySelectorAll('.dplayer-danmaku-move')) { // コメントの数だけ置換\n // スクロール中のコメントの表示座標を計算\n const position = comment.getBoundingClientRect().left - this.player.video.getBoundingClientRect().left;\n comments_html = comments_html.replace(/transform: translateX\\(.*?\\);/, `left: ${position}px;`)\n .replaceAll('border: 2px solid #E64F97;', '');\n }\n\n // HTML を画像として取得\n // SVG のサイズはコメントが表示されている要素に合わせる (そうしないとプレイヤー側と一致しない)\n // SVG はベクター画像なので、リサイズしても画質が変わらないはず\n return await this.commentsHTMLtoSVGImage(\n comments_html,\n (this.player.template.danmaku as HTMLDivElement).offsetWidth,\n (this.player.template.danmaku as HTMLDivElement).offsetHeight,\n );\n }\n\n\n /**\n * 現在表示されているニコニコ実況のコメントを Canvas に描画する\n */\n private async drawComments(comments_image: HTMLImageElement): Promise {\n\n // コメント描画領域がコントロールの表示によりリサイズされている (=16:9でない) 場合も考慮して、コメント要素の offsetWidth から高さを求める\n // 映像の横解像度 (ex: 1920) がコメント描画領域の幅 (ex: 1280) の何倍かの割合 (ex: 1.5 (150%))\n const draw_scale_ratio = this.canvas.width / (this.player.template.danmaku as HTMLDivElement).offsetWidth;\n\n // コメント描画領域の高さを映像の横解像度に合わせて(コメント描画領域のアスペクト比を維持したまま)拡大した値\n // 映像の縦解像度が 1080 のとき、コントロールがコメント領域と被っていない or 表示されていないなら、この値は 1080 に近くなる\n const draw_height = (this.player.template.danmaku as HTMLDivElement).offsetHeight * draw_scale_ratio;\n\n this.canvas_context.drawImage(comments_image, 0, 0, this.canvas.width, draw_height);\n }\n\n\n /**\n * Canvas もしくは OffscreenCanvas に描画されている画像を Blob に変換する\n * JPEG 画像の品質は 99% にした方が若干 Blob 変換までの速度が速い (?)\n * @param canvas Canvas もしくは OffscreenCanvas\n * @returns Blob 化した画像\n */\n private async exportToBlob(canvas: HTMLCanvasElement | OffscreenCanvas): Promise {\n if (canvas instanceof OffscreenCanvas) {\n return await canvas.convertToBlob({type: 'image/jpeg', quality: 0.99});\n } else {\n return new Promise((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob !== null) {\n resolve(blob);\n } else {\n reject(new Error('Failed to convert canvas to blob'));\n }\n }, 'image/jpeg', 0.99);\n });\n }\n }\n\n\n /**\n * キャプチャ画像に番組情報と撮影時刻、字幕やコメントが合成されているかどうかのメタデータ (EXIF) をセットする\n * @param blob キャプチャ画像の Blob オブジェクト\n * @param program EXIF にセットする番組情報オブジェクト\n * @param caption_text 字幕のテキスト (キャプチャしたときに字幕が表示されていなければ null)\n * @param is_caption_composited 字幕が合成されているか\n * @param is_comment_composited コメントが合成されているか\n * @returns EXIF が追加されたキャプチャ画像の Blob オブジェクト\n */\n private async setEXIFDataToCapture(\n blob: Blob,\n program: IProgram,\n caption_text: string | null,\n is_caption_composited: boolean,\n is_comment_composited: boolean,\n ): Promise {\n\n // 番組開始時刻換算のキャプチャ時刻 (秒)\n const captured_playback_position = dayjs().diff(dayjs(program.start_time), 'second', true);\n\n // EXIF の XPComment 領域に入れるメタデータの JSON オブジェクト\n // 撮影時刻とチャンネル・番組を一意に特定できる情報を入れる\n const json: ICaptureExifData = {\n captured_at: dayjs().format('YYYY-MM-DDTHH:mm:ss+09:00'), // ISO8601 フォーマットのキャプチャ時刻\n captured_playback_position: captured_playback_position, // 番組開始時刻換算のキャプチャ時刻 (秒)\n network_id: program.network_id, // 番組が放送されたチャンネルのネットワーク ID\n service_id: program.service_id, // 番組が放送されたチャンネルのサービス ID\n event_id: program.event_id, // 番組のイベント ID\n title: program.title, // 番組タイトル\n description: program.description, // 番組概要\n start_time: program.start_time, // 番組開始時刻 (ISO8601 フォーマット)\n end_time: program.end_time, // 番組終了時刻 (ISO8601 フォーマット)\n duration: program.duration, // 番組長 (秒)\n caption_text: caption_text, // 字幕のテキスト (キャプチャした瞬間に字幕が表示されていなかったときは null)\n is_caption_composited: is_caption_composited, // 字幕が合成されているか\n is_comment_composited: is_comment_composited, // コメントが合成されているか\n }\n\n // 保存する EXIF メタデータを構築\n // ref: 「カメラアプリで体感するWeb App」4.2\n const datetime = dayjs().format('YYYY:MM:DD HH:mm:ss'); // すべてコロンで区切るのがポイント\n const exif: piexif.IExif = {\n '0th': {\n // 必須らしいプロパティ\n // とりあえずデフォルト値 (?) を設定しておく\n [piexif.TagValues.ImageIFD.XResolution]: [72, 1],\n [piexif.TagValues.ImageIFD.YResolution]: [72, 1],\n [piexif.TagValues.ImageIFD.ResolutionUnit]: 2,\n [piexif.TagValues.ImageIFD.YCbCrPositioning]: 1,\n // 撮影時刻\n [piexif.TagValues.ImageIFD.DateTime]: datetime,\n // ソフトウェア名\n [piexif.TagValues.ImageIFD.Software]: `KonomiTV version ${Utils.version}`,\n // Microsoft 拡張のコメント領域(エクスプローラーで出てくるコメント欄と同じもの)\n // ref: https://stackoverflow.com/a/66186660/17124142\n [piexif.TagValues.ImageIFD.XPComment]: [...Buffer.from(JSON.stringify(json), 'ucs2')],\n },\n 'Exif': {\n // 必須らしいプロパティ\n // とりあえずデフォルト値 (?) を設定しておく\n [piexif.TagValues.ExifIFD.ExifVersion]: '0230',\n [piexif.TagValues.ExifIFD.ComponentsConfiguration]: '\\x01\\x02\\x03\\x00',\n [piexif.TagValues.ExifIFD.FlashpixVersion]: '0100',\n [piexif.TagValues.ExifIFD.ColorSpace]: 1,\n // 撮影時刻\n [piexif.TagValues.ExifIFD.DateTimeOriginal]: datetime,\n [piexif.TagValues.ExifIFD.DateTimeDigitized]: datetime,\n },\n };\n const exif_string = piexif.dump(exif); // バイナリ文字列に変換した EXIF データ\n\n // piexifjs はバイナリ文字列か DataURL しか受け付けないので、Blob をバイナリ文字列に変換\n const blob_string: string = await new Promise((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(reader.result as string);\n reader.onerror = reject;\n reader.readAsBinaryString(blob); // バイナリ文字列で読み込む\n });\n\n // 画像に EXIF を挿入\n // 戻り値は EXIF が追加された画像のバイナリ文字列 (なぜ未だにバイナリ文字列で実装してるんだ…)\n const blob_string_new = piexif.insert(exif_string, blob_string);\n\n // 画像のバイナリ文字列を ArrayBuffer に変換\n // ref: 「カメラアプリで体感するWeb App」4.2\n const buffer = new Uint8Array(blob_string_new.length);\n for (let index = 0; index < buffer.length; index++) {\n buffer[index] = blob_string_new.charCodeAt(index) & 0xff;\n }\n\n // 新しい Blob を返す\n return new Blob([buffer], {type: blob.type});\n }\n\n\n /**\n * KonomiTV サーバーにキャプチャ画像をアップロードする関数\n * @param blob キャプチャ画像の Blob\n * @param filename サーバーに保存するときのファイル名\n */\n private async uploadCaptureToServer(blob: Blob, filename: string): Promise {\n\n // キャプチャ画像の File オブジェクト (= Blob) を FormData に入れる\n // multipart/form-data で送るために必要\n // ref: https://r17n.page/2020/02/04/nodejs-axios-file-upload-api/\n const form_data = new FormData();\n form_data.append('image', blob, filename);\n\n // キャプチャ画像アップロード API にリクエスト\n try {\n await Vue.axios.post('/captures', form_data, {headers: {'Content-Type': 'multipart/form-data'}});\n } catch (error) {\n console.error(error);\n this.player.notice('キャプチャのアップロードに失敗しました。');\n }\n }\n}\n","\n/**\n * プレイヤー周りのユーティリティ\n */\nexport class PlayerUtils {\n\n /**\n * プレイヤーの背景画像をランダムで取得し、その URL を返す\n * @returns ランダムで設定されたプレイヤーの背景画像の URL\n */\n static generatePlayerBackgroundURL(): string {\n const background_count = 12; // 12種類から選択\n const random = (Math.floor(Math.random() * background_count) + 1);\n return `/assets/images/player-backgrounds/${random.toString().padStart(2, '0')}.jpg`;\n }\n\n\n /**\n * 現在のブラウザで H.265 / HEVC 映像が再生できるかどうかを取得する\n * ref: https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding#mediacapabilities\n * @returns 再生できるなら true、できないなら false\n */\n static isHEVCVideoSupported(): boolean {\n // hvc1.1.6.L123.B0 の部分は呪文 (HEVC であることと、そのプロファイルを示す値らしい)\n return document.createElement('video').canPlayType('video/mp4; codecs=\"hvc1.1.6.L123.B0\"') === 'probably';\n }\n}\n","\nimport dayjs from 'dayjs';\nimport 'dayjs/locale/ja';\nimport isBetween from 'dayjs/plugin/isBetween';\nimport isSameOrAfter from 'dayjs/plugin/isSameOrAfter';\nimport isSameOrBefore from 'dayjs/plugin/isSameOrBefore'\n\nimport { IProgram } from '@/interface';\nimport Utils from './Utils';\n\n/**\n * 番組情報周りのユーティリティ\n */\nexport class ProgramUtils {\n\n /**\n * 番組情報中の[字]や[解]などの記号をいい感じに装飾する\n * @param program 番組情報のオブジェクト\n * @param key 番組情報のオブジェクトから取り出すプロパティのキー\n * @returns 装飾した文字列\n */\n static decorateProgramInfo(program: IProgram | null, key: string): string {\n\n // program が空でないかつ、program[key] が存在する\n if (program !== null && program[key] !== null) {\n\n // 番組情報に含まれる HTML の特殊文字で表示がバグらないように、事前に HTML エスケープしておく\n const text = Utils.escapeHTML(program[key]);\n\n // 本来 ARIB 外字である記号の一覧\n // ref: https://ja.wikipedia.org/wiki/%E7%95%AA%E7%B5%84%E8%A1%A8\n // ref: https://github.com/xtne6f/EDCB/blob/work-plus-s/EpgDataCap3/EpgDataCap3/ARIB8CharDecode.cpp#L1319\n const mark = '新|終|再|交|映|手|声|多|副|字|文|CC|OP|二|S|B|SS|無|無料' +\n 'C|S1|S2|S3|MV|双|デ|D|N|W|P|H|HV|SD|天|解|料|前|後初|生|販|吹|PPV|' +\n '演|移|他|収|・|英|韓|中|字/日|字/日英|3D|2K|4K|8K|5.1|7.1|22.2|60P|120P|d|HC|HDR|SHV|UHD|VOD|配|初';\n\n // 正規表現を作成\n const pattern1 = new RegExp(`\\\\((二|字|再)\\\\)`, 'g'); // 通常の括弧で囲まれている記号\n const pattern2 = new RegExp(`\\\\[(${mark})\\\\]`, 'g');\n\n // 正規表現で置換した結果を返す\n return text.replace(pattern1, '$1')\n .replace(pattern2, '$1');\n\n // 番組情報がない時間帯\n } else {\n\n dayjs.extend(isSameOrAfter);\n dayjs.extend(isSameOrBefore);\n dayjs.extend(isBetween);\n\n // 23時~翌7時 (0:00 ~ 06:59 or 23:00 ~ 23:59) の間なら放送を休止している可能性が高いので、放送休止と表示する\n const now = dayjs();\n const pause_time_start = dayjs().hour(0).minute(0).second(0);\n const pause_time_end = dayjs().hour(6).minute(59).second(59);\n const pause_time_start_23 = dayjs().hour(23).minute(0).second(0);\n const pause_time_end_23 = dayjs().hour(23).minute(59).second(59);\n if ((now.isSameOrAfter(pause_time_start) && now.isSameOrBefore(pause_time_end)) ||\n (now.isSameOrAfter(pause_time_start_23) && now.isSameOrBefore(pause_time_end_23))) {\n if (key === 'title') {\n return '放送休止'; // タイトル\n } else {\n return 'この時間は放送を休止しています。'; // 番組概要\n }\n\n // それ以外の時間帯では、「番組情報がありません」と表示する\n // 急な番組変更の影響で、一時的にその時間帯に対応する番組情報が消えることがある\n // 特に Mirakurun バックエンドでは高頻度で収集した EIT[p/f] が比較的すぐ反映されるため、この現象が起こりやすい\n // 日中に放送休止(停波)になることはまずあり得ないので、番組情報が取得できてないだけで視聴できるかも?というニュアンスを与える\n } else {\n if (key === 'title') {\n return '番組情報がありません'; // タイトル\n } else {\n return 'この時間の番組情報を取得できませんでした。'; // 番組概要\n }\n }\n }\n }\n\n\n /**\n * オブジェクトからプロパティを取得し、もしプロパティが存在しなければ代替値を返す\n * @param items 対象のオブジェクト\n * @param key オブジェクトから取り出すプロパティのキー\n * @param default_value 取得できなかった際の代替値\n * @returns オブジェクト取得した値 or 代替値\n */\n static getAttribute(items: {[key: string]: any}, key: string, default_value: any): any {\n\n // items が空でないかつ、items[key] が存在する\n if (items !== null && items[key] !== undefined && items[key] !== null) {\n\n // items[key] の内容を返す\n return items[key];\n\n // 指定された代替値を返す\n } else {\n return default_value;\n }\n }\n\n\n /**\n * 番組の進捗状況を取得する\n * @param program 番組情報\n * @returns 番組の進捗状況(%単位)\n */\n static getProgramProgress(program: IProgram): number {\n\n // program が空でない\n if (program !== null) {\n\n // 番組開始時刻から何秒進んだか\n const progress = dayjs(dayjs()).diff(program.start_time, 'second');\n\n // %単位の割合を算出して返す\n return progress / program.duration * 100;\n\n // 放送休止中\n } else {\n return 0;\n }\n }\n\n\n /**\n * 番組の放送時刻を取得する\n * @param program 番組情報\n * @param is_short 時刻のみ返すかどうか\n * @returns 番組の放送時刻\n */\n static getProgramTime(program: IProgram, is_short: boolean = false): string {\n\n // program が空でなく、かつ番組時刻が初期値でない\n if (program !== null && program.start_time !== '2000-01-01T00:00:00+09:00') {\n\n // dayjs で日付を扱いやすく\n dayjs.locale('ja'); // ロケールを日本に設定\n const start_time = dayjs(program.start_time);\n const end_time = dayjs(program.end_time);\n const duration = program.duration / 60; // 分換算\n\n // 時刻のみ返す\n if (is_short === true) { // 時刻のみ\n return `${start_time.format('HH:mm')} ~ ${end_time.format('HH:mm')}`;\n // 通常\n } else {\n return `${start_time.format('YYYY/MM/DD (dd) HH:mm')} ~ ${end_time.format('HH:mm')} (${duration}分)`;\n }\n\n // 放送休止中\n } else {\n\n // 時刻のみ返す\n if (is_short === true) {\n return '--:-- ~ --:--';\n // 通常\n } else {\n return '----/--/-- (-) --:-- ~ --:-- (--分)';\n }\n }\n }\n}\n","\n// 共通ユーティリティをデフォルトとしてインポート\nimport Utils from '@/utils/Utils';\nexport default Utils;\n\n// Utils フォルダ配下のユーティリティを一括でインポートできるように\nexport * from '@/utils/ChannelUtils';\nexport * from '@/utils/PlayerCaptureHandler';\nexport * from '@/utils/PlayerUtils';\nexport * from '@/utils/ProgramUtils';\n","\nimport Utils from '@/utils';\nimport axios from 'axios'\n\n// ref: https://note.com/quoizunda/n/nb62e13e73499\n\n// Axios のインスタンスを作成\nconst axios_instance = axios.create();\n\n// HTTP リクエスト前に割り込んで行われる処理\naxios_instance.interceptors.request.use(config => {\n\n // API のベース URL を設定\n // BaseURL が明示的に指定されているときは設定しない\n if (config.baseURL === undefined) {\n config.baseURL = Utils.api_base_url;\n }\n\n // アクセストークンが取得できたら(=ログインされていれば)\n // 取得したアクセストークンを Authorization ヘッダーに Bearer トークンとしてセット\n // これを忘れると(当然ながら)ログインしていない扱いになる\n const access_token = Utils.getAccessToken();\n if (access_token !== null) {\n config.headers['Authorization'] = `Bearer ${access_token}`;\n }\n\n // KonomiTV クライアントのバージョンを設定\n // 今のところ使わないが、将来的にクライアントとサーバーを分離することを見据えて念のため\n config.headers['X-KonomiTV-Version'] = Utils.version;\n\n return config;\n})\n\n// ここで返したインスタンスを VueAxios (Vue.axios) に設定する\nexport default axios_instance;\n","\nimport Vue from 'vue';\nimport Vuetify from 'vuetify/lib/framework';\nimport { VSnackbar, VBtn, VIcon } from 'vuetify/lib';\n\nVue.use(Vuetify);\n\n// vuetify-message-snackbar を使うのに必要\nVue.component('v-snackbar', VSnackbar);\nVue.component('v-btn', VBtn);\nVue.component('v-icon', VIcon);\n\nexport default new Vuetify({\n theme: {\n dark: true,\n themes: {\n dark: {\n primary: '#E64F97',\n secondary: '#E33157',\n twitter: {\n base: '#4F82E6',\n lighten1: '#799FEC',\n lighten2: '#41A5F1',\n },\n gray: '#66514C',\n black: '#110A09',\n background: {\n base: '#1E1310',\n lighten1: '#2F221F',\n lighten2: '#433532',\n lighten3: '#4c3c38',\n },\n text: {\n base: '#FFEAEA',\n darken1: '#D9C7C7',\n darken2: '#8E7F7E',\n darken3: '#786968',\n }\n }\n },\n options: {\n customProperties: true,\n },\n },\n});\n","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"channels-container channels-container--home\",class:{'channels-container--loading': _vm.is_loading}},[_c('v-tabs-fix',{staticClass:\"channels-tab\",attrs:{\"centered\":\"\"},model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type,]){return _c('v-tab',{key:channels_type,staticClass:\"channels-tab__item\"},[_vm._v(_vm._s(channels_type))])}),1),_c('v-tabs-items-fix',{staticClass:\"channels-list\",model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type, channels]){return _c('v-tab-item-fix',{key:channels_type,staticClass:\"channels-tabitem\"},[_c('div',{staticClass:\"channels\",class:`channels--tab-${channels_type} channels--length-${channels.length}`},[_vm._l((channels),function(channel){return _c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:channel.id,staticClass:\"channel\",attrs:{\"to\":`/tv/watch/${channel.channel_id}`}},[_c('div',{staticClass:\"channel__broadcaster\"},[_c('img',{staticClass:\"channel__broadcaster-icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${channel.channel_id}/logo`}}),_c('div',{staticClass:\"channel__broadcaster-content\"},[_c('span',{staticClass:\"channel__broadcaster-name\"},[_vm._v(\"Ch: \"+_vm._s(channel.channel_number)+\" \"+_vm._s(channel.channel_name))]),_c('div',{staticClass:\"channel__broadcaster-status\"},[_c('div',{staticClass:\"channel__broadcaster-status-force\",class:`channel__broadcaster-status-force--${_vm.ChannelUtils.getChannelForceType(channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"12px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"勢い:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(channel, 'channel_force', '--')))]),_c('span',{staticStyle:{\"margin-left\":\"3px\"}},[_vm._v(\" コメ/分\")])],1),_c('div',{staticClass:\"channel__broadcaster-status-viewers ml-4\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:eye\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"視聴数:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(channel.viewers))])],1)])]),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip\",value:(_vm.isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'),expression:\"isPinnedChannel(channel.channel_id) ? 'ピン留めを外す' : 'ピン留めする'\"}],staticClass:\"channel__broadcaster-pin\",class:{'channel__broadcaster-pin--pinned': _vm.isPinnedChannel(channel.channel_id)},on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.isPinnedChannel(channel.channel_id) ? _vm.removePinnedChannel(channel.channel_id) : _vm.addPinnedChannel(channel.channel_id)},\"mousedown\":function($event){$event.preventDefault();$event.stopPropagation();/* 親要素の波紋が広がらないように */}}},[_c('Icon',{attrs:{\"icon\":\"fluent:pin-20-filled\",\"width\":\"24px\"}})],1)]),_c('div',{staticClass:\"channel__program-present\"},[_c('div',{staticClass:\"channel__program-present-title-wrapper\"},[_c('span',{staticClass:\"channel__program-present-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'title'))}}),_c('span',{staticClass:\"channel__program-present-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_present)))])]),_c('span',{staticClass:\"channel__program-present-description\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'description'))}})]),_c('v-spacer'),_c('div',{staticClass:\"channel__program-following\"},[_c('div',{staticClass:\"channel__program-following-title\"},[_c('span',{staticClass:\"channel__program-following-title-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"channel__program-following-title-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}}),_c('span',{staticClass:\"channel__program-following-title-text\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_following, 'title'))}})],1),_c('span',{staticClass:\"channel__program-following-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_following)))])]),_c('div',{staticClass:\"channel__progressbar\"},[_c('div',{staticClass:\"channel__progressbar-progress\",style:(`width:${_vm.ProgramUtils.getProgramProgress(channel.program_present)}%;`)})])],1)}),(channels_type === 'ピン留め' && channels.length === 0)?_c('div',{staticClass:\"pinned-container d-flex justify-center align-center w-100\"},[_c('div',{staticClass:\"d-flex justify-center align-center flex-column\"},[_c('h2',[_vm._v(\"ピン留めされているチャンネルがありません。\")]),_c('div',{staticClass:\"mt-4 text--text text--darken-1\"},[_vm._v(\"各チャンネルの \"),_c('Icon',{staticStyle:{\"position\":\"relative\",\"bottom\":\"-5px\"},attrs:{\"icon\":\"fluent:pin-20-filled\",\"width\":\"22px\"}}),_vm._v(\" アイコンから、よくみるチャンネルをこのタブにピン留めできます。\")],1),_c('div',{staticClass:\"mt-2 text--text text--darken-1\"},[_vm._v(\"チャンネルをピン留めすると、このタブが最初に表示されます。\")])])]):_vm._e()],2)])}),1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c;return _c('header',{staticClass:\"header\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"konomitv-logo ml-3 ml-md-6\",attrs:{\"to\":\"/tv/\"}},[_c('img',{staticClass:\"konomitv-logo__image\",attrs:{\"src\":\"/assets/images/logo.svg\",\"height\":\"21\"}})]),_c('v-spacer')],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","import { render, staticRenderFns } from \"./Header.vue?vue&type=template&id=506af489&scoped=true&\"\nvar script = {}\nimport style0 from \"./Header.vue?vue&type=style&index=0&id=506af489&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"506af489\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"navigation-container elevation-8\"},[_c('nav',{staticClass:\"navigation\"},[_c('div',{staticClass:\"navigation-scroll\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/tv/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:tv-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"テレビをみる\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/videos/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:movies-and-tv-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"ビデオをみる\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/timetable/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:calendar-ltr-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"番組表\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/captures/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:image-multiple-24-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"キャプチャ\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/watchlists/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"ic:round-playlist-play\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"ウォッチリスト\")])],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/histories/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:history-16-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"視聴履歴\")])],1),_c('v-spacer'),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"navigation__link\",attrs:{\"active-class\":\"navigation__link--active\",\"to\":\"/settings/\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",attrs:{\"icon\":\"fluent:settings-20-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"設定\")])],1),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.top\",value:(_vm.is_update_available ? `アップデートがあります (version ${_vm.latest_version})` : ''),expression:\"is_update_available ? `アップデートがあります (version ${latest_version})` : ''\",modifiers:{\"top\":true}}],staticClass:\"navigation__link\",class:{'navigation__link--version': _vm.Utils.version.includes('-dev')},attrs:{\"active-class\":\"navigation__link--active\",\"href\":\"https://github.com/tsukumijima/KonomiTV\"}},[_c('Icon',{staticClass:\"navigation__link-icon\",class:{'navigation__link-icon--highlight': _vm.is_update_available},attrs:{\"icon\":\"fluent:info-16-regular\",\"width\":\"26px\"}}),_c('span',{staticClass:\"navigation__link-text\"},[_vm._v(\"version \"+_vm._s(_vm.Utils.version))])],1)],1)])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport { IVersionInformation } from '@/interface';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Navigation',\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // 最新のバージョン\n latest_version: '' as string,\n\n // アップデートが利用可能か\n is_update_available: false as boolean,\n }\n },\n async created() {\n try {\n\n // バージョン情報を取得\n const version_info: IVersionInformation = (await Vue.axios.get(`/version`)).data;\n this.latest_version = version_info.latest_version;\n\n // もし現在のサーバーバージョン (-dev を除く) と最新のサーバーバージョンが異なるなら、アップデートが利用できる旨を表示する\n // 現在のサーバーバージョンが -dev 付きで、かつ最新のサーバーバージョンが -dev なし の場合 (リリース版がリリースされたとき) も同様に表示する\n // つまり開発版だと同じバージョンのリリース版がリリースされたときにしかアップデート通知が表示されない事になるが、ひとまずこれで…\n if ((version_info.version.includes('-dev') === false && version_info.version !== version_info.latest_version) ||\n (version_info.version.includes('-dev') === true && version_info.version.replace('-dev', '') === version_info.latest_version)) {\n this.is_update_available = true;\n }\n\n } catch (error) {\n throw new Error(error); // エラー内容をコンソールに表示して終了\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Navigation.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Navigation.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Navigation.vue?vue&type=template&id=3c027344&scoped=true&\"\nimport script from \"./Navigation.vue?vue&type=script&lang=ts&\"\nexport * from \"./Navigation.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Navigation.vue?vue&type=style&index=0&id=3c027344&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3c027344\",\n null\n \n)\n\nexport default component.exports","\n\nimport Vue from 'vue';\n\nimport { ChannelTypePretty, IChannel } from '@/interface';\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'TV-Home',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n\n // タブの状態管理\n tab: null as number | null,\n\n // ローディング中かどうか\n is_loading: true,\n\n // インターバル ID\n // ページ遷移時に setInterval(), setTimeout() の実行を止めるのに使う\n // setInterval(), setTimeout() の返り値を登録する\n interval_ids: [] as number[],\n\n // チャンネル情報リスト\n channels_list: new Map() as Map,\n\n // ピン留めしているチャンネルの ID (ex: gr011) が入るリスト\n pinned_channel_ids: [] as string[],\n }\n },\n // 開始時に実行\n created() {\n\n // チャンネル情報を取得\n this.update();\n\n // 00秒までの残り秒数\n // 現在 16:01:34 なら 26 (秒) になる\n const residue_second = 60 - (Math.floor(new Date().getTime() / 1000) % 60);\n\n // 00秒になるまで待ってから\n // 番組は基本1分単位で組まれているため、20秒や45秒など中途半端な秒数で更新してしまうと反映が遅れてしまう\n this.interval_ids.push(window.setTimeout(() => {\n\n // チャンネル情報を更新\n this.update();\n\n // チャンネル情報を定期的に更新\n this.interval_ids.push(window.setInterval(() => {\n this.update();\n }, 30 * 1000)); // 30秒おき\n\n }, residue_second * 1000));\n },\n // 終了前に実行\n beforeDestroy() {\n\n // clearInterval() ですべての setInterval(), setTimeout() の実行を止める\n // clearInterval() と clearTimeout() は中身共通なので問題ない\n for (const interval_id of this.interval_ids) {\n window.clearInterval(interval_id);\n }\n },\n methods: {\n\n // チャンネル情報一覧を取得し、画面を更新する\n async update() {\n\n // チャンネル情報一覧 API にアクセス\n let channels_response;\n try {\n channels_response = await Vue.axios.get('/channels');\n } catch (error) {\n console.error(error); // エラー内容を表示\n return;\n }\n\n // is_display が true のチャンネルのみに絞り込むフィルタ関数\n // 放送していないサブチャンネルを表示から除外する\n const filter = (channel: IChannel) => {\n return channel.is_display;\n }\n\n // チャンネルリストを再構築\n // 1つでもチャンネルが存在するチャンネルタイプのみ表示するように\n // たとえば SKY (スカパー!プレミアムサービス) のタブは SKY に属すチャンネルが1つもない(=受信できない)なら表示されない\n this.channels_list = new Map();\n if (channels_response.data.GR.length > 0) this.channels_list.set('地デジ', channels_response.data.GR.filter(filter));\n if (channels_response.data.BS.length > 0) this.channels_list.set('BS', channels_response.data.BS.filter(filter));\n if (channels_response.data.CS.length > 0) this.channels_list.set('CS', channels_response.data.CS.filter(filter));\n if (channels_response.data.CATV.length > 0) this.channels_list.set('CATV', channels_response.data.CATV.filter(filter));\n if (channels_response.data.SKY.length > 0) this.channels_list.set('SKY', channels_response.data.SKY.filter(filter));\n if (channels_response.data.STARDIGIO.length > 0) this.channels_list.set('StarDigio', channels_response.data.STARDIGIO.filter(filter));\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList(this.is_loading ? true : false);\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n\n // チャンネルをピン留めする\n addPinnedChannel(channel_id: string) {\n\n // 現在ピン留めされているチャンネルを取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めするチャンネルの ID を追加\n this.pinned_channel_ids.push(channel_id);\n\n // 設定を保存\n Utils.setSettingsItem('pinned_channel_ids', this.pinned_channel_ids);\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList();\n },\n\n // チャンネルをピン留めから外す\n removePinnedChannel(channel_id: string) {\n\n // 現在ピン留めされているチャンネルを取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めを外すチャンネルの ID を削除\n this.pinned_channel_ids.splice(this.pinned_channel_ids.indexOf(channel_id), 1);\n\n // 設定を保存\n Utils.setSettingsItem('pinned_channel_ids', this.pinned_channel_ids);\n\n // ピン留めされているチャンネルのリストを更新\n this.updatePinnedChannelList();\n },\n\n // ピン留めされているチャンネルのリストを更新する\n updatePinnedChannelList(is_update_tab: boolean = true) {\n\n // ピン留めされているチャンネルの ID を取得\n this.pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めされているチャンネル情報のリスト\n const pinned_channels = [] as IChannel[];\n\n // チャンネル ID が一致したチャンネルの情報を保存する\n for (const pinned_channel_id of this.pinned_channel_ids) {\n const pinned_channel_type = ChannelUtils.getChannelType(pinned_channel_id, true) as ChannelTypePretty;\n const pinned_channel = this.channels_list.get(pinned_channel_type).find((channel) => {\n return channel.channel_id === pinned_channel_id; // チャンネル ID がピン留めされているチャンネルのものと同じ\n });\n // チャンネル情報を取得できているときだけ\n // サブチャンネルをピン留めしたが、マルチ編成が終了して現在は放送していない場合などに備える (BS142 など)\n // 現在放送していないチャンネルは this.channels_list に入れた段階で弾いているため、チャンネル情報を取得できない\n if (pinned_channel !== undefined) {\n pinned_channels.push(pinned_channel);\n }\n }\n\n if (!this.channels_list.has('ピン留め')) {\n // タブの一番左にピン留めタブを表示する\n this.channels_list = new Map([['ピン留め', pinned_channels], ...this.channels_list]);\n } else {\n // 既に存在するピン留めタブにチャンネル情報を設定する\n this.channels_list.set('ピン留め', pinned_channels);\n }\n\n // pinned_channels が空の場合は、タブを地デジタブに変更\n // ピン留めができる事を示唆するためにピン留めタブ自体は残す\n if (pinned_channels.length === 0 && is_update_tab === true) {\n this.tab = 1;\n }\n },\n\n // チャンネルがピン留めされているか\n isPinnedChannel(channel_id: string): boolean {\n\n // 引数のチャンネルがピン留めリストに存在するかを返す\n return this.pinned_channel_ids.includes(channel_id);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Home.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Home.vue?vue&type=template&id=0185e37f&scoped=true&\"\nimport script from \"./Home.vue?vue&type=script&lang=ts&\"\nexport * from \"./Home.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Home.vue?vue&type=style&index=0&id=0185e37f&prod&lang=scss&\"\nimport style1 from \"./Home.vue?vue&type=style&index=1&id=0185e37f&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0185e37f\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('main',{staticClass:\"watch-container\",class:{\n 'watch-container--control-display': _vm.is_control_display,\n 'watch-container--panel-display': _vm.is_panel_display,\n 'watch-container--fullscreen': _vm.is_fullscreen,\n }},[_c('nav',{staticClass:\"watch-navigation\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"watch-navigation__icon\",attrs:{\"to\":\"/tv/\"}},[_c('img',{staticClass:\"watch-navigation__icon-image\",attrs:{\"src\":\"/assets/images/icon.svg\",\"width\":\"23px\"}})]),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('テレビをみる'),expression:\"'テレビをみる'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/tv/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:tv-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('ビデオをみる'),expression:\"'ビデオをみる'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/videos/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:movies-and-tv-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('番組表'),expression:\"'番組表'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/timetable/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:calendar-ltr-20-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('キャプチャ'),expression:\"'キャプチャ'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/captures/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:image-multiple-24-regular\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('ウォッチリスト'),expression:\"'ウォッチリスト'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/watchlists/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"ic:round-playlist-play\",\"width\":\"26px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('視聴履歴'),expression:\"'視聴履歴'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/histories/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:history-16-regular\",\"width\":\"26px\"}})],1),_c('v-spacer'),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:('設定'),expression:\"'設定'\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"to\":\"/settings/\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:settings-20-regular\",\"width\":\"26px\"}})],1),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.right\",value:(`version ${_vm.Utils.version}`),expression:\"`version ${Utils.version}`\",modifiers:{\"right\":true}}],staticClass:\"watch-navigation__link\",attrs:{\"active-class\":\"watch-navigation__link--active\",\"href\":\"https://github.com/tsukumijima/KonomiTV\"}},[_c('Icon',{staticClass:\"watch-navigation__link-icon\",attrs:{\"icon\":\"fluent:info-16-regular\",\"width\":\"26px\"}})],1)],1),_c('div',{staticClass:\"watch-content\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event, true)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event, true)},\"click\":function($event){return _vm.controlDisplayTimer($event, true)}}},[_c('header',{staticClass:\"watch-header\"},[_c('img',{staticClass:\"watch-header__broadcaster\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('span',{staticClass:\"watch-header__program-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'title'))}}),_c('span',{staticClass:\"watch-header__program-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_present, true)))]),_c('v-spacer'),_c('span',{staticClass:\"watch-header__now\"},[_vm._v(_vm._s(_vm.time))])],1),_c('div',{staticClass:\"watch-player\",class:{\n 'watch-player--loading': _vm.is_loading,\n 'watch-player--virtual-keyboard-display': _vm.is_virtual_keyboard_display && _vm.Utils.hasActiveElementClass('dplayer-comment-input'),\n }},[_c('div',{staticClass:\"watch-player__background\",class:{'watch-player__background--display': _vm.is_background_display},style:({backgroundImage: `url(${_vm.background_url})`})},[_c('img',{staticClass:\"watch-player__background-logo\",attrs:{\"src\":\"/assets/images/logo.svg\"}})]),_c('v-progress-circular',{staticClass:\"watch-player__buffering\",class:{'watch-player__buffering--display': _vm.is_video_buffering && (_vm.is_loading || (_vm.player !== null && !_vm.player.video.paused))},attrs:{\"indeterminate\":\"\",\"size\":\"60\",\"width\":\"6\"}}),_c('div',{staticClass:\"watch-player__dplayer\"}),_c('div',{staticClass:\"watch-player__button\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.top\",value:('前のチャンネル'),expression:\"'前のチャンネル'\",modifiers:{\"top\":true}}],staticClass:\"switch-button switch-button-up\",attrs:{\"to\":`/tv/watch/${_vm.channel_previous.channel_id}`}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:ios-arrow-left-24-filled\",\"width\":\"32px\",\"rotate\":\"1\"}})],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"switch-button switch-button-panel switch-button-panel--open\",on:{\"click\":function($event){_vm.is_panel_display = !_vm.is_panel_display}}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:navigation-16-filled\",\"width\":\"32px\"}})],1),_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"},{name:\"tooltip\",rawName:\"v-tooltip.bottom\",value:('次のチャンネル'),expression:\"'次のチャンネル'\",modifiers:{\"bottom\":true}}],staticClass:\"switch-button switch-button-down\",attrs:{\"to\":`/tv/watch/${_vm.channel_next.channel_id}`}},[_c('Icon',{staticClass:\"switch-button-icon\",attrs:{\"icon\":\"fluent:ios-arrow-right-24-filled\",\"width\":\"33px\",\"rotate\":\"1\"}})],1)],1)],1)]),_c('div',{staticClass:\"watch-panel\",on:{\"mousemove\":function($event){return _vm.controlDisplayTimer($event)},\"touchmove\":function($event){return _vm.controlDisplayTimer($event)},\"click\":function($event){return _vm.controlDisplayTimer($event)}}},[_c('div',{staticClass:\"watch-panel__header\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-close-button\",on:{\"click\":function($event){_vm.is_panel_display = false}}},[_c('Icon',{staticClass:\"panel-close-button__icon\",attrs:{\"icon\":\"akar-icons:chevron-right\",\"width\":\"25px\"}}),_c('span',{staticClass:\"panel-close-button__text\"},[_vm._v(\"閉じる\")])],1),_c('v-spacer'),_c('div',{staticClass:\"panel-broadcaster\"},[_c('img',{staticClass:\"panel-broadcaster__icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('div',{staticClass:\"panel-broadcaster__number\"},[_vm._v(_vm._s(_vm.channel.channel_number))]),_c('div',{staticClass:\"panel-broadcaster__name\"},[_vm._v(_vm._s(_vm.channel.channel_name))])])],1),_c('div',{staticClass:\"watch-panel__content-container\"},[_c('Program',{staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Program'},attrs:{\"channel\":_vm.channel}}),_c('Channel',{staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Channel'},attrs:{\"channels_list\":_vm.channels_list}}),_c('Comment',{ref:\"Comment\",staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Comment'},attrs:{\"channel\":_vm.channel,\"player\":_vm.player}}),_c('Twitter',{ref:\"Twitter\",staticClass:\"watch-panel__content\",class:{'watch-panel__content--active': _vm.tv_panel_active_tab === 'Twitter'},attrs:{\"channel\":_vm.channel,\"player\":_vm.player,\"is_virtual_keyboard_display\":_vm.is_virtual_keyboard_display},on:{\"panel_folding_requested\":function($event){_vm.is_panel_display = false}}})],1),_c('div',{staticClass:\"watch-panel__navigation\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Program'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Program'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-solid:info-circle\",\"width\":\"33px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"番組情報\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Channel'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Channel'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-solid:broadcast-tower\",\"width\":\"34px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"チャンネル\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Comment'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Comment'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"29px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"コメント\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"panel-navigation-button\",class:{'panel-navigation-button--active': _vm.tv_panel_active_tab === 'Twitter'},on:{\"click\":function($event){_vm.tv_panel_active_tab = 'Twitter'}}},[_c('Icon',{staticClass:\"panel-navigation-button__icon\",attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"34px\"}}),_c('span',{staticClass:\"panel-navigation-button__text\"},[_vm._v(\"Twitter\")])],1)])])]),_c('v-dialog',{attrs:{\"max-width\":\"1000\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.shortcut_key_modal),callback:function ($$v) {_vm.shortcut_key_modal=$$v},expression:\"shortcut_key_modal\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"px-5 pt-4 pb-3 d-flex align-center font-weight-bold\"},[_c('Icon',{attrs:{\"icon\":\"fluent:keyboard-20-filled\",\"height\":\"28px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"キーボードショートカット\")]),_c('v-spacer'),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"d-flex align-center rounded-circle cursor-pointer px-2 py-2\",on:{\"click\":function($event){_vm.shortcut_key_modal = false}}},[_c('Icon',{attrs:{\"icon\":\"fluent:dismiss-12-filled\",\"width\":\"23px\",\"height\":\"23px\"}})],1)],1),_c('div',{staticClass:\"px-5 pb-4\"},[_c('v-row',_vm._l((_vm.shortcut_key_list),function(shortcut_key_column,shortcut_key_column_name){return _c('v-col',{key:shortcut_key_column_name,attrs:{\"cols\":\"6\"}},_vm._l((shortcut_key_column),function(shortcut_keys){return _c('div',{key:shortcut_keys.name,staticClass:\"mt-3\"},[_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold\"},[_c('Icon',{attrs:{\"icon\":shortcut_keys.icon,\"height\":shortcut_keys.icon_height}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(_vm._s(shortcut_keys.name))])],1),_vm._l((shortcut_keys.shortcuts),function(shortcut){return _c('div',{key:shortcut.name,staticClass:\"mt-3\"},[_c('div',{staticClass:\"text-subtitle-2 mt-2 d-flex align-center font-weight-medium\"},[_c('span',{staticClass:\"mr-2\",domProps:{\"innerHTML\":_vm._s(shortcut.name)}}),_c('div',{staticClass:\"ml-auto d-flex align-center flex-shrink-0\"},_vm._l((shortcut.keys),function(key,index){return _c('div',{key:key.name,staticClass:\"ml-auto d-flex align-center\"},[_c('span',{staticClass:\"shortcut-key\"},[_vm._l((key.name.split(';')),function(key_name){return _c('Icon',{directives:[{name:\"show\",rawName:\"v-show\",value:(key.icon === true),expression:\"key.icon === true\"}],key:key_name,attrs:{\"icon\":key_name,\"height\":\"18px\"}})}),(key.icon === false)?_c('span',{domProps:{\"innerHTML\":_vm._s(key.name)}}):_vm._e()],2),(index < (shortcut.keys.length - 1))?_c('span',{staticClass:\"shortcut-key-plus\"},[_vm._v(\"+\")]):_vm._e()])}),0)])])})],2)}),0)}),1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n// 番組情報を表すインターフェイス\nexport interface IProgram {\n id: string;\n network_id: number;\n service_id: number;\n event_id: number;\n channel_id: string;\n title: string;\n description: string;\n detail: {[key: string]: string};\n start_time: string;\n end_time: string;\n duration: number;\n is_free: boolean;\n genre: {major: string; middle: string}[];\n video_type: string;\n video_codec: string;\n video_resolution: string;\n primary_audio_type: string;\n primary_audio_language: string;\n primary_audio_sampling_rate: string;\n secondary_audio_type: string | null;\n secondary_audio_language: string | null;\n secondary_audio_sampling_rate: string | null;\n}\n\n// 番組情報を表すインターフェイスのデフォルト値\nexport const IProgramDefault: IProgram = {\n id: 'NID0-SID0',\n network_id: 0,\n service_id: 0,\n event_id: 0,\n channel_id: 'gr000',\n title: '取得中…',\n description: '取得中…',\n detail: {},\n start_time: '2000-01-01T00:00:00+09:00',\n end_time: '2000-01-01T00:00:00+09:00',\n duration: 0,\n is_free: true,\n genre: [],\n video_type: '映像1080i(1125i)、アスペクト比16:9 パンベクトルなし',\n video_codec: 'mpeg2',\n video_resolution: '1080i',\n primary_audio_type: '2/0モード(ステレオ)',\n primary_audio_language: '日本語',\n primary_audio_sampling_rate: '48kHz',\n secondary_audio_type: null,\n secondary_audio_language: null,\n secondary_audio_sampling_rate: null,\n}\n\n// チャンネルタイプの型\nexport type ChannelType = 'GR' | 'BS' | 'CS' | 'CATV' | 'SKY' | 'STARDIGIO';\n\n// チャンネルタイプの型 (実際のチャンネルリストに表示される表現)\nexport type ChannelTypePretty = 'ピン留め' | '地デジ' | 'BS' | 'CS' | 'CATV' | 'SKY' | 'StarDigio';\n\n// チャンネル情報を表すインターフェイス\nexport interface IChannel {\n id: string;\n network_id: number;\n service_id: number;\n transport_stream_id: number | null;\n remocon_id: number | null;\n channel_id: string;\n channel_number: string;\n channel_name: string;\n channel_type: ChannelType;\n channel_force: number | null;\n channel_comment: number | null;\n is_subchannel: boolean;\n is_radiochannel: boolean;\n is_display: boolean;\n viewers: number;\n program_present: IProgram;\n program_following: IProgram;\n}\n\n// チャンネル情報を表すインターフェイスのデフォルト値\nexport const IChannelDefault: IChannel = {\n id: 'NID0-SID0',\n network_id: 0,\n service_id: 0,\n transport_stream_id: null,\n remocon_id: null,\n channel_id: 'gr000',\n channel_number: '---',\n channel_name: '取得中…',\n channel_type: 'GR',\n channel_force: null,\n channel_comment: null,\n is_subchannel: false,\n is_radiochannel: false,\n is_display: true,\n viewers: 0,\n program_present: IProgramDefault,\n program_following: IProgramDefault,\n}\n\n// ユーザーアカウントに紐づく Twitter アカウントの情報を表すインターフェイス\nexport interface ITwitterAccount {\n id: number;\n name: string;\n screen_name: string;\n icon_url: string;\n created_at: string;\n updated_at: string;\n}\n\n// ユーザーアカウントの情報を表すインターフェイス\nexport interface IUser {\n id: number;\n name: string;\n is_admin: boolean;\n niconico_user_id: number | null;\n niconico_user_name: string | null;\n niconico_user_premium: boolean | null;\n twitter_accounts: ITwitterAccount[];\n created_at: string;\n updated_at: string;\n}\n\n// バージョン情報を表すインターフェイス\nexport interface IVersionInformation {\n version: string;\n latest_version: string;\n environment: 'Windows' | 'Linux' | 'Linux-Docker';\n backend: 'EDCB' | 'Mirakurun';\n encoder: 'FFmpeg' | 'QSVEncC' | 'NVEncC' | 'VCEEncC';\n}\n\n// DPlayer のコメントデータの型\n// KonomiTV で使うプロパティのみ定義している\n// ref: https://github.com/tsukumijima/DPlayer/blob/master/src/js/danmaku.js#L86-L96\nexport interface IDPlayerDanmaku {\n author: string;\n time: number;\n text: string;\n color: string;\n type: 'top' | 'right' | 'bottom';\n size: 'big' | 'medium' | 'small';\n}\n\n// コメントを送信する際に DPlayer から受け取るオプションの型\n// KonomiTV で使うプロパティのみ定義している\n// ref: https://github.com/tsukumijima/DPlayer/blob/master/src/js/danmaku.js#L98-L121\nexport interface IDPlayerDanmakuSendOptions {\n data: IDPlayerDanmaku;\n success: () => void;\n error: (message: string | undefined) => void;\n}\n\n// キャプチャに書き込む EXIF メタデータの型\nexport interface ICaptureExifData {\n captured_at: string;\n captured_playback_position: number;\n network_id: number;\n service_id: number;\n event_id: number;\n title: string;\n description: string;\n start_time: string;\n end_time: string;\n duration: number;\n caption_text: string | null;\n is_caption_composited: boolean;\n is_comment_composited: boolean;\n}\n\n// ミュート済みのコメントのキーワードが入るリスト\nexport interface IMutedCommentKeywords {\n match: 'partial' | 'forward' | 'backward' | 'exact' | 'regex';\n pattern: string;\n}\n","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"channels-container channels-container--watch\"},[_c('v-tabs-fix',{staticClass:\"channels-tab\",attrs:{\"centered\":\"\",\"show-arrows\":\"\"},model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type,]){return _c('v-tab',{key:channels_type,staticClass:\"channels-tab__item\"},[_vm._v(\" \"+_vm._s(channels_type)+\" \")])}),1),_c('div',{staticClass:\"channels-list-container\"},[_c('v-tabs-items-fix',{staticClass:\"channels-list\",model:{value:(_vm.tab),callback:function ($$v) {_vm.tab=$$v},expression:\"tab\"}},_vm._l((Array.from(_vm.channels_list)),function([channels_type, channels]){return _c('v-tab-item-fix',{key:channels_type,staticClass:\"channels\"},_vm._l((channels),function(channel){return _c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:channel.id,staticClass:\"channel\",attrs:{\"to\":`/tv/watch/${channel.channel_id}`}},[_c('div',{staticClass:\"channel__broadcaster\"},[_c('img',{staticClass:\"channel__broadcaster-icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${channel.channel_id}/logo`}}),_c('div',{staticClass:\"channel__broadcaster-content\"},[_c('span',{staticClass:\"channel__broadcaster-name\"},[_vm._v(\"Ch: \"+_vm._s(channel.channel_number)+\" \"+_vm._s(channel.channel_name))]),_c('div',{staticClass:\"channel__broadcaster-force\",class:`channel__broadcaster-force--${_vm.ChannelUtils.getChannelForceType(channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"11px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(channel, 'channel_force', '-')))])],1)])]),_c('div',{staticClass:\"channel__program-present\"},[_c('span',{staticClass:\"channel__program-present-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_present, 'title'))}}),_c('span',{staticClass:\"channel__program-present-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_present)))])]),_c('div',{staticClass:\"channel__program-following\"},[_c('div',{staticClass:\"channel__program-following-title\"},[_c('span',{staticClass:\"channel__program-following-title-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"channel__program-following-title-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}}),_c('span',{staticClass:\"channel__program-following-title-text\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(channel.program_following, 'title'))}})],1),_c('span',{staticClass:\"channel__program-following-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(channel.program_following)))])]),_c('div',{staticClass:\"channel__progressbar\"},[_c('div',{staticClass:\"channel__progressbar-progress\",style:(`width:${_vm.ProgramUtils.getProgramProgress(channel.program_present)}%;`)})])])}),1)}),1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { ChannelTypePretty, IChannel } from '@/interface';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Panel-ChannelTab',\n props: {\n // チャンネル情報リスト\n channels_list: {\n type: Map as PropType>,\n required: true,\n }\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n\n // タブの状態管理\n tab: null,\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Channel.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Channel.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Channel.vue?vue&type=template&id=f0f56584&scoped=true&\"\nimport script from \"./Channel.vue?vue&type=script&lang=ts&\"\nexport * from \"./Channel.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Channel.vue?vue&type=style&index=0&id=f0f56584&prod&lang=scss&\"\nimport style1 from \"./Channel.vue?vue&type=style&index=1&id=f0f56584&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"f0f56584\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"comment-container\"},[_c('section',{staticClass:\"comment-header\"},[_c('h2',{staticClass:\"comment-header__title\"},[_c('Icon',{staticClass:\"comment-header__title-icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"height\":\"18.5px\"}}),_c('span',{staticClass:\"comment-header__title-text\"},[_vm._v(\"コメント\")])],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"comment-header__button ml-auto\",on:{\"click\":function($event){_vm.comment_mute_settings_modal = !_vm.comment_mute_settings_modal}}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"11px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ミュート設定\")])],1)]),_c('section',{ref:\"comment_list_wrapper\",staticClass:\"comment-list-wrapper\"},[_c('div',{staticClass:\"comment-list-dropdown\",class:{'comment-list-dropdown--display': _vm.is_comment_list_dropdown_display},style:({'--comment-list-dropdown-top': `${_vm.comment_list_dropdown_top}px`})},[_c('v-list',{staticStyle:{\"background\":\"var(--v-background-lighten1)\"}},[_c('v-list-item',{staticStyle:{\"min-height\":\"30px\"},attrs:{\"dense\":\"\"},on:{\"click\":function($event){_vm.addMutedKeywords(_vm.comment_list_dropdown_comment.text); _vm.is_comment_list_dropdown_display = false}}},[_c('v-list-item-title',{staticClass:\"d-flex align-center\"},[_c('Icon',{attrs:{\"icon\":\"fluent:comment-dismiss-20-filled\",\"width\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"このコメントをミュート\")])],1)],1),_c('v-list-item',{staticStyle:{\"min-height\":\"30px\"},attrs:{\"dense\":\"\"},on:{\"click\":function($event){_vm.addMutedNiconicoUserIDs(_vm.comment_list_dropdown_comment.user_id); _vm.is_comment_list_dropdown_display = false}}},[_c('v-list-item-title',{staticClass:\"d-flex align-center\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-prohibited-20-filled\",\"width\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"このコメントの投稿者をミュート\")])],1)],1)],1)],1),_c('div',{staticClass:\"comment-list-cover\",class:{'comment-list-cover--display': _vm.is_comment_list_dropdown_display},on:{\"click\":function($event){_vm.is_comment_list_dropdown_display = false}}}),_c('DynamicScroller',{staticClass:\"comment-list\",attrs:{\"direction\":'vertical',\"items\":_vm.comment_list,\"min-item-size\":34},scopedSlots:_vm._u([{key:\"default\",fn:function({item, active}){return [_c('DynamicScrollerItem',{attrs:{\"item\":item,\"active\":active,\"size-dependencies\":[item.text]}},[_c('div',{staticClass:\"comment\",class:{'comment--my-post': item.my_post}},[_c('span',{staticClass:\"comment__text\"},[_vm._v(_vm._s(item.text))]),_c('span',{staticClass:\"comment__time\"},[_vm._v(_vm._s(item.time))]),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\",value:(!_vm.Utils.isTouchDevice()),expression:\"!Utils.isTouchDevice()\"}],staticClass:\"comment__icon\",on:{\"mouseup\":function($event){return _vm.displayCommentListDropdown($event, item)},\"touchend\":function($event){return _vm.displayCommentListDropdown($event, item)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:more-vertical-20-filled\",\"width\":\"20px\"}})],1)])])]}}])}),(_vm.initialize_failed_message === null && _vm.comment_list.length === 0)?_c('div',{staticClass:\"comment-announce\"},[_c('div',{staticClass:\"comment-announce__heading\"},[_vm._v(\"まだコメントがありません。\")]),_vm._m(0)]):_vm._e(),(_vm.initialize_failed_message !== null && _vm.comment_list.length === 0)?_c('div',{staticClass:\"comment-announce\"},[_c('div',{staticClass:\"comment-announce__heading\"},[_vm._v(\"コメントがありません。\")]),_c('div',{staticClass:\"comment-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(_vm._s(_vm.initialize_failed_message))])])]):_vm._e()],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"comment-scroll-button elevation-5\",class:{'comment-scroll-button--display': _vm.is_manual_scroll},on:{\"click\":function($event){_vm.is_manual_scroll = false; _vm.scrollCommentList(true);}}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-down-12-filled\",\"height\":\"29px\"}})],1),_c('CommentMuteSettings',{model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}})],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"comment-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(\"このチャンネルに対応するニコニコ実況のコメントが、リアルタイムで表示されます。\")])])\n}]\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('v-dialog',{attrs:{\"max-width\":\"740\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"px-5 pt-5 pb-3 d-flex align-center font-weight-bold\",staticStyle:{\"height\":\"60px\"}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"26px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"コメントのミュート設定\")]),_c('v-spacer'),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"d-flex align-center rounded-circle cursor-pointer px-2 py-2\",on:{\"click\":function($event){_vm.comment_mute_settings_modal = false}}},[_c('Icon',{attrs:{\"icon\":\"fluent:dismiss-12-filled\",\"width\":\"23px\",\"height\":\"23px\"}})],1)],1),_c('div',{staticClass:\"px-5 pb-5\"},[_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:comment-dismiss-20-filled\",\"width\":\"24px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"ミュート済みのキーワード\")]),_c('v-btn',{staticClass:\"ml-auto\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.muted_comment_keywords.push({id: Date.now(), match: 'partial', pattern: ''})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"height\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)],1),_c('div',{staticClass:\"muted-comment-items\"},_vm._l((_vm.muted_comment_keywords),function(muted_comment_keyword){return _c('div',{key:muted_comment_keyword.id,staticClass:\"muted-comment-item\"},[_c('v-text-field',{staticClass:\"muted-comment-item__input\",attrs:{\"type\":\"search\",\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"ミュートするキーワードを入力\"},model:{value:(muted_comment_keyword.pattern),callback:function ($$v) {_vm.$set(muted_comment_keyword, \"pattern\", $$v)},expression:\"muted_comment_keyword.pattern\"}}),_c('v-select',{staticClass:\"muted-comment-item__match-type\",attrs:{\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"items\":_vm.muted_comment_keyword_match_type},model:{value:(muted_comment_keyword.match),callback:function ($$v) {_vm.$set(muted_comment_keyword, \"match\", $$v)},expression:\"muted_comment_keyword.match\"}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"muted-comment-item__delete-button\",on:{\"click\":function($event){_vm.muted_comment_keywords.splice(_vm.muted_comment_keywords.indexOf(muted_comment_keyword), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"20px\"}})],1)],1)}),0),_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-prohibited-20-filled\",\"width\":\"24px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"ミュート済みのニコニコユーザー ID\")]),_c('v-btn',{staticClass:\"ml-auto\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.muted_niconico_user_ids.push({id: Date.now(), user_id: ''})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"height\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)],1),_c('div',{staticClass:\"muted-comment-items\"},_vm._l((_vm.muted_niconico_user_ids),function(muted_niconico_user_id){return _c('div',{key:muted_niconico_user_id.id,staticClass:\"muted-comment-item\"},[_c('v-text-field',{staticClass:\"muted-comment-item__input\",attrs:{\"type\":\"search\",\"dense\":\"\",\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"ミュートするニコニコユーザー ID を入力\"},model:{value:(muted_niconico_user_id.user_id),callback:function ($$v) {_vm.$set(muted_niconico_user_id, \"user_id\", $$v)},expression:\"muted_niconico_user_id.user_id\"}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"muted-comment-item__delete-button\",on:{\"click\":function($event){_vm.muted_niconico_user_ids.splice(_vm.muted_niconico_user_ids.indexOf(muted_niconico_user_id), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"20px\"}})],1)],1)}),0),_c('div',{staticClass:\"text-subtitle-1 d-flex align-center font-weight-bold mt-4\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"24px\",\"height\":\"20px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"クイック設定\")])],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_vulgar_comments\"}},[_vm._v(\" 露骨な表現を含むコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_vulgar_comments\"}},[_vm._v(\" 性的な単語などの露骨・下品な表現を含むコメントを、一括でミュートするかを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_vulgar_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_vulgar_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_vulgar_comments\", $$v)},expression:\"settings.mute_vulgar_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_abusive_discriminatory_prejudiced_comments\"}},[_vm._v(\" 罵倒や誹謗中傷、差別的な表現、政治的に偏った表現を含むコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_abusive_discriminatory_prejudiced_comments\"}},[_vm._v(\" 『死ね』『殺す』などの罵倒や誹謗中傷、特定の国や人々への差別的な表現、政治的に偏った表現を含むコメントを、一括でミュートするかを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_abusive_discriminatory_prejudiced_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_abusive_discriminatory_prejudiced_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_abusive_discriminatory_prejudiced_comments\", $$v)},expression:\"settings.mute_abusive_discriminatory_prejudiced_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_big_size_comments\"}},[_vm._v(\" 文字サイズが大きいコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_big_size_comments\"}},[_vm._v(\" 通常より大きい文字サイズで表示されるコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" 文字サイズが大きいコメントには迷惑なコメントが多いです。基本的にはオンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_big_size_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_big_size_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_big_size_comments\", $$v)},expression:\"settings.mute_big_size_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_fixed_comments\"}},[_vm._v(\" 映像の上下に固定表示されるコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_fixed_comments\"}},[_vm._v(\" 映像の上下に固定された状態で表示されるコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" 固定表示されるコメントが煩わしいと感じる方は、オンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_fixed_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_fixed_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_fixed_comments\", $$v)},expression:\"settings.mute_fixed_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_colored_comments\"}},[_vm._v(\" 色付きのコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_colored_comments\"}},[_vm._v(\" 白以外の色で表示される色付きのコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" この設定をオンにしておくと、目立つ色のコメントを一掃できます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_colored_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_colored_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_colored_comments\", $$v)},expression:\"settings.mute_colored_comments\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"mute_consecutive_same_characters_comments\"}},[_vm._v(\" 8文字以上同じ文字が連続しているコメントをミュートする \")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"mute_consecutive_same_characters_comments\"}},[_vm._v(\" 『wwwwwwwwwww』『あばばばばばばばばば』など、8文字以上同じ文字が連続しているコメントを、一括でミュートするかを設定します。\"),_c('br'),_vm._v(\" しばしばあるテンプレコメントが煩わしいと感じる方は、オンにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"mute_consecutive_same_characters_comments\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.mute_consecutive_same_characters_comments),callback:function ($$v) {_vm.$set(_vm.settings, \"mute_consecutive_same_characters_comments\", $$v)},expression:\"settings.mute_consecutive_same_characters_comments\"}})],1)])],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { IMutedCommentKeywords } from '@/interface';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'CommentMuteSettings',\n // カスタム v-model を実装する\n // ref: https://jp.vuejs.org/v2/guide/components-custom-events.html\n model: {\n prop: 'showing', // v-model で渡された値が \"showing\" props に入る\n event: 'change' // \"change\" イベントで親コンポーネントに反映\n },\n props: {\n // コメントのミュート設定のモーダルを表示するか\n showing: {\n type: Boolean as PropType,\n required: true,\n }\n },\n data() {\n return {\n\n // インターバルのタイマー ID\n interval_timer_id: 0,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n\n // ミュート済みのキーワードが入るリスト\n muted_comment_keywords: (Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[]).map((keyword, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {\n id: Date.now() + index,\n match: keyword.match as ('partial' | 'forward' | 'backward' | 'exact' | 'regex'),\n pattern: keyword.pattern as string,\n };\n }),\n\n // ミュート済みのキーワードのマッチタイプ\n muted_comment_keyword_match_type: [\n {text: '部分一致', value: 'partial'},\n {text: '前方一致', value: 'forward'},\n {text: '後方一致', value: 'backward'},\n {text: '完全一致', value: 'exact'},\n {text: '正規表現', value: 'regex'},\n ],\n\n // ミュート済みのニコニコユーザー ID が入るリスト\n muted_niconico_user_ids: (Utils.getSettingsItem('muted_niconico_user_ids') as string[]).map((user_id, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {\n id: Date.now() + index,\n user_id: user_id,\n };\n }),\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {}\n const setting_keys = [\n 'mute_vulgar_comments',\n 'mute_abusive_discriminatory_prejudiced_comments',\n 'mute_big_size_comments',\n 'mute_fixed_comments',\n 'mute_colored_comments',\n 'mute_consecutive_same_characters_comments',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n created() {\n // 1秒に1回、muted_comment_keywords と muted_niconico_user_ids の変更内容を同期する\n // コメントリストからのミュート設定の変更を反映するために必要\n this.interval_timer_id = window.setInterval(() => {\n const new_muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n if (JSON.stringify(this.muted_comment_keywords) !== JSON.stringify(new_muted_comment_keywords)) {\n this.muted_comment_keywords = (new_muted_comment_keywords).map((keyword, index) => {\n return {\n id: Date.now() + index,\n match: keyword.match as ('partial' | 'forward' | 'backward' | 'exact' | 'regex'),\n pattern: keyword.pattern as string,\n };\n });\n }\n const new_muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n if (JSON.stringify(this.muted_niconico_user_ids.map((muted_niconico_user_id) => muted_niconico_user_id.user_id)) !==\n JSON.stringify(new_muted_niconico_user_ids)) {\n this.muted_niconico_user_ids = (new_muted_niconico_user_ids).map((user_id, index) => {\n return {\n id: Date.now() + index,\n user_id: user_id,\n };\n });\n }\n }, 1000);\n },\n beforeDestroy() {\n // インスタンスの破棄前にタイマーを解除する\n window.clearInterval(this.interval_timer_id);\n },\n watch: {\n\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n },\n\n // ミュート済みのキーワードが変更されたら随時 LocalStorage に保存する\n muted_comment_keywords: {\n deep: true,\n handler() {\n Utils.setSettingsItem('muted_comment_keywords', this.muted_comment_keywords.map((muted_comment_keyword) => {\n delete muted_comment_keyword.id;\n return muted_comment_keyword;\n }));\n }\n },\n\n // ミュート済みのニコニコユーザー ID が変更されたら随時 LocalStorage に保存する\n muted_niconico_user_ids: {\n deep: true,\n handler() {\n Utils.setSettingsItem('muted_niconico_user_ids', this.muted_niconico_user_ids.map((muted_niconico_user_id) => {\n return muted_niconico_user_id.user_id;\n }));\n }\n },\n\n // showing (親コンポーネント側) の変更を監視し、変更されたら comment_mute_settings_modal に反映する\n showing() {\n this.comment_mute_settings_modal = this.showing as boolean;\n },\n\n // comment_mute_settings_modal (子コンポーネント側) の変更を監視し、変更されたら this.$emit() で親コンポーネントに伝える\n comment_mute_settings_modal() {\n this.$emit('change', this.comment_mute_settings_modal);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CommentMuteSettings.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./CommentMuteSettings.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./CommentMuteSettings.vue?vue&type=template&id=7179f41e&scoped=true&\"\nimport script from \"./CommentMuteSettings.vue?vue&type=script&lang=ts&\"\nexport * from \"./CommentMuteSettings.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./CommentMuteSettings.vue?vue&type=style&index=0&id=7179f41e&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"7179f41e\",\n null\n \n)\n\nexport default component.exports","\n\nimport { AxiosResponse } from 'axios';\nimport { Buffer } from 'buffer';\nimport dayjs from 'dayjs';\nimport Vue, { PropType } from 'vue';\n\nimport { IChannel, IDPlayerDanmakuSendOptions, IMutedCommentKeywords, IUser } from '@/interface';\nimport CommentMuteSettings from '@/components/Settings/CommentMuteSettings.vue';\nimport Utils from '@/utils';\n\n// このコンポーネント内でのコメントのインターフェイス\ninterface IComment {\n id: number;\n text: string;\n time: string;\n user_id: string;\n my_post: boolean;\n}\n\n// 「露骨な表現を含むコメントをミュートする」のフィルタ正規表現\nconst mute_vulgar_comments_pattern = new RegExp(Buffer.from('cHJwcnzvvZDvvZLvvZDvvZJ8U0VYfFPjgIdYfFPil69YfFPil4tYfFPil49YfO+8s++8pe+8uHzvvLPjgIfvvLh877yz4pev77y4fO+8s+KXi++8uHzvvLPil4/vvLh844Ki44OA44Or44OIfOOCouODiuOCpXzjgqLjg4rjg6t844Kk44Kr6IetfOOCpOOBj3zjgYbjgpPjgZN844Km44Oz44KzfOOBhuOCk+OBoXzjgqbjg7Pjg4F844Ko44Kt44ObfOOBiOOBoeOBiOOBoXzjgYjjgaPjgaF844Ko44OD44OBfOOBiOOBo+OCjXzjgqjjg4Pjg61844GI44KNfOOCqOODrXzlt6Xlj6N844GK44GV44KP44KK44G+44KTfOOBiuOBl+OBo+OBk3zjgqrjgrfjg4PjgrN844Kq44OD44K144OzfOOBiuOBo+OBseOBhHzjgqrjg4Pjg5HjgqR844Kq44OK44OL44O8fOOBiuOBquOBu3zjgqrjg4rjg5t844GK44Gx44GEfOOCquODkeOCpHzjgYpwfOOBiu+9kHzjgqrjg5Xjg5HjgrN844Ks44Kk44K444OzfOOCreODs+OCv+ODnnzjgY/jgbHjgYJ844GP44Gx44GBfOOCr+ODquODiOODquOCuXzjgq/jg7Pjg4t844GU44GP44GU44GP44GU44GP44GU44GPfOOCs+ODs+ODieODvOODoHzjgrbjg7zjg6Hjg7N844K344KzfOOBl+OBk+OBl+OBk3zjgrfjgrPjgrfjgrN844GZ44GR44GZ44GRfOOBm+OBhOOBiOOBjXzjgZnjgYXjgYXjgYXjgYXjgYV844GZ44GG44GG44GG44GG44GGfOOCu+OCr+ODreOCuXzjgrvjg4Pjgq/jgrl844K744OV44OsfOOBoeOBo+OBseOBhHzjgaHjgaPjg5HjgqR844OB44OD44OR44KkfOOBoeOCk+OBk3zjgaHjgIfjgZN844Gh4pev44GTfOOBoeKXi+OBk3zjgaHil4/jgZN844OB44Oz44KzfOODgeOAh+OCs3zjg4Hil6/jgrN844OB4peL44KzfOODgeKXj+OCs3zjgaHjgpPjgb1844Gh44CH44G9fOOBoeKXr+OBvXzjgaHil4vjgb1844Gh4peP44G9fOODgeODs+ODnXzjg4HjgIfjg51844OB4pev44OdfOODgeKXi+ODnXzjg4Hil4/jg51844Gh44KT44Gh44KTfOODgeODs+ODgeODs3zjgabjgYPjgpPjgabjgYPjgpN844OG44Kj44Oz44OG44Kj44OzfOODhuOCo+ODs+ODnXzjg4fjgqvjgYR844OH44Oq44OY44OrfOiEseOBknzjgbHjgYTjgoLjgb9844OR44OR5rS7fOOBteOBhuODu3zjgbXjgYbigKZ844G144GFfO++jO+9qXzjgbXjgY/jgonjgb/jgYvjgZF844G144GP44KJ44KT44GnfOOBuuOBo+OBn3zjgbrjgo3jgbrjgo1844Oa44Ot44Oa44OtfO++je++n+++m+++je++n+++m3zjg5Xjgqfjg6l844G844Gj44GNfOODneODq+ODjnzjgbzjgo3jgpN844Oc44Ot44OzfO++ju++nu++m+++nXzjgb3jgo3jgop844Od44Ot44OqfO++ju++n+++m+++mHzjg57jg7PjgY3jgaR844Oe44Oz44Kt44OEfOOBvuOCk+OBk3zjgb7jgIfjgZN844G+4pev44GTfOOBvuKXi+OBk3zjgb7il4/jgZN844Oe44Oz44KzfOODnuOAh+OCs3zjg57il6/jgrN844Oe4peL44KzfOODnuKXj+OCs3zjgb7jgpPjgZXjgpN844KC44Gj44GT44KKfOODouODg+OCs+ODqnzjgoLjgb/jgoLjgb9844Oi44Of44Oi44OffOODpOOBo+OBn3zjg6TjgaPjgaZ844Ok44KJfOOChOOCieOBm+OCjXzjg6Tjgop844Ok44KLfOODpOOCjHzjg6Tjgo1844Op44OW44ObfOODr+ODrOODoXzllph86Zmw5qC4fOmZsOiMjnzpmbDllId85rer5aSifOmZsOavm3znlKPjgoHjgot85aWz44Gu5a2Q44Gu5pelfOaxmuOBo+OBleOCk3zlp6Z86aiO5LmX5L2NfOmHkeeOiXzmnIjntYx85b6M6IOM5L2NfOWtkOS9nOOCinzlsITnsr585L+h6ICFfOeyvua2snzpgI/jgZF85oCn5LqkfOeyvuWtkHzmraPluLjkvY185oCn5b60fOaAp+eahHznlJ/nkIZ85a+45q2i44KBfOe0oOadkHzmirHjgYR85oqx44GLfOaKseOBjXzmirHjgY985oqx44GRfOaKseOBk3zkubPpppZ85oGl5Z6ifOS4reOBoOOBl3zkuK3lh7rjgZd85bC/fOaKnOOBhHzmipzjgZHjgarjgYR85oqc44GR44KLfOaKnOOBkeOCjHzohqjjgol85YuD6LW3fOaPieOBvnzmj4njgb985o+J44KAfOaPieOCgXzmvKvmuZZ844CH772efOKXr++9nnzil4vvvZ584peP772efOOAh+ODg+OCr+OCuXzil6/jg4Pjgq/jgrl84peL44OD44Kv44K5fOKXj+ODg+OCr+OCuQ==', 'base64').toString());\n\n// 「罵倒や差別的な表現を含むコメントをミュートする」のフィルタ正規表現\nconst mute_abusive_discriminatory_prejudiced_comments_pattern = new RegExp(Buffer.from('44CCfOOCouOCueODmnzjgqTjgqvjgox844Kk44Op44Gk44GPfOOCpuOCuHzjgqbjg7zjg6h844Km44OofOOCpuODqOOCr3zjgqbjg7J844GN44KC44GEfOOCreODouOCpHzjgq3jg6LjgYR844KtL+ODoC/jg4F844Ks44Kk44K4fO+9tu++nu+9su+9vO++nnzjgqzjgq1844Kr44K5fOOCreODg+OCunzjgY3jgaHjgYzjgYR844Kt44OB44Ks44KkfOOCreODoOODgXzjg4Hjg6fjg7N85Y2D44On44OzfOOBpOOCk+OBvHzjg4Tjg7Pjg5x844ON44OI44Km44OofOOBq+OBoOOBguOBgnzjg4vjg4B8776G776A776efOODkeODvOODqHzjg5Hjg6h844OR44Oo44KvfOOBtuOBo+OBlXzjg5bjg4PjgrV844G244GV44GEfOODluOCteOCpHzjgb7jgazjgZF844Oh44Kv44OpfOODkOOCq3zjg6DjgqvjgaTjgY986bq755Sf44K744Oh44Oz44OIfOaFsOWuieWppnzlrrPlhZB85aSW5a2XfOWnpuWbvXzpn5Plm7186Z+T5LitfOmfk+aXpXzln7rlnLDlpJZ85rCX5oyB44Gh5oKqfOWbveS6pOaWree1tnzmrrp86aCD44GZfOWcqOaXpXzlj4LmlL/mqKl85q2744GtfOawj+OBrXzvvoDvvot85q255YyVfOatueODknzpmpzlrrN85pat5LqkfOS4remfk3zmnJ3prq585b6055So5belfOWjunzml6Xpn5N85pel5bidfOeymOedgHzlj43ml6V86aas6bm/fOeZuumBlHzmnLR85aOy5Zu9fOS4jeW/q3zplpPmipzjgZF86Z2W5Zu9', 'base64').toString());\n\nexport default Vue.extend({\n name: 'Panel-CommentTab',\n components: {\n CommentMuteSettings,\n },\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n },\n // プレイヤーのインスタンス\n player: {\n type: null as PropType, // 代入当初は null になるため苦肉の策\n required: true,\n }\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // 手動スクロール状態かどうか\n is_manual_scroll: false,\n\n // 自動スクロール中かどうか\n // 自動スクロール中の場合、scroll イベントが発火しても無視する\n is_auto_scrolling: false,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // コメントリストの配列\n comment_list: [] as IComment[],\n\n // コメントリストの要素\n comment_list_element: null as HTMLElement | null,\n\n // コメントリストのドロップダウン関連\n is_comment_list_dropdown_display: false as boolean,\n comment_list_dropdown_top: 0 as number,\n comment_list_dropdown_comment: null as IComment | null,\n\n // 視聴セッションの WebSocket のインスタンス\n watch_session: null as WebSocket | null,\n\n // コメントセッションの WebSocket のインスタンス\n comment_session: null as WebSocket | null,\n\n // 視聴セッション・コメントセッションの初期化に失敗した際のエラーメッセージ\n // 視聴中チャンネルのニコニコ実況がないときなどに発生する\n initialize_failed_message: null as string | null,\n\n // vpos を計算する基準となる時刻のタイムスタンプ\n vpos_base_timestamp: 0,\n\n // 座席維持用のタイマーのインターバル ID\n keep_seat_interval_id: 0,\n\n // ResizeObserver のインスタンス\n resize_observer: null as ResizeObserver | null,\n\n // ResizeObserver の監視対象の要素\n resize_observer_element: null as HTMLElement | null,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n }\n },\n // 終了前に実行\n beforeDestroy() {\n\n // destroy() を実行\n this.destroy();\n\n // ResizeObserver を終了\n if (this.resize_observer !== null) {\n this.resize_observer.unobserve(this.resize_observer_element);\n }\n },\n watch: {\n\n // チャンネル情報が変更されたとき\n // created() だとチャンネル情報の取得前に実行してしまう\n // this が変わってしまうのでアロー関数は使えない\n async channel(new_channel: IChannel, old_channel: IChannel) {\n\n // 前のチャンネル情報と次のチャンネル情報で channel_id が変わってたら\n if (new_channel.channel_id !== old_channel.channel_id) {\n\n // 0.5秒だけ待ってから\n // 連続してチャンネルを切り替えた時などに毎回コメントサーバーに接続しないように猶予を設ける\n // ただし、最初 (channel_id が gr000 の初期値になっている) だけは待たずに実行する\n if (old_channel.channel_id !== 'gr000') {\n await Utils.sleep(0.5);\n // 0.5 秒待った結果、channel_id が既に変更されているので終了\n if (this.channel.channel_id !== new_channel.channel_id) {\n return;\n }\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // コメントリストの要素を取得\n if (this.comment_list_element === null) {\n this.comment_list_element = this.$el.querySelector('.comment-list');\n }\n\n // 現在コメントリストがユーザーイベントでスクロールされているかどうか\n let is_user_scrolling = false;\n\n // mousedown → mouseup 中: スクロールバーをマウスでドラッグ\n // 残念ながらスクロールバーのドラッグ中は mousemove のイベントが発火しないため、直接 is_user_scrolling を設定する\n this.comment_list_element.onmousedown = (event: MouseEvent) => {\n // コメントリストの要素の左上を起点としたカーソルのX座標を求める\n const x = event.clientX - this.comment_list_element.getBoundingClientRect().left;\n // 座標が clientWidth 以上であれば、スクロールバー上で mousedown されたものとする\n if (x > this.comment_list_element.clientWidth) is_user_scrolling = true;\n }\n this.comment_list_element.onmouseup = (event: MouseEvent) => {\n // コメントリストの要素の左上を起点としたカーソルのX座標を求める\n const x = event.clientX - this.comment_list_element.getBoundingClientRect().left;\n // 座標が clientWidth 以上であれば、スクロールバー上で mouseup されたものとする\n if (x > this.comment_list_element.clientWidth) is_user_scrolling = false;\n }\n\n // ユーザーによるスクロールイベントで is_user_scrolling を true にする\n // 0.1 秒後に false にする(継続してイベントが発火すれば再び true になる)\n const on_user_scrolling = () => {\n is_user_scrolling = true;\n window.setTimeout(() => is_user_scrolling = false, 100);\n }\n\n // 現在コメントリストがドラッグされているかどうか\n let is_dragging = false;\n // touchstart → touchend 中: スクロールバーをタップでドラッグ\n this.comment_list_element.ontouchstart = () => is_dragging = true;\n this.comment_list_element.ontouchend = () => is_dragging = false;\n // touchmove + is_dragging 中: コメントリストをタップでドラッグしてスクロール\n this.comment_list_element.ontouchmove = () => is_dragging === true ? on_user_scrolling(): '';\n\n // wheel 中: マウスホイールの回転\n this.comment_list_element.onwheel = on_user_scrolling;\n\n // コメントリストがスクロールされた際、自動スクロール中でない&ユーザーイベントで操作されていれば、手動スクロールモードに設定\n // 手動スクロールモードでは自動スクロールを行わず、ユーザーがコメントリストをスクロールできるようにする\n this.comment_list_element.onscroll = async () => {\n\n // scroll イベントは自動スクロールでも発火してしまうので、ユーザーイベントによるスクロールかを確認しないといけない\n // 自動スクロール中かどうかは is_auto_scrolling が true のときで判定できるはずだが、\n // コメントが多くなると is_auto_scrolling が false なのに scroll イベントが遅れて発火してしまうことがある\n if (this.is_auto_scrolling === false && is_user_scrolling === true) {\n\n // 手動スクロールを有効化\n this.is_manual_scroll = true;\n\n // イベント発火時点では scrollTop の値が完全に下にスクロールされていない場合があるため、0.1秒だけ待つ\n await Utils.sleep(0.1);\n\n // 一番下までスクロールされていたら自動スクロールに戻す\n if ((this.comment_list_element.scrollTop + this.comment_list_element.offsetHeight) >\n (this.comment_list_element.scrollHeight - 10)) { // 一番下から 10px 以内\n this.is_manual_scroll = false; // 手動スクロールを無効化\n }\n }\n }\n\n // リサイズ時のイベントを初期化\n await this.initReserveObserver();\n\n // ユーザーアカウントの情報を取得\n try {\n this.user = (await Vue.axios.get('/users/me')).data;\n } catch (error) {\n this.user = null;\n }\n\n try {\n\n // 視聴セッションを初期化\n const comment_session_info = await this.initWatchSession();\n\n // vpos の基準時刻のタイムスタンプを取得\n // vpos は番組開始時間からの累計秒(10ミリ秒単位)\n this.vpos_base_timestamp = dayjs(comment_session_info['vpos_base_time']).unix() * 100;\n\n // コメントセッションを初期化\n await this.initCommentSession(comment_session_info);\n\n } catch (error) {\n\n // 初期化に失敗した場合のエラーメッセージを保存しておく\n // 初期化に失敗したのにコメントを送信しようとした際に表示するもの\n this.initialize_failed_message = error.message;\n console.error(error.toString());\n }\n }\n }\n },\n methods: {\n\n // 視聴セッションを初期化\n async initWatchSession(): Promise<{[key: string]: string | null}> {\n\n // セッション情報を取得\n let watch_session_info: AxiosResponse;\n try {\n watch_session_info = await Vue.axios.get(`/channels/${this.channel.channel_id}/jikkyo`);\n } catch (error) {\n throw new Error(error); // エラー内容をコンソールに表示して終了\n }\n\n // セッション情報を取得できなかった\n if (watch_session_info.data.is_success === false) {\n\n // 一部を除くエラーメッセージはプレイヤーにも通知する\n if ((watch_session_info.data.detail !== 'このチャンネルはニコニコ実況に対応していません。') &&\n (watch_session_info.data.detail !== '現在放送中のニコニコ実況がありません。')) {\n this.player.notice(watch_session_info.data.detail);\n }\n\n throw new Error(watch_session_info.data.detail); // エラー内容をコンソールに表示して終了\n }\n\n // イベント内で値を返すため、Promise で包む\n return new Promise((resolve) => {\n\n // 視聴セッション WebSocket を開く\n this.watch_session = new WebSocket(watch_session_info.data.audience_token);\n\n // 視聴セッション WebSocket を開いたとき\n this.watch_session.addEventListener('open', () => {\n\n // 視聴セッションをリクエスト\n // 公式ドキュメントいわく、stream フィールドは Optional らしい\n // サーバー負荷軽減のため、映像が不要な場合は必ず省略してくださいとのこと\n this.watch_session.send(JSON.stringify({\n 'type': 'startWatching',\n 'data': {\n 'reconnect': false,\n },\n }));\n });\n\n // 視聴セッション WebSocket からメッセージを受信したとき\n this.watch_session.addEventListener('message', async (event) => {\n\n // 受信したメッセージ\n const message = JSON.parse(event.data);\n\n switch (message.type) {\n\n // 部屋情報(実際には統合されていて、全てアリーナ扱いになっている)\n case 'room': {\n\n // コメントサーバーへの接続情報の入ったオブジェクトを返す\n return resolve({\n // コメントサーバーへの接続情報\n 'message_server': message.data.messageServer.uri,\n // コメントサーバー上のスレッド ID\n 'thread_id': message.data.threadId,\n // vpos を計算する基準となる時刻 (ISO8601形式)\n 'vpos_base_time': message.data.vposBaseTime,\n // メッセージサーバーから受信するコメント (chat メッセージ) に yourpost フラグを付けるためのキー\n 'your_post_key': (message.data.yourPostKey ? message.data.yourPostKey : null),\n });\n }\n\n // 座席情報\n case 'seat': {\n\n // keepIntervalSec の秒数ごとに keepSeat を送信して座席を維持する\n this.keep_seat_interval_id = window.setInterval(() => {\n // セッションがまだ開いていれば\n if (this.watch_session.readyState === 1) {\n // 座席を維持\n this.watch_session.send(JSON.stringify({\n 'type': 'keepSeat',\n }));\n // setInterval を解除\n } else {\n window.clearInterval(this.keep_seat_interval_id);\n }\n }, message.data.keepIntervalSec * 1000);\n break;\n }\n\n // ping-pong\n case 'ping': {\n\n // pong を返してセッションを維持する\n // 送り返さなかった場合、勝手にセッションが閉じられてしまう\n this.watch_session.send(JSON.stringify({\n 'type': 'pong',\n }));\n break;\n }\n\n // エラー情報\n case 'error': {\n\n // エラー情報\n let error:string;\n switch (message.data.code) {\n\n case 'CONNECT_ERROR':\n error = 'コメントサーバーに接続できません。';\n break;\n case 'CONTENT_NOT_READY':\n error = 'ニコニコ実況が配信できない状態です。';\n break;\n case 'NO_THREAD_AVAILABLE':\n error = 'コメントスレッドを取得できません。';\n break;\n case 'NO_ROOM_AVAILABLE':\n error = 'コメント部屋を取得できません。';\n break;\n case 'NO_PERMISSION':\n error = 'API にアクセスする権限がありません。';\n break;\n case 'NOT_ON_AIR':\n error = 'ニコニコ実況が放送中ではありません。';\n break;\n case 'BROADCAST_NOT_FOUND':\n error = 'ニコニコ実況の配信情報を取得できません。';\n break;\n case 'INTERNAL_SERVERERROR':\n error = 'ニコニコ実況でサーバーエラーが発生しています。';\n break;\n default:\n error = `ニコニコ実況でエラーが発生しています。(${message.data.code})`;\n break;\n }\n\n // エラー情報を表示\n console.log(`error occurred. code: ${message.data.code}`);\n if (this.player.danmaku.showing) {\n this.player.notice(error);\n }\n\n break;\n }\n\n // 再接続を求められた\n case 'reconnect': {\n\n // waitTimeSec に記載の秒数だけ待ってから再接続する\n await Utils.sleep(message.data.waitTimeSec);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n // 公式ドキュメントには reconnect で送られてくる audienceToken で再接続しろと書いてあるんだけど、\n // 確実性的な面で実装が面倒なので当面このままにしておく\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n\n break;\n }\n\n // 視聴セッションが閉じられた(4時のリセットなど)\n case 'disconnect': {\n\n // 実際に接続が閉じられる前に disconnect イベントが送られてきたので、onclose イベントを削除する\n // onclose イベントが発火するのは不意に切断されたときなど最終手段\n if (this.watch_session) this.watch_session.onclose = null;\n\n // 接続切断の理由\n let disconnect_reason;\n switch (message.data.reason) {\n\n case 'TAKEOVER':\n disconnect_reason = 'ニコニコ実況の番組から追い出されました。';\n break;\n case 'NO_PERMISSION':\n disconnect_reason = 'ニコニコ実況の番組の座席を取得できませんでした。';\n break;\n case 'END_PROGRAM':\n disconnect_reason = 'ニコニコ実況がリセットされたか、コミュニティの番組が終了しました。';\n break;\n case 'PING_TIMEOUT':\n disconnect_reason = 'コメントサーバーとの接続生存確認に失敗しました。';\n break;\n case 'TOO_MANY_CONNECTIONS':\n disconnect_reason = 'ニコニコ実況の同一ユーザからの接続数上限を越えています。';\n break;\n case 'TOO_MANY_WATCHINGS':\n disconnect_reason = 'ニコニコ実況の同一ユーザからの視聴番組数上限を越えています。';\n break;\n case 'CROWDED':\n disconnect_reason = 'ニコニコ実況の番組が満席です。';\n break;\n case 'MAINTENANCE_IN':\n disconnect_reason = 'ニコニコ実況はメンテナンス中です。';\n break;\n case 'SERVICE_TEMPORARILY_UNAVAILABLE':\n disconnect_reason = 'ニコニコ実況で一時的にサーバーエラーが発生しています。';\n break;\n default:\n disconnect_reason = `ニコニコ実況との接続が切断されました。(${message.data.reason})`;\n break;\n }\n\n // 接続切断の理由を表示\n console.log(`disconnected. reason: ${message.data.reason}`);\n if (this.player.danmaku.showing) {\n this.player.notice(disconnect_reason);\n }\n\n // 5 秒ほど待ってから再接続する\n await Utils.sleep(5);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n\n break;\n }\n }\n });\n\n\n // 視聴セッションの接続が閉じられたとき(ネットワークが切断された場合など)\n // イベントを無効化しやすいように敢えて onclose で実装する\n this.watch_session.onclose = async (event) => {\n\n // 接続切断の理由を表示\n console.log(`disconnected. code: ${event.code}`);\n if (this.player.danmaku.showing) {\n this.player.notice(`ニコニコ実況との接続が切断されました。(code: ${event.code})`);\n }\n\n // 10 秒ほど待ってから再接続する\n // ニコ生側から切断された場合と異なりネットワークが切断された可能性が高いので、間を多めに取る\n await Utils.sleep(10);\n if (this.player.danmaku.showing) {\n this.player.notice('ニコニコ実況に再接続しています…');\n }\n\n // 前の視聴セッション・コメントセッションを破棄\n this.destroy();\n\n // 視聴セッションを再初期化\n const comment_session_info = await this.initWatchSession();\n\n // コメントセッションを再初期化\n await this.initCommentSession(comment_session_info);\n };\n });\n },\n\n // コメントセッションを初期化\n async initCommentSession(comment_session_info: {[key: string]: string | null}) {\n\n // タブが非表示状態のときにコメントを格納する配列\n // タブが表示状態になったらコメントリストにのみ表示する(遅れているのでプレイヤーには表示しない)\n let comment_list_buffer: IComment[] = [];\n\n // 最初に送信されてくるコメントを受信し終えたかどうかのフラグ\n let is_received_initial_comment = false;\n\n // コメントセッション WebSocket を開く\n this.comment_session = new WebSocket(comment_session_info.message_server);\n\n // コメントセッション WebSocket を開いたとき\n this.comment_session.addEventListener('open', () => {\n\n // コメント送信をリクエスト\n // このコマンドを送らないとコメントが送信されてこない\n this.comment_session.send(JSON.stringify([\n { 'ping': {'content': 'rs:0'} },\n { 'ping': {'content': 'ps:0'} },\n {\n 'thread': {\n 'version': '20061206', // 設定必須\n 'thread': comment_session_info.thread_id, // スレッド ID\n 'threadkey': comment_session_info.your_post_key, // スレッドキー\n 'user_id': '', // ユーザー ID(設定不要らしい)\n 'res_from': -50, // 最初にコメントを 50 個送信する\n }\n },\n { 'ping': {'content': 'pf:0'} },\n { 'ping': {'content': 'rf:0'} },\n ]));\n });\n\n // コメントセッション WebSocket からメッセージを受信したとき\n this.comment_session.addEventListener('message', async (event_raw) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n\n // thread メッセージのみ\n if (event.thread !== undefined) {\n\n // 接続成功のコールバックを DPlayer に通知\n if (event.thread.resultcode === 0) {\n\n // 接続失敗のコールバックを DPlayer に通知\n } else {\n const message = 'コメントサーバーに接続できませんでした。';\n console.error('Error: ' + message);\n }\n }\n\n // ping メッセージのみ\n // rf:0 が送られてきたら初回コメントの受信は完了\n if (event.ping !== undefined && event.ping.content === 'rf:0') {\n\n // 最初に送信されてくるコメントを受信し終えたフラグを立てる\n is_received_initial_comment = true;\n\n // コメントリストを一番下にスクロール\n // 初回コメントは量が多いので、一括でスクロールする\n this.scrollCommentList();\n }\n\n // コメントデータを取得\n const comment = event.chat;\n\n // コメントがない or 広告用など特殊な場合は弾く\n if (comment === undefined ||\n comment.content === undefined ||\n comment.content.match(/\\/[a-z]+ /)) {\n return;\n }\n\n // 自分のコメントも表示しない\n if (comment.yourpost && comment.yourpost === 1) {\n return;\n }\n\n // ミュート対象のコメントかどうかを判定し、もしそうならここで弾く\n if (this.isMutedComment(comment.content as string, comment.user_id as string)) {\n console.log('Muted comment: ' + comment.content);\n return;\n }\n\n // 色・位置・サイズ\n let color = '#FFEAEA'; // コメント色のデフォルト\n let position: 'top' | 'right' | 'bottom' = 'right'; // コメント位置のデフォルト\n let size: 'big' | 'medium' | 'small' = 'medium'; // コメントサイズのデフォルト\n if (comment.mail !== undefined && comment.mail !== null) {\n\n // コマンドをスペースで区切って配列にしたもの (184 は事前に除外)\n const commands = comment.mail.replace('184', '').split(' ');\n\n for (const command of commands) { // コマンドごとに\n // コメント色指定コマンドがあれば取得\n if (this.getCommentColor(command) !== null) {\n color = this.getCommentColor(command);\n }\n // コメント位置指定コマンドがあれば取得\n if (this.getCommentPosition(command) !== null) {\n position = this.getCommentPosition(command);\n }\n // コメントサイズ指定コマンドがあれば取得\n // コメントサイズのコマンドは DPlayer とニコニコで共通なので、変換の必要はない\n if (command === 'big' || command === 'medium' || command === 'small') {\n size = command;\n }\n }\n }\n\n // 「映像の上下に固定表示されるコメントをミュートする」がオンの場合\n // コメントの位置が top (上固定) もしくは bottom (下固定) のときは弾く\n if (Utils.getSettingsItem('mute_fixed_comments') === true && (position === 'top' || position === 'bottom')) {\n console.log('Muted comment (Fixed): ' + comment.content);\n return;\n }\n\n // 「色付きのコメントをミュートする」がオンの場合\n // コメントの色が #FFEAEA (デフォルト) 以外のときは弾く\n if (Utils.getSettingsItem('mute_colored_comments') === true && color !== '#FFEAEA') {\n console.log('Muted comment (Colored): ' + comment.content);\n return;\n }\n\n // 「文字サイズが大きいコメントをミュートする」がオンの場合\n // コメントのサイズが big のときは弾く\n if (Utils.getSettingsItem('mute_big_size_comments') === true && size === 'big') {\n console.log('Muted comment (Big): ' + comment.content);\n return;\n }\n\n // 配信に発生する遅延分待ってから\n // 最初にドカッと送信されてくる初回コメントは少し前に投稿されたコメント群なので、遅らせずに表示させる\n if (is_received_initial_comment) {\n const comment_delay_time = Utils.getSettingsItem('comment_delay_time');\n await Utils.sleep(comment_delay_time);\n }\n\n // コメントリストのコメントが 500 件を超えたら古いものから順に削除する\n // 仮想スクロールとはいえ、さすがに 500 件を超えると重くなりそう\n // 手動スクロール時は実行しない\n if (this.comment_list.length >= 500 && this.is_manual_scroll === false) {\n while (this.comment_list.length >= 500) {\n this.comment_list.shift();\n }\n }\n\n // コメントリストへ追加するオブジェクト\n // コメント投稿時刻はフォーマットしてから\n const comment_dict: IComment = {\n id: comment.no,\n text: comment.content,\n time: dayjs(comment.date * 1000).format('HH:mm:ss'),\n user_id: comment.user_id,\n my_post: false,\n };\n\n // タブが非表示状態のときは、バッファにコメントを追加するだけで終了する\n // ここで追加すると、タブが表示状態になったときに一斉に描画されて大変なことになる\n if (document.visibilityState === 'hidden') {\n comment_list_buffer.push(comment_dict);\n return;\n }\n\n // コメントリストに追加\n this.comment_list.push(comment_dict);\n\n // // コメントリストを一番下にスクロール\n // 最初に受信したコメントは上の処理で一括でスクロールさせる\n if (is_received_initial_comment) {\n this.scrollCommentList();\n }\n\n // コメント描画 (再生時のみ)\n // 最初に受信したコメントはリアルタイムなコメントではないため、描画しないように\n if (is_received_initial_comment) {\n if (!this.player.video.paused){\n this.player.danmaku.draw({\n text: comment.content,\n color: color,\n type: position,\n size: size,\n });\n }\n }\n });\n\n // タブの表示/非表示の状態が切り替わったときのイベント\n // 表示状態になったときにバッファにあるコメントをコメントリストに表示する\n document.onvisibilitychange = () => {\n if (document.visibilityState === 'visible') {\n this.comment_list.push(...comment_list_buffer); // コメントリストに一括で追加\n comment_list_buffer = []; // バッファをクリア\n this.scrollCommentList(); // コメントリストをスクロール\n }\n };\n },\n\n // コメントを送信する\n async sendComment(options: IDPlayerDanmakuSendOptions) {\n\n // 初期化に失敗しているときは実行せず、保存しておいたエラーメッセージを表示する\n if (this.initialize_failed_message !== null) {\n options.error(this.initialize_failed_message);\n return;\n }\n\n // 未ログイン時\n if (this.user === null) {\n options.error('コメントするには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // ニコニコアカウント未連携時\n if (this.user.niconico_user_id === null) {\n options.error('コメントするには、ニコニコアカウントと連携してください。');\n return;\n }\n\n // 一般会員ではコメント位置の指定 (ue, shita) が無視されるので、事前にエラーにしておく\n if (this.user.niconico_user_premium === false && (options.data.type === 'top' || options.data.type === 'bottom')) {\n options.error('コメントを上下に固定するには、ニコニコアカウントのプレミアム会員登録が必要です。');\n return;\n }\n\n // 一般会員ではコメントサイズ大きめの指定 (big) が無視されるので、事前にエラーにしておく\n if (this.user.niconico_user_premium === false && options.data.size === 'big') {\n options.error('コメントサイズを大きめに設定するには、ニコニコアカウントのプレミアム会員登録が必要です。');\n return;\n }\n\n // DPlayer 上のコメント色(カラーコード)とニコニコの色コマンド定義のマッピング\n const color_table = {\n '#FFEAEA': 'white',\n '#F02840': 'red',\n '#FD7E80': 'pink',\n '#FDA708': 'orange',\n '#FFE133': 'yellow',\n '#64DD17': 'green',\n '#00D4F5': 'cyan',\n '#4763FF': 'blue',\n };\n\n // DPlayer 上のコメント位置を表す数値とニコニコの位置コマンド定義のマッピング\n const position_table = {\n 'top': 'ue',\n 'right': 'naka',\n 'bottom': 'shita',\n };\n\n // vpos を計算 (10ミリ秒単位)\n // 番組開始時間からの累計秒らしいけど、なぜ指定しないといけないのかは不明\n const vpos = Math.floor(new Date().getTime() / 10) - this.vpos_base_timestamp;\n\n // コメントを送信\n this.watch_session.send(JSON.stringify({\n 'type': 'postComment',\n 'data': {\n 'text': options.data.text, // コメント本文\n 'color': color_table[options.data.color.toUpperCase()], // コメントの色\n 'position': position_table[options.data.type], // コメント位置\n 'size': options.data.size, // コメントサイズ (DPlayer とニコニコで表現が共通)\n 'vpos': vpos, // 番組開始時間からの累計秒(10ミリ秒単位)\n 'isAnonymous': true, // 匿名コメント (184)\n }\n }));\n\n // 自分のコメントをコメントリストに追加\n this.comment_list.push({\n id: new Date().getTime(),\n text: options.data.text,\n time: dayjs().format('HH:mm:ss'),\n user_id: `${this.user.niconico_user_id}`,\n my_post: true, // コメントリスト上でハイライトする\n });\n\n // コメント送信のレスポンスを取得\n // 簡単にイベントリスナーを削除できるため、あえて onmessage で実装している\n this.watch_session.onmessage = (event) => {\n\n // 受信したメッセージ\n const message = JSON.parse(event.data);\n\n switch (message.type) {\n\n // postCommentResult\n // これが送られてくる → コメント送信に成功している\n case 'postCommentResult': {\n\n // コメント成功のコールバックを DPlayer に通知\n options.success();\n\n // イベントリスナーを削除\n this.watch_session.onmessage = null;\n break;\n }\n\n // error\n // コメント送信直後に error が送られてきた → コメント送信に失敗している\n case 'error': {\n\n // エラーメッセージ\n let error = `コメントの送信に失敗しました。(${message.data.code})`;\n switch (message.data.code) {\n case 'COMMENT_POST_NOT_ALLOWED': {\n error = 'コメントが許可されていません。';\n break;\n }\n case 'INVALID_MESSAGE': {\n error = 'コメント内容が無効です。';\n break;\n }\n }\n\n // コメント失敗のコールバックを DPlayer に通知\n options.error(error);\n\n // イベントリスナーを解除\n this.watch_session.onmessage = null;\n break;\n }\n }\n };\n },\n\n // リサイズ時のイベントを初期化\n async initReserveObserver() {\n\n // 監視対象の要素\n this.resize_observer_element = document.querySelector('.watch-player');\n\n // タイムアウト ID\n // 一時的に無効にした transition を有効化する際に利用する\n let animation_timeout_id = null;\n\n // プレイヤーの要素がリサイズされた際に発火するイベント\n const on_resize = () => {\n\n // 映像の要素\n const video_element = document.querySelector('.dplayer-video-wrap-aspect');\n\n // コメント描画領域の要素\n const comment_area_element = document.querySelector('.dplayer-danmaku');\n\n // プレイヤー全体と映像の高さの差(レターボックス)から、コメント描画領域の高さを狭める必要があるかを判定する\n // 2で割っているのは単体の差を測るため\n if (this.resize_observer_element === null || this.resize_observer_element.clientHeight === null) return;\n if (video_element === null || video_element.clientHeight === null) return;\n const letter_box_height = (this.resize_observer_element.clientHeight - video_element.clientHeight) / 2;\n\n // 70px or 54px (高さが 450px 以下) 以下ならヘッダー(番組名などの表示)と被るので対応する\n const threshold = window.matchMedia('(max-height: 450px)').matches ? 50 : 66;\n if (letter_box_height < threshold) {\n\n // コメント描画領域に必要な上下マージン\n const comment_area_vertical_margin = (threshold - letter_box_height) * 2;\n\n // 狭めるコメント描画領域の幅\n // 映像の要素の幅をそのまま利用する\n const comment_area_width = video_element.clientWidth;\n\n // 狭めるコメント描画領域の高さ\n const comment_area_height = video_element.clientHeight - comment_area_vertical_margin;\n\n // 狭めるコメント描画領域のアスペクト比を求める\n // https://tech.arc-one.jp/asepct-ratio/\n const gcd = (x: number, y: number) => { // 最大公約数を求める関数\n if (y === 0) return x;\n return gcd(y, x % y);\n }\n // 幅と高さの最大公約数を求める\n const gcd_result = gcd(comment_area_width, comment_area_height);\n // 幅と高さをそれぞれ最大公約数で割ってアスペクト比を算出\n const comment_area_height_aspect = `${comment_area_width / gcd_result} / ${comment_area_height / gcd_result}`;\n\n // 一時的に transition を無効化する\n // アスペクト比の設定は連続して行われるが、その際に transition が適用されるとワンテンポ遅れたアニメーションになってしまう\n comment_area_element.style.transition = 'none';\n\n // コメント描画領域に算出したアスペクト比を設定する\n comment_area_element.style.setProperty('--comment-area-aspect-ratio', comment_area_height_aspect);\n\n // コメント描画領域に必要な上下マージンを設定する\n comment_area_element.style.setProperty('--comment-area-vertical-margin', `${comment_area_vertical_margin}px`);\n\n // 以前セットされた setTimeout() を止める\n window.clearTimeout(animation_timeout_id);\n\n // 0.2秒後に実行する\n // 0.2秒より前にもう一度リサイズイベントが来た場合はタイマーがクリアされるため実行されない\n window.setTimeout(() => {\n\n // 再び transition を有効化する\n comment_area_element.style.transition = '';\n\n }, 0.2 * 1000);\n\n } else {\n\n // コメント描画領域に設定したアスペクト比・上下マージンを削除する\n comment_area_element.style.removeProperty('--comment-area-aspect-ratio');\n comment_area_element.style.removeProperty('--comment-area-vertical-margin');\n }\n }\n\n // 要素の監視を開始\n this.resize_observer = new ResizeObserver(on_resize);\n this.resize_observer.observe(this.resize_observer_element);\n\n // 0.6 秒待ってから初回実行\n // チャンネル切り替え後、再初期化されたプレイヤーに適用するため(早いと再初期化前のプレイヤーに適用されてしまう)\n window.setTimeout(on_resize, 0.6 * 1000);\n },\n\n // コメントリストを一番下までスクロールする\n async scrollCommentList(smooth: boolean = false) {\n\n // 手動スクロールモードの時は実行しない\n if (this.is_manual_scroll === true) return;\n\n // 自動スクロール中のフラグを立てる\n this.is_auto_scrolling = true;\n\n // 0.01 秒待って実行し、念押しで2回実行しないと完全に最下部までスクロールされない…(ブラウザの描画バグ?)\n // this.$nextTick() は効かなかった\n for (let index = 0; index < 3; index++) {\n await Utils.sleep(0.01);\n if (smooth === true) { // スムーズスクロール\n this.comment_list_element.scrollTo({top: this.comment_list_element.scrollHeight, left: 0, behavior: 'smooth'});\n } else {\n this.comment_list_element.scrollTo(0, this.comment_list_element.scrollHeight);\n }\n }\n\n // 0.1 秒待つ(重要)\n await Utils.sleep(0.1);\n\n // 自動スクロール中のフラグを降ろす\n this.is_auto_scrolling = false;\n },\n\n /**\n * ニコニコの色指定を 16 進数カラーコードに置換する\n * @param color ニコニコの色指定\n * @return 16 進数カラーコード\n */\n getCommentColor(color: string): string {\n const color_table = {\n 'white': '#FFEAEA',\n 'red': '#F02840',\n 'pink': '#FD7E80',\n 'orange': '#FDA708',\n 'yellow': '#FFE133',\n 'green': '#64DD17',\n 'cyan': '#00D4F5',\n 'blue': '#4763FF',\n 'purple': '#D500F9',\n 'black': '#1E1310',\n 'white2': '#CCCC99',\n 'niconicowhite': '#CCCC99',\n 'red2': '#CC0033',\n 'truered': '#CC0033',\n 'pink2': '#FF33CC',\n 'orange2': '#FF6600',\n 'passionorange': '#FF6600',\n 'yellow2': '#999900',\n 'madyellow': '#999900',\n 'green2': '#00CC66',\n 'elementalgreen': '#00CC66',\n 'cyan2': '#00CCCC',\n 'blue2': '#3399FF',\n 'marineblue': '#3399FF',\n 'purple2': '#6633CC',\n 'nobleviolet': '#6633CC',\n 'black2': '#666666',\n };\n if (color_table[color] !== undefined) {\n return color_table[color];\n } else {\n return null;\n }\n },\n\n /**\n * ニコニコの位置指定を DPlayer の位置指定に置換する\n * @param position ニコニコの位置指定\n * @return DPlayer の位置指定\n */\n getCommentPosition(position: string): 'top' | 'right' | 'bottom' {\n switch (position) {\n case 'ue':\n return 'top';\n case 'naka':\n return 'right';\n case 'shita':\n return 'bottom';\n default:\n return null;\n }\n },\n\n /**\n * ミュート対象のコメントかどうかを判断する\n * @param comment コメント\n * @param user_id コメントを投稿したユーザーの ID\n * @return ミュート対象のコメントなら true を返す\n */\n isMutedComment(comment: string, user_id: string): boolean {\n\n // キーワードミュート処理\n const muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n for (const muted_comment_keyword of muted_comment_keywords) {\n if (muted_comment_keyword.pattern === '') continue; // キーワードが空文字のときは無視\n switch (muted_comment_keyword.match) {\n // 部分一致\n case 'partial':\n if (comment.includes(muted_comment_keyword.pattern)) return true;\n break;\n // 前方一致\n case 'forward':\n if (comment.startsWith(muted_comment_keyword.pattern)) return true;\n break;\n // 後方一致\n case 'backward':\n if (comment.endsWith(muted_comment_keyword.pattern)) return true;\n break;\n // 完全一致\n case 'exact':\n if (comment === muted_comment_keyword.pattern) return true;\n break;\n // 正規表現\n case 'regex':\n if (new RegExp(muted_comment_keyword.pattern).test(comment)) return true;\n break;\n }\n }\n\n // 「露骨な表現を含むコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_vulgar_comments') === true) {\n if (mute_vulgar_comments_pattern.test(comment)) return true;\n }\n\n // 「罵倒や差別的な表現を含むコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_abusive_discriminatory_prejudiced_comments') === true) {\n if (mute_abusive_discriminatory_prejudiced_comments_pattern.test(comment)) return true;\n }\n\n // 「8文字以上同じ文字が連続しているコメントをミュートする」がオンの場合\n if (Utils.getSettingsItem('mute_consecutive_same_characters_comments') === true) {\n if (/(.)\\1{7,}/.test(comment)) return true;\n }\n\n // 「NHK→計1447ID/内プレ425ID/総33372米 ◆ Eテレ → 計73ID/内プレ19ID/総941米」のような\n // 迷惑コメントを一括で弾く (あえてミュートしたくないユースケースが思い浮かばないのでデフォルトで弾く)\n if (/最高\\d+米\\/|計\\d+ID|総\\d+米/.test(comment)) return true;\n\n // ユーザー ID ミュート処理\n const muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n for (const muted_niconico_user_id of muted_niconico_user_ids) {\n if (user_id === muted_niconico_user_id) return true;\n }\n\n // いずれのミュート処理にも引っかからなかった (ミュート対象ではない)\n return false;\n },\n\n // ミュート済みキーワードリストに追加する (完全一致)\n addMutedKeywords(comment: string) {\n const muted_comment_keywords = Utils.getSettingsItem('muted_comment_keywords') as IMutedCommentKeywords[];\n muted_comment_keywords.push({\n match: 'exact',\n pattern: comment,\n });\n Utils.setSettingsItem('muted_comment_keywords', muted_comment_keywords);\n },\n\n // ミュート済みニコニコユーザー ID リストに追加する\n addMutedNiconicoUserIDs(user_id: string) {\n const muted_niconico_user_ids = Utils.getSettingsItem('muted_niconico_user_ids') as string[];\n muted_niconico_user_ids.push(user_id);\n Utils.setSettingsItem('muted_niconico_user_ids', muted_niconico_user_ids);\n },\n\n // ドロップダウンメニューを表示する\n displayCommentListDropdown(event: Event, comment: IComment) {\n const comment_list_wrapper_rect = (this.$refs.comment_list_wrapper as HTMLDivElement).getBoundingClientRect();\n const comment_list_dropdown_height = 76; // 76px はドロップダウンメニューの高さ\n const comment_button_rect = (event.currentTarget as HTMLElement).getBoundingClientRect()\n // メニューの表示位置をクリックされたコメントに合わせる\n this.comment_list_dropdown_top = comment_button_rect.top - comment_list_wrapper_rect.top;\n // メニューがコメントリストからはみ出るときだけ、表示位置を上側に調整\n if ((this.comment_list_dropdown_top + comment_list_dropdown_height) > comment_list_wrapper_rect.height) {\n this.comment_list_dropdown_top = this.comment_list_dropdown_top - comment_list_dropdown_height + comment_button_rect.height;\n }\n // 表示位置を調整できたので、メニューを表示\n this.comment_list_dropdown_comment = comment;\n this.is_comment_list_dropdown_display = true;\n },\n\n // 破棄する\n destroy() {\n\n // 初期化失敗時のメッセージをクリア\n this.initialize_failed_message = null;\n\n // コメントリストをクリア\n this.comment_list = [];\n\n // タブの表示/非表示の状態が切り替わったときのイベントを削除\n document.onvisibilitychange = null;\n\n // 視聴セッションを閉じる\n if (this.watch_session !== null) {\n this.watch_session.onclose = null; // WebSocket が閉じられた際のイベントを削除\n this.watch_session.close(); // WebSocket を閉じる\n this.watch_session = null; // null に戻す\n }\n\n // コメントセッションを閉じる\n if (this.comment_session !== null) {\n this.comment_session.onclose = null; // WebSocket が閉じられた際のイベントを削除\n this.comment_session.close(); // WebSocket を閉じる\n this.comment_session = null; // null に戻す\n }\n\n // 座席保持用のタイマーをクリア\n window.clearInterval(this.keep_seat_interval_id);\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Comment.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Comment.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Comment.vue?vue&type=template&id=23e84f03&scoped=true&\"\nimport script from \"./Comment.vue?vue&type=script&lang=ts&\"\nexport * from \"./Comment.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Comment.vue?vue&type=style&index=0&id=23e84f03&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"23e84f03\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"program-container\"},[_c('section',{staticClass:\"program-broadcaster\"},[_c('img',{staticClass:\"program-broadcaster__icon\",attrs:{\"src\":`${_vm.Utils.api_base_url}/channels/${(_vm.$route.params.channel_id)}/logo`}}),_c('div',{staticClass:\"program-broadcaster__number\"},[_vm._v(\"Ch: \"+_vm._s(_vm.channel.channel_number))]),_c('div',{staticClass:\"program-broadcaster__name\"},[_vm._v(_vm._s(_vm.channel.channel_name))])]),_c('section',{staticClass:\"program-info\"},[_c('h1',{staticClass:\"program-info__title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'title'))}}),_c('div',{staticClass:\"program-info__time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_present)))]),_c('div',{staticClass:\"program-info__description\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_present, 'description'))}}),_c('div',{staticClass:\"program-info__genre-container\"},_vm._l((_vm.ProgramUtils.getAttribute(_vm.channel.program_present, 'genre', [])),function(genre,genre_index){return _c('div',{key:genre_index,staticClass:\"program-info__genre\"},[_vm._v(\" \"+_vm._s(genre.major)+\" / \"+_vm._s(genre.middle)+\" \")])}),0),_c('div',{staticClass:\"program-info__next\"},[_c('span',{staticClass:\"program-info__next-decorate\"},[_vm._v(\"NEXT\")]),_c('Icon',{staticClass:\"program-info__next-icon\",attrs:{\"icon\":\"fluent:fast-forward-20-filled\",\"width\":\"16px\"}})],1),_c('span',{staticClass:\"program-info__next-title\",domProps:{\"innerHTML\":_vm._s(_vm.ProgramUtils.decorateProgramInfo(_vm.channel.program_following, 'title'))}}),_c('div',{staticClass:\"program-info__next-time\"},[_vm._v(_vm._s(_vm.ProgramUtils.getProgramTime(_vm.channel.program_following)))]),_c('div',{staticClass:\"program-info__status\"},[_c('div',{staticClass:\"program-info__status-force\",class:`program-info__status-force--${_vm.ChannelUtils.getChannelForceType(_vm.channel.channel_force)}`},[_c('Icon',{attrs:{\"icon\":\"fa-solid:fire-alt\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"勢い:\")]),_c('span',{staticClass:\"ml-2\"},[_vm._v(_vm._s(_vm.ProgramUtils.getAttribute(_vm.channel, 'channel_force', '--'))+\" コメ/分\")])],1),_c('div',{staticClass:\"program-info__status-viewers ml-5\"},[_c('Icon',{attrs:{\"icon\":\"fa-solid:eye\",\"height\":\"14px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"視聴数:\")]),_c('span',{staticClass:\"ml-1\"},[_vm._v(_vm._s(_vm.channel.viewers))])],1)])]),_c('section',{staticClass:\"program-detail-container\"},_vm._l((_vm.ProgramUtils.getAttribute(_vm.channel.program_present, 'detail', {})),function(detail_text,detail_heading){return _c('div',{key:detail_heading,staticClass:\"program-detail\"},[_c('h2',{staticClass:\"program-detail__heading\"},[_vm._v(_vm._s(detail_heading))]),_c('div',{staticClass:\"program-detail__text\",domProps:{\"innerHTML\":_vm._s(_vm.Utils.URLtoLink(detail_text))}})])}),0)])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue, { PropType } from 'vue';\n\nimport { IChannel } from '@/interface';\nimport Utils, { ChannelUtils, ProgramUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Panel-ProgramTab',\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n }\n },\n data() {\n return {\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ChannelUtils: ChannelUtils,\n ProgramUtils: ProgramUtils,\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Program.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Program.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Program.vue?vue&type=template&id=3c7f1e0c&scoped=true&\"\nimport script from \"./Program.vue?vue&type=script&lang=ts&\"\nexport * from \"./Program.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Program.vue?vue&type=style&index=0&id=3c7f1e0c&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"3c7f1e0c\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"twitter-container\"},[_c('v-dialog',{attrs:{\"content-class\":\"zoom-capture-modal-container\",\"max-width\":\"980\",\"transition\":\"slide-y-transition\"},model:{value:(_vm.zoom_capture_modal),callback:function ($$v) {_vm.zoom_capture_modal=$$v},expression:\"zoom_capture_modal\"}},[_c('div',{staticClass:\"zoom-capture-modal\"},[_c('img',{staticClass:\"zoom-capture-modal__image\",attrs:{\"src\":_vm.zoom_capture ? _vm.zoom_capture.image_url: ''}}),_c('a',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"zoom-capture-modal__download\",attrs:{\"href\":_vm.zoom_capture ? _vm.zoom_capture.image_url : '',\"download\":_vm.zoom_capture ? _vm.zoom_capture.filename : ''}},[_c('Icon',{attrs:{\"icon\":\"fa6-solid:download\",\"width\":\"45px\"}})],1)])]),_c('div',{staticClass:\"tab-container\"},[_c('div',{staticClass:\"tab-content\",class:{'tab-content--active': _vm.twitter_active_tab === 'Capture'}},[_c('div',{staticClass:\"captures\"},_vm._l((_vm.captures),function(capture){return _c('div',{key:capture.image_url,staticClass:\"capture\",class:{\n 'capture--selected': capture.selected,\n 'capture--focused': capture.focused,\n 'capture--disabled': !capture.selected && _vm.tweet_captures.length >= 4,\n },on:{\"click\":function($event){return _vm.clickCapture(capture)}}},[_c('img',{staticClass:\"capture__image\",attrs:{\"src\":capture.image_url}}),_c('div',{staticClass:\"capture__disabled-cover\"}),_c('div',{staticClass:\"capture__selected-number\"},[_vm._v(_vm._s(_vm.tweet_captures.findIndex(blob => blob === capture.blob) + 1))]),_c('Icon',{staticClass:\"capture__selected-checkmark\",attrs:{\"icon\":\"fluent:checkmark-circle-16-filled\"}}),_c('div',{staticClass:\"capture__selected-border\"}),_c('div',{staticClass:\"capture__focused-border\"}),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"capture__zoom\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.zoom_capture_modal = true; _vm.zoom_capture = capture},\"mousedown\":function($event){$event.preventDefault();$event.stopPropagation();/* 親要素の波紋が広がらないように */}}},[_c('Icon',{attrs:{\"icon\":\"fluent:zoom-in-16-regular\",\"width\":\"32px\"}})],1)],1)}),0),_c('div',{directives:[{name:\"show\",rawName:\"v-show\",value:(_vm.captures.length === 0),expression:\"captures.length === 0\"}],staticClass:\"capture-announce\"},[_c('div',{staticClass:\"capture-announce__heading\"},[_vm._v(\"まだキャプチャがありません。\")]),_vm._m(0)])])]),_c('div',{staticClass:\"tab-button-container\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Search'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Search'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:search-16-filled\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"ツイート検索\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Timeline'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Timeline'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:home-16-regular\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"タイムライン\")])],1),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tab-button\",class:{'tab-button--active': _vm.twitter_active_tab === 'Capture'},on:{\"click\":function($event){_vm.twitter_active_tab = 'Capture'}}},[_c('Icon',{attrs:{\"icon\":\"fluent:image-copy-20-regular\",\"height\":\"18px\"}}),_c('span',{staticClass:\"tab-button__text\"},[_vm._v(\"キャプチャ\")])],1)]),_c('div',{staticClass:\"tweet-form\",class:{\n 'tweet-form--focused': _vm.is_tweet_hashtag_form_focused || _vm.is_tweet_text_form_focused,\n 'tweet-form--virtual-keyboard-display': _vm.is_virtual_keyboard_display &&\n (_vm.Utils.hasActiveElementClass('tweet-form__hashtag-form') || _vm.Utils.hasActiveElementClass('tweet-form__textarea')) &&\n (() => {_vm.is_hashtag_list_display = false; return true;})(),\n }},[_c('div',{staticClass:\"tweet-form__hashtag\"},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.tweet_hashtag),expression:\"tweet_hashtag\"}],staticClass:\"tweet-form__hashtag-form\",attrs:{\"type\":\"search\",\"placeholder\":\"#ハッシュタグ\"},domProps:{\"value\":(_vm.tweet_hashtag)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.tweet_hashtag=$event.target.value},function($event){return _vm.updateTweetLetterCount()}],\"focus\":function($event){_vm.is_tweet_hashtag_form_focused = true},\"blur\":function($event){_vm.is_tweet_hashtag_form_focused = false},\"change\":function($event){_vm.tweet_hashtag = _vm.formatHashtag(_vm.tweet_hashtag)}}}),_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tweet-form__hashtag-list-button\",on:{\"click\":function($event){_vm.is_hashtag_list_display = !_vm.is_hashtag_list_display}}},[_c('Icon',{attrs:{\"icon\":\"fluent:clipboard-text-ltr-32-regular\",\"height\":\"22px\"}})],1)]),_c('textarea',{directives:[{name:\"model\",rawName:\"v-model\",value:(_vm.tweet_text),expression:\"tweet_text\"}],ref:\"tweet_text\",staticClass:\"tweet-form__textarea\",attrs:{\"placeholder\":\"ツイート\"},domProps:{\"value\":(_vm.tweet_text)},on:{\"input\":[function($event){if($event.target.composing)return;_vm.tweet_text=$event.target.value},function($event){return _vm.updateTweetLetterCount()}],\"paste\":function($event){return _vm.pasteClipboardData($event)},\"focus\":function($event){_vm.is_tweet_text_form_focused = true},\"blur\":function($event){_vm.is_tweet_text_form_focused = false}}}),_c('div',{staticClass:\"tweet-form__control\"},[_c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"account-button\",class:{'account-button--no-login': !_vm.is_logged_in_twitter},on:{\"click\":function($event){return _vm.clickAccountButton()}}},[_c('img',{staticClass:\"account-button__icon\",attrs:{\"src\":_vm.is_logged_in_twitter ? _vm.selected_twitter_account.icon_url : '/assets/images/account-icon-default.png'}}),_c('span',{staticClass:\"account-button__screen-name\"},[_vm._v(\" \"+_vm._s(_vm.is_logged_in_twitter ? `@${_vm.selected_twitter_account.screen_name}` : '連携されていません')+\" \")]),_c('Icon',{staticClass:\"account-button__menu\",attrs:{\"icon\":\"fluent:more-circle-20-regular\",\"width\":\"22px\"}})],1),_c('div',{staticClass:\"limit-meter\"},[_c('div',{staticClass:\"limit-meter__content\",class:{\n 'limit-meter__content--yellow': _vm.tweet_letter_count <= 20,\n 'limit-meter__content--red': _vm.tweet_letter_count <= 0,\n }},[_c('Icon',{staticStyle:{\"margin-right\":\"-2px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"12px\"}}),_c('span',[_vm._v(_vm._s(_vm.tweet_letter_count))])],1),_c('div',{staticClass:\"limit-meter__content\"},[_c('Icon',{attrs:{\"icon\":\"fluent:image-16-filled\",\"width\":\"14px\"}}),_c('span',[_vm._v(_vm._s(_vm.tweet_captures.length)+\"/4\")])],1)]),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"tweet-button\",attrs:{\"disabled\":!_vm.is_logged_in_twitter || _vm.tweet_letter_count < 0 ||\n (_vm.tweet_letter_count === 140 && _vm.tweet_captures.length === 0)},on:{\"click\":function($event){return _vm.sendTweet()}}},[_c('Icon',{attrs:{\"icon\":\"fa-brands:twitter\",\"height\":\"16px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ツイート\")])],1)])]),_c('div',{staticClass:\"twitter-account-list\",class:{'twitter-account-list--display': _vm.is_twitter_account_list_display}},_vm._l((_vm.user.twitter_accounts),function(twitter_account){return _c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],key:twitter_account.id,staticClass:\"twitter-account\",on:{\"click\":function($event){return _vm.updateSelectedTwitterAccount(twitter_account)}}},[_c('img',{staticClass:\"twitter-account__icon\",attrs:{\"src\":twitter_account.icon_url}}),_c('div',{staticClass:\"twitter-account__info\"},[_c('div',{staticClass:\"twitter-account__name\"},[_vm._v(_vm._s(twitter_account.name))]),_c('div',{staticClass:\"twitter-account__screen-name\"},[_vm._v(\"@\"+_vm._s(twitter_account.screen_name))])]),_c('Icon',{directives:[{name:\"show\",rawName:\"v-show\",value:(twitter_account.id === _vm.selected_twitter_account_id),expression:\"twitter_account.id === selected_twitter_account_id\"}],staticClass:\"twitter-account__check\",attrs:{\"icon\":\"fluent:checkmark-16-filled\",\"width\":\"24px\"}})],1)}),0),_c('div',{staticClass:\"hashtag-list\",class:{\n 'hashtag-list--display': _vm.is_hashtag_list_display,\n 'hashtag-list--virtual-keyboard-display': _vm.is_virtual_keyboard_display && _vm.Utils.hasActiveElementClass('hashtag__input'),\n }},[_c('div',{staticClass:\"hashtag-heading\"},[_c('div',{staticClass:\"hashtag-heading__text\"},[_c('Icon',{attrs:{\"icon\":\"charm:hash\",\"width\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"ハッシュタグリスト\")])],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag-heading__add-button\",on:{\"click\":function($event){_vm.saved_twitter_hashtags.push({id: Date.now(), text: '#ここにハッシュタグを入力', editing: false})}}},[_c('Icon',{attrs:{\"icon\":\"fluent:add-12-filled\",\"width\":\"17px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"追加\")])],1)]),_c('draggable',{staticClass:\"hashtag-container\",attrs:{\"handle\":\".hashtag__sort-handle\"},model:{value:(_vm.saved_twitter_hashtags),callback:function ($$v) {_vm.saved_twitter_hashtags=$$v},expression:\"saved_twitter_hashtags\"}},_vm._l((_vm.saved_twitter_hashtags),function(hashtag){return _c('div',{directives:[{name:\"ripple\",rawName:\"v-ripple\",value:(!hashtag.editing),expression:\"!hashtag.editing\"}],key:hashtag.id,staticClass:\"hashtag\",class:{'hashtag--editing': hashtag.editing},on:{\"click\":function($event){_vm.tweet_hashtag = hashtag.text; _vm.tweet_hashtag = _vm.formatHashtag(_vm.tweet_hashtag);\n _vm.updateTweetLetterCount(); _vm.window.setTimeout(() => _vm.is_hashtag_list_display = false, 150)}}},[_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(hashtag.text),expression:\"hashtag.text\"}],staticClass:\"hashtag__input\",attrs:{\"type\":\"search\",\"disabled\":!hashtag.editing},domProps:{\"value\":(hashtag.text)},on:{\"click\":function($event){$event.stopPropagation();},\"input\":function($event){if($event.target.composing)return;_vm.$set(hashtag, \"text\", $event.target.value)}}}),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag__edit-button\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();hashtag.editing = !hashtag.editing; hashtag.text = _vm.formatHashtag(hashtag.text, true)}}},[_c('Icon',{attrs:{\"icon\":hashtag.editing ? 'fluent:checkmark-16-filled': 'fluent:edit-16-filled',\"width\":\"17px\"}})],1),_c('button',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"hashtag__delete-button\",on:{\"click\":function($event){$event.preventDefault();$event.stopPropagation();_vm.saved_twitter_hashtags.splice(_vm.saved_twitter_hashtags.indexOf(hashtag), 1)}}},[_c('Icon',{attrs:{\"icon\":\"fluent:delete-16-filled\",\"width\":\"17px\"}})],1),_c('div',{staticClass:\"hashtag__sort-handle\"},[_c('Icon',{attrs:{\"icon\":\"material-symbols:drag-handle-rounded\",\"width\":\"17px\"}})],1)])}),0)],1)],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"capture-announce__text\"},[_c('p',{staticClass:\"mt-0 mb-0\"},[_vm._v(\"プレイヤーのキャプチャボタンやショートカットキーでキャプチャを撮ると、ここに表示されます。\")]),_c('p',{staticClass:\"mt-2 mb-0\"},[_vm._v(\"表示されたキャプチャを選択してからツイートすると、キャプチャを付けてツイートできます。\")])])\n}]\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue, { PropType } from 'vue';\nimport draggable from 'vuedraggable'\n\nimport { IChannel, ITwitterAccount, IUser } from '@/interface';\nimport Utils from '@/utils';\n\n// このコンポーネント内でのキャプチャのインターフェイス\ninterface ITweetCapture {\n blob: Blob;\n filename: string;\n image_url: string;\n selected: boolean;\n focused: boolean;\n}\n\n// このコンポーネント内でのハッシュタグのインターフェイス\ninterface IHashtag {\n id: number;\n text: string;\n editing: boolean;\n}\n\nexport default Vue.extend({\n name: 'Panel-TwitterTab',\n components: {\n draggable,\n },\n props: {\n // チャンネル情報\n channel: {\n type: Object as PropType,\n required: true,\n },\n // プレイヤーのインスタンス\n player: {\n type: null as PropType, // 代入当初は null になるため苦肉の策\n required: true,\n },\n // 仮想キーボードが表示されているかどうか\n is_virtual_keyboard_display: {\n type: Boolean as PropType,\n required: true,\n },\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // window.setTimeout() にアクセスできるように\n window: window,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // Twitter アカウントを1つでも連携しているかどうか\n is_logged_in_twitter: false,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // 現在ツイート対象として選択されている Twitter アカウント\n selected_twitter_account: null as ITwitterAccount | null,\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID\n selected_twitter_account_id: Utils.getSettingsItem('selected_twitter_account_id') as number | null,\n\n // 連携している Twitter アカウントリストを表示しているか\n is_twitter_account_list_display: false,\n\n // 保存している Twitter のハッシュタグが入るリスト\n saved_twitter_hashtags: (Utils.getSettingsItem('saved_twitter_hashtags') as string[]).map((hashtag, index) => {\n // id プロパティは :key=\"\" に指定するためにつける ID (ミリ秒単位のタイムスタンプ + index で適当に一意になるように)\n return {id: Date.now() + index, text: hashtag, editing: false} as IHashtag;\n }),\n\n // ハッシュタグリストを表示しているか\n is_hashtag_list_display: false,\n\n // 既定で表示される Twitter タブ内のタブ\n twitter_active_tab: Utils.getSettingsItem('twitter_active_tab') as ('Search' | 'Timeline' | 'Capture'),\n\n // キャプチャを拡大表示するモーダルの表示状態\n zoom_capture_modal: false,\n\n // 現在モーダルで拡大表示中のキャプチャのオブジェクト\n zoom_capture: null as ITweetCapture | null,\n\n // キャプチャリスト\n captures: [] as ITweetCapture[],\n\n // キャプチャリストの要素\n captures_element: null as HTMLDivElement | null,\n\n // ツイートハッシュタグフォームにフォーカスしているか\n is_tweet_hashtag_form_focused: false,\n\n // ツイート本文フォームにフォーカスしているか\n is_tweet_text_form_focused: false,\n\n // ツイートのハッシュタグ\n tweet_hashtag: '',\n\n // ツイート本文\n tweet_text: '',\n\n // ツイートに添付するキャプチャの Blob のリスト\n tweet_captures: [] as Blob[],\n\n // 文字数カウント\n tweet_letter_count: 140,\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n\n // 連携している Twitter アカウントがあれば true に設定\n if (this.user.twitter_accounts.length > 0) {\n this.is_logged_in_twitter = true;\n\n // 現在ツイート対象として選択されている Twitter アカウントの ID が設定されていない or ID に紐づく Twitter アカウントがない\n // 連携している Twitter アカウントのうち、一番最初のものを自動選択する\n // ここで言う Twitter アカウントの ID は DB 上で連番で振られるもので、Twitter アカウントそのものの固有 ID ではない\n if (this.selected_twitter_account_id === null ||\n !this.user.twitter_accounts.some((twitter_account) => twitter_account.id === this.selected_twitter_account_id)) {\n this.selected_twitter_account_id = this.user.twitter_accounts[0].id;\n Utils.setSettingsItem('selected_twitter_account_id', this.selected_twitter_account_id);\n }\n\n // 現在ツイート対象として選択されている Twitter アカウントを取得・設定\n const twitter_account_index = this.user.twitter_accounts.findIndex((twitter_account) => {\n return twitter_account.id === this.selected_twitter_account_id; // Twitter アカウントの ID が選択されているものと一致する\n });\n this.selected_twitter_account = this.user.twitter_accounts[twitter_account_index];\n }\n }\n\n // 局タグ追加処理を走らせる (ハッシュタグフォームのフォーマット処理も同時に行われるが、元々空なので無意味)\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag);\n },\n beforeDestroy() {\n // 終了前にすべてのキャプチャの Blob URL を revoke してリソースを解放する\n for (const capture of this.captures) {\n URL.revokeObjectURL(capture.image_url);\n }\n },\n watch: {\n\n // チャンネル情報が変更されたとき\n // 前のチャンネル情報と次のチャンネル情報で channel_id が変わってたら局タグ追加処理を走らせる\n async channel(new_channel: IChannel, old_channel: IChannel) {\n if (new_channel.channel_id !== old_channel.channel_id) {\n const old_channel_hashtag = this.getChannelHashtag(old_channel.channel_name) ?? '';\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag.replaceAll(old_channel_hashtag, ''));\n }\n },\n\n // 保存しているハッシュタグが変更されたら随時 LocalStorage に保存する\n saved_twitter_hashtags: {\n deep: true,\n handler() {\n Utils.setSettingsItem('saved_twitter_hashtags', this.saved_twitter_hashtags.map(hashtag => hashtag.text));\n }\n }\n },\n methods: {\n\n // ユーザーアカウントの情報を取得する\n async syncAccountInfo() {\n try {\n this.user = (await Vue.axios.get('/users/me')).data;\n } catch (error) {\n // ログインされていないので未ログイン状態に設定\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n // 文字数カウントを変更するイベント\n updateTweetLetterCount() {\n\n // サロゲートペアを考慮し、スプレッド演算子で一度配列化してから数えている\n // ref: https://qiita.com/suin/items/3da4fb016728c024eaca\n this.tweet_letter_count = 140 - [...this.tweet_hashtag].length - [...this.tweet_text].length;\n },\n\n // アカウントボタンが押されたときのイベント\n clickAccountButton() {\n\n // Twitter アカウントが連携されていない場合は Twitter 設定画面に飛ばす\n if (!this.is_logged_in_twitter) {\n\n // 視聴ページ以外に遷移するため、フルスクリーンを解除しないと画面が崩れる\n if (document.fullscreenElement) {\n document.exitFullscreen();\n }\n\n this.$router.push({path: '/settings/twitter'});\n return;\n }\n\n // アカウントリストの表示/非表示を切り替え\n this.is_twitter_account_list_display = !this.is_twitter_account_list_display;\n\n // アカウントリストが表示されているなら、ハッシュタグリストを非表示にする\n if (this.is_twitter_account_list_display === true) {\n this.is_hashtag_list_display = false;\n }\n },\n\n // クリップボード内のデータがペーストされたときのイベント\n pasteClipboardData(event: ClipboardEvent) {\n\n // 一応配列になっているので回しているが、基本1回のペーストにつき DataTransferItem は1個しか入らない\n for (const clipboard_item of event.clipboardData.items) {\n\n // 画像のみを対象にする (DataTransferItem.type には MIME タイプが入る)\n if (clipboard_item.type.startsWith('image/')) {\n\n // クリップボード内の画像データを File オブジェクトとして取得し、キャプチャリストに追加\n const file = clipboard_item.getAsFile();\n this.addCaptureList(file, file.name);\n }\n }\n },\n\n // 選択されている Twitter アカウントを更新する\n updateSelectedTwitterAccount(twitter_account: ITwitterAccount) {\n this.selected_twitter_account_id = twitter_account.id;\n Utils.setSettingsItem('selected_twitter_account_id', this.selected_twitter_account_id);\n this.selected_twitter_account = twitter_account;\n\n // Twitter アカウントリストのオーバーレイを閉じる (少し待ってから閉じたほうが体感が良い)\n window.setTimeout(() => this.is_twitter_account_list_display = false, 150);\n },\n\n // キャプチャリスト内のキャプチャがクリックされたときのイベント\n clickCapture(capture: ITweetCapture) {\n\n // 選択されたキャプチャが3枚まで & まだ選択されていないならキャプチャをツイート対象に追加する\n if (this.tweet_captures.length < 4 && capture.selected === false) {\n capture.selected = true;\n this.tweet_captures.push(capture.blob);\n } else {\n // ツイート対象のキャプチャになっていたら取り除く\n const index = this.tweet_captures.findIndex(blob => blob === capture.blob);\n if (index > -1) {\n this.tweet_captures.splice(index, 1);\n }\n // キャプチャの選択を解除\n capture.selected = false;\n }\n },\n\n // 撮ったキャプチャを親コンポーネントから受け取り、キャプチャリストに追加する\n async addCaptureList(blob: Blob, filename: string) {\n\n if (this.captures_element === null) {\n this.captures_element = this.$el.querySelector('.tab-content');\n }\n\n // 撮ったキャプチャが50件を超えていたら、重くなるので古いものから削除する\n // 削除する前に Blob URL を revoke してリソースを解放するのがポイント\n if (this.captures.length > 50) {\n URL.revokeObjectURL(this.captures[0].image_url);\n this.captures.shift();\n }\n\n // キャプチャリストにキャプチャを追加\n const blob_url = URL.createObjectURL(blob);\n this.captures.push({\n blob: blob,\n filename: filename,\n image_url: blob_url,\n selected: false,\n focused: false,\n });\n\n // キャプチャリストを下にスクロール\n // this.$nextTick() のコールバックで DOM の更新を待つ\n this.$nextTick(() => {\n this.captures_element.scrollTo({\n top: this.captures_element.scrollHeight,\n behavior: 'smooth',\n });\n });\n },\n\n // 撮ったキャプチャに番組タイトルの透かしを描画する\n async drawProgramTitleOnCapture(capture: Blob): Promise {\n\n // キャプチャの Blob を createImageBitmap() で Canvas に描ける ImageBitmap に変換\n const image_bitmap = await createImageBitmap(capture);\n\n // OffscreenCanvas が使えるなら使う (OffscreenCanvas の方がパフォーマンスが良い)\n const canvas = ('OffscreenCanvas' in window) ?\n new OffscreenCanvas(image_bitmap.width, image_bitmap.height) : document.createElement('canvas');\n\n // Canvas にキャプチャを描画\n const context = canvas.getContext('2d');\n context.drawImage(image_bitmap, 0, 0);\n image_bitmap.close();\n\n // 描画設定\n context.font = `bold 22px 'YakuHanJPs', 'Open Sans', 'Hiragino Sans', 'Noto Sans JP', sans-serif`; // フォント\n context.fillStyle = 'rgba(255, 255, 255, 70%)'; // 半透明の白\n context.shadowColor = 'rgba(0, 0, 0, 100%)' // 影の色\n context.shadowBlur = 4; // 影をぼかすしきい値\n context.shadowOffsetX = 0; // 影のX座標\n context.shadowOffsetY = 0; // 影のY座標\n\n // 番組タイトルの透かしを描画\n switch (Utils.getSettingsItem('tweet_capture_watermark_position')) {\n case 'TopLeft': {\n context.textAlign = 'left'; // 左寄せ\n context.textBaseline = 'top'; // ベースラインを上寄せ\n context.fillText(this.channel.program_present.title, 16, 12);\n break;\n }\n case 'TopRight': {\n context.textAlign = 'right'; // 右寄せ\n context.textBaseline = 'top'; // ベースラインを上寄せ\n context.fillText(this.channel.program_present.title, canvas.width - 16, 12);\n break;\n }\n case 'BottomLeft': {\n context.textAlign = 'left'; // 左寄せ\n context.textBaseline = 'bottom'; // ベースラインを下寄せ\n context.fillText(this.channel.program_present.title, 16, canvas.height - 12);\n break;\n }\n case 'BottomRight': {\n context.textAlign = 'right'; // 右寄せ\n context.textBaseline = 'bottom'; // ベースラインを下寄せ\n context.fillText(this.channel.program_present.title, canvas.width - 16, canvas.height - 12);\n break;\n }\n }\n\n // Blob にして返す\n if ('OffscreenCanvas' in window) {\n return await (canvas as OffscreenCanvas).convertToBlob({type: 'image/jpeg', quality: 1});\n } else {\n return new Promise(resolve => (canvas as HTMLCanvasElement).toBlob(blob => resolve(blob), 'image/jpeg', 1));\n }\n },\n\n // チャンネル名から対応する局タグを取得する\n // とりあえず三大首都圏 + BS のみ対応\n getChannelHashtag(channel_name: string): string | null {\n // NHK\n if (channel_name.startsWith('NHK総合')) {\n return '#nhk';\n } else if (channel_name.startsWith('NHKEテレ')) {\n return '#etv';\n // 民放\n } else if (channel_name.startsWith('日テレ')) {\n return '#ntv';\n } else if (channel_name.startsWith('読売テレビ')) {\n return '#ytv';\n } else if (channel_name.startsWith('中京テレビ')) {\n return '#chukyotv';\n } else if (channel_name.startsWith('テレビ朝日')) {\n return '#tvasahi';\n } else if (channel_name.startsWith('ABCテレビ')) {\n return '#abc';\n } else if (channel_name.startsWith('メ~テレ')) {\n return '#nagoyatv';\n } else if (channel_name.startsWith('TBS') && !channel_name.includes('TBSチャンネル')) {\n return '#tbs';\n } else if (channel_name.startsWith('MBS')) {\n return '#mbs';\n } else if (channel_name.startsWith('CBC')) {\n return '#cbc';\n } else if (channel_name.startsWith('テレビ東京')) {\n return '#tvtokyo';\n } else if (channel_name.startsWith('テレビ大阪')) {\n return '#tvo';\n } else if (channel_name.startsWith('テレビ愛知')) {\n return '#tva';\n } else if (channel_name.startsWith('フジテレビ')) {\n return '#fujitv';\n } else if (channel_name.startsWith('関西テレビ')) {\n return '#kantele';\n } else if (channel_name.startsWith('東海テレビ')) {\n return '#tokaitv';\n // 独立局\n } else if (channel_name.startsWith('TOKYO MX')) {\n return '#tokyomx';\n } else if (channel_name.startsWith('tvk')) {\n return '#tvk';\n } else if (channel_name.startsWith('チバテレ')) {\n return '#chibatv';\n } else if (channel_name.startsWith('テレ玉')) {\n return '#teletama';\n } else if (channel_name.startsWith('サンテレビ')) {\n return '#suntv';\n } else if (channel_name.startsWith('KBS京都')) {\n return '#kbs';\n // BS・CS\n } else if (channel_name.startsWith('NHKBS1')) {\n return '#nhkbs1';\n } else if (channel_name.startsWith('NHKBSプレミアム')) {\n return '#nhkbsp';\n } else if (channel_name.startsWith('BS日テレ')) {\n return '#bsntv';\n } else if (channel_name.startsWith('BS朝日')) {\n return '#bsasahi';\n } else if (channel_name.startsWith('BS-TBS')) {\n return '#bstbs';\n } else if (channel_name.startsWith('BSテレ東')) {\n return '#bstvtokyo';\n } else if (channel_name.startsWith('BSフジ')) {\n return '#bsfuji';\n } else if (channel_name.startsWith('BS11イレブン')) {\n return '#bs11';\n } else if (channel_name.startsWith('BS12トゥエルビ')) {\n return '#bs12';\n } else if (channel_name.startsWith('AT-X')) {\n return '#at_x';\n }\n\n return null;\n },\n\n // ハッシュタグを整形(余計なスペースなどを削り、全角ハッシュを半角ハッシュへ、全角スペースを半角スペースに置換)\n formatHashtag(tweet_hashtag: string, from_hashtag_list: boolean = false): string {\n\n // ハッシュとスペースの表記ゆれを統一し、連続するハッシュやスペースを1つにする\n const tweet_hashtag_array = tweet_hashtag.trim()\n .replaceAll('♯', '#').replaceAll('#', '#').replace(/#{2,}/g, '#').replaceAll(' ', ' ').replaceAll(/ +/g,' ').split(' ')\n .filter(hashtag => hashtag !== '');\n\n // ハッシュタグがついてない場合にハッシュタグを付与\n for (let index in tweet_hashtag_array) {\n if (!tweet_hashtag_array[index].startsWith('#')) {\n tweet_hashtag_array[index] = `#${tweet_hashtag_array[index]}`;\n }\n }\n\n // 設定でオンになっている場合のみ、視聴中チャンネルの局タグを自動的に追加する (ハッシュタグリスト内のハッシュタグは除外)\n if (Utils.getSettingsItem('auto_add_watching_channel_hashtag') === true && from_hashtag_list === false) {\n const channel_hashtag = this.getChannelHashtag(this.channel.channel_name);\n if (channel_hashtag !== null) {\n if (tweet_hashtag_array.includes(channel_hashtag) === false) {\n tweet_hashtag_array.push(channel_hashtag);\n }\n }\n }\n\n return tweet_hashtag_array.join(' ');\n },\n\n // ツイートを送信する\n async sendTweet() {\n\n // ハッシュタグを整形\n this.tweet_hashtag = this.formatHashtag(this.tweet_hashtag);\n const tweet_hashtag = this.tweet_hashtag;\n\n // 実際に送るツイート本文を作成\n let tweet_text = this.tweet_text;\n if (tweet_hashtag !== '') { // ハッシュタグが入力されているときのみ\n switch (Utils.getSettingsItem('tweet_hashtag_position')) {\n // ツイート本文の前に追加する\n case 'Prepend': {\n tweet_text = `${tweet_hashtag} ${this.tweet_text}`;\n break;\n }\n // ツイート本文の後に追加する\n case 'Append': {\n tweet_text = `${this.tweet_text} ${tweet_hashtag}`;\n break;\n }\n // ツイート本文の前に追加してから改行する\n case 'PrependWithLineBreak': {\n tweet_text = `${tweet_hashtag}\\n${this.tweet_text}`;\n break;\n }\n // ツイート本文の後に改行してから追加する\n case 'AppendWithLineBreak': {\n tweet_text = `${this.tweet_text}\\n${tweet_hashtag}`;\n break;\n }\n }\n }\n\n // multipart/form-data でツイート本文と画像(選択されている場合)を送る\n const form_data = new FormData();\n form_data.append('tweet', tweet_text);\n for (let tweet_capture of this.tweet_captures) {\n // キャプチャへの透かしの描画がオンの場合、キャプチャの Blob を透かし付きのものに差し替える\n if (Utils.getSettingsItem('tweet_capture_watermark_position') !== 'None') {\n tweet_capture = await this.drawProgramTitleOnCapture(tweet_capture);\n }\n form_data.append('images', tweet_capture);\n }\n\n // 連投防止のため、フォーム上のツイート本文・キャプチャの選択・キャプチャのフォーカスを消去\n // 送信した感を出す意味合いもある\n for (const capture of this.captures) {\n capture.selected = false;\n capture.focused = false;\n }\n this.tweet_captures = [];\n this.tweet_text = '';\n\n // パネルを閉じるように親コンポーネントに伝える\n if (Utils.getSettingsItem('fold_panel_after_sending_tweet') === true) {\n this.$emit('panel_folding_requested');\n (this.$refs.tweet_text as HTMLTextAreaElement).blur(); // フォーカスを外す\n }\n\n try {\n\n // ツイート送信 API にリクエスト\n const result = await Vue.axios.post(`/twitter/accounts/${this.selected_twitter_account.screen_name}/tweets`, form_data, {\n headers: {'Content-Type': 'multipart/form-data'},\n });\n\n // 成功 or 失敗に関わらず detail の内容をそのまま通知する\n if (result.data.is_success === true) {\n this.player.notice(result.data.detail);\n } else {\n this.player.notice('エラー: ' + result.data.detail);\n }\n\n } catch (error) {\n console.error(error);\n this.player.notice('エラー: ツイートの送信に失敗しました。');\n }\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Twitter.vue?vue&type=template&id=4a0651e4&scoped=true&\"\nimport script from \"./Twitter.vue?vue&type=script&lang=ts&\"\nexport * from \"./Twitter.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Twitter.vue?vue&type=style&index=0&id=4a0651e4&prod&lang=scss&\"\nimport style1 from \"./Twitter.vue?vue&type=style&index=1&id=4a0651e4&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"4a0651e4\",\n null\n \n)\n\nexport default component.exports","\n\nimport { AxiosResponse } from 'axios';\nimport dayjs from 'dayjs';\n// @ts-ignore JavaScript で書かれているので型定義がなく、作ろうとするとややこしくなるので黙殺\nimport DPlayer from 'dplayer';\nimport mpegts from 'mpegts.js';\nimport Vue from 'vue';\n\nimport { ChannelTypePretty, IChannel, IChannelDefault } from '@/interface';\nimport Channel from '@/components/Panel/Channel.vue';\nimport Comment from '@/components/Panel/Comment.vue';\nimport Program from '@/components/Panel/Program.vue';\nimport Twitter from '@/components/Panel/Twitter.vue';\nimport Utils, { ChannelUtils, PlayerCaptureHandler, PlayerUtils, ProgramUtils } from '@/utils';\n\n// 低遅延モードオン時の再生バッファ (秒単位)\n// これ以上小さくすると再生が詰まりやすくなる印象\nconst PLAYBACK_BUFFER_SEC_LOW_LATENCY = 1.5;\n\n// 低遅延モードオフ時の再生バッファ (秒単位)\n// 3秒程度の遅延を許容する\nconst PLAYBACK_BUFFER_SEC = 3.0;\n\nexport default Vue.extend({\n name: 'TV-Watch',\n components: {\n Channel,\n Comment,\n Program,\n Twitter,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n ProgramUtils: ProgramUtils,\n\n // 現在時刻\n time: dayjs().format('YYYY/MM/DD HH:mm:ss'),\n\n // 表示されるパネルのタブ\n tv_panel_active_tab: Utils.getSettingsItem('tv_panel_active_tab'),\n\n // 背景の URL\n background_url: '',\n\n // プレイヤーのローディング状態\n // 既定でローディングとする\n is_loading: true,\n\n // プレイヤーが映像の再生をバッファリングしているか\n // 視聴開始時以外にも、ネットワークが遅くて再生が一時的に途切れたときなどで表示される\n // 既定でバッファリング中とする\n is_video_buffering: true,\n\n // プレイヤーの背景を表示するか\n // 既定で表示しない\n is_background_display: false,\n\n // コントロールを表示するか\n // 既定で表示する\n is_control_display: true,\n\n // パネルを表示するか\n // panel_display_state が 'AlwaysDisplay' なら常に表示し、'AlwaysFold' なら常に折りたたむ\n // 'RestorePreviousState' なら showed_panel_last_time の値を使い、前回の状態を復元する\n is_panel_display: (() => {\n switch (Utils.getSettingsItem('panel_display_state')) {\n case 'AlwaysDisplay':\n return true;\n case 'AlwaysFold':\n return false;\n case 'RestorePreviousState':\n return Utils.getSettingsItem('showed_panel_last_time');\n }\n })() as boolean,\n\n // フルスクリーン状態かどうか\n is_fullscreen: false,\n\n // IME 変換中かどうか\n is_ime_composing: false,\n\n // 仮想キーボードが表示されているか\n is_virtual_keyboard_display: false,\n\n // プレイヤーからのコメント送信から間もないかどうか\n is_comment_send_just_did: false,\n\n // インターバル ID\n // ページ遷移時に setInterval(), setTimeout() の実行を止めるのに使う\n // setInterval(), setTimeout() の返り値を登録する\n interval_ids: [] as number[],\n\n // コントロール表示切り替え用のインターバル ID\n // 混ぜるとダメなので独立させる\n control_interval_id: 0,\n\n // ***** チャンネル *****\n\n // チャンネル ID\n channel_id: this.$route.params.channel_id,\n\n // チャンネル情報\n // IChannelDefault に情報取得が完了するまでの間表示される初期値が定義されている\n channel: IChannelDefault,\n\n // 前のチャンネルのチャンネル情報\n channel_previous: IChannelDefault,\n\n // 次のチャンネルのチャンネル情報\n channel_next: IChannelDefault,\n\n // チャンネル情報リスト\n channels_list: new Map() as Map,\n\n // ***** プレイヤー *****\n\n // プレイヤー (DPlayer) のインスタンス\n player: null,\n\n // RomSound の AudioContext\n romsounds_context: null as AudioContext | null,\n\n // RomSound の AudioBuffer(音声データ)が入るリスト\n romsounds_buffers: [] as AudioBuffer[] | null,\n\n // イベントソースのインスタンス\n eventsource: null as EventSource | null,\n\n // フルスクリーン状態が切り替わったときのハンドラー\n fullscreen_handler: null as () => void | null,\n\n // キャプチャハンドラーのインスタンス\n capture_handler: null as PlayerCaptureHandler | null,\n\n // ***** キーボードショートカット *****\n\n // ショートカットキーのハンドラー\n shortcut_key_handler: null as (event: KeyboardEvent) => void | null,\n\n // ショートカットキーの最終押下時刻のタイムスタンプ\n shortcut_key_pressed_at: Date.now(),\n\n // キーボードショートカットの一覧のモーダルを表示するか\n shortcut_key_modal: false,\n\n // キーボードショートカットの一覧に表示するショートカットキーのリスト\n shortcut_key_list: {\n left_column: [\n {\n name: '全般',\n icon: 'fluent:home-20-filled',\n icon_height: '22px',\n shortcuts: [\n { name: '数字キー・テンキーに対応するリモコン番号 (1~12) の地デジチャンネルに切り替える', keys: [{name: '1~9, 0, -(=), ^(~)', icon: false}] },\n { name: '数字キー・テンキーに対応するリモコン番号 (1~12) の BS チャンネルに切り替える', keys: [{name: 'Shift', icon: false}, {name: '1~9, 0, -(=), ^(~)', icon: false}] },\n { name: '前のチャンネルに切り替える', keys: [{name: 'fluent:arrow-up-12-filled', icon: true}] },\n { name: '次のチャンネルに切り替える', keys: [{name: 'fluent:arrow-down-12-filled', icon: true}] },\n { name: 'キーボードショートカットの一覧を表示する', keys: [{name: '/(?)', icon: false}] },\n ]\n },\n {\n name: 'プレイヤー',\n icon: 'fluent:play-20-filled',\n icon_height: '20px',\n shortcuts: [\n { name: '再生 / 一時停止の切り替え', keys: [{name: 'Space', icon: false}] },\n { name: '再生 / 一時停止の切り替え (キャプチャタブ表示時)', keys: [{name: 'Shift', icon: false}, {name: 'Space', icon: false}] },\n { name: 'プレイヤーの音量を上げる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-up-12-filled', icon: true}] },\n { name: 'プレイヤーの音量を下げる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-down-12-filled', icon: true}] },\n { name: '停止して0.5秒早戻し', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-left-12-filled', icon: true}] },\n { name: '停止して0.5秒早送り', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'fluent:arrow-right-12-filled', icon: true}] },\n { name: 'フルスクリーンの切り替え', keys: [{name: 'F', icon: false}] },\n { name: 'ライブストリームの同期', keys: [{name: 'W', icon: false}] },\n { name: 'Picture-in-Picture の表示切り替え', keys: [{name: 'E', icon: false}] },\n { name: '字幕の表示切り替え', keys: [{name: 'S', icon: false}] },\n { name: 'コメントの表示切り替え', keys: [{name: 'D', icon: false}] },\n { name: '映像をキャプチャする', keys: [{name: 'C', icon: false}] },\n { name: '映像をコメントを付けてキャプチャする', keys: [{name: 'V', icon: false}] },\n { name: 'コメント入力フォームにフォーカスする', keys: [{name: 'M', icon: false}] },\n { name: 'コメント入力フォームを閉じる', keys: [{name: Utils.CtrlOrCmd(), icon: false}, {name: 'M', icon: false}] },\n ]\n },\n ],\n right_column: [\n {\n name: 'パネル',\n icon: 'fluent:panel-right-20-filled',\n icon_height: '24px',\n shortcuts: [\n { name: 'パネルの表示切り替え', keys: [{name: 'P', icon: false}] },\n { name: '番組情報タブを表示する', keys: [{name: 'K', icon: false}] },\n { name: 'チャンネルタブを表示する', keys: [{name: 'L', icon: false}] },\n { name: 'コメントタブを表示する', keys: [{name: ';(+)', icon: false}] },\n { name: 'Twitter タブを表示する', keys: [{name: ':(*)', icon: false}] },\n ]\n },\n {\n name: 'Twitter',\n icon: 'fa-brands:twitter',\n icon_height: '22px',\n shortcuts: [\n { name: 'ツイート検索タブを表示する', keys: [{name: '[ (「)', icon: false}] },\n { name: 'タイムラインタブを表示する', keys: [{name: '] (」)', icon: false}] },\n { name: 'キャプチャタブを表示する', keys: [{name: '\(¥)', icon: false}] },\n { name: 'ツイート入力フォームにフォーカスを当てる/フォーカスを外す', keys: [{name: 'Tab', icon: false}] },\n { name: 'キャプチャにフォーカスする', keys: [{name: 'キャプチャタブを表示', icon: false}, {name: 'fluent:arrow-up-12-filled;fluent:arrow-down-12-filled;fluent:arrow-left-12-filled;fluent:arrow-right-12-filled', icon: true}] },\n { name: 'キャプチャを拡大表示する/
キャプチャの拡大表示を閉じる', keys: [{name: 'キャプチャにフォーカス', icon: false}, {name: 'Enter', icon: false}] },\n { name: 'キャプチャを選択する/
キャプチャの選択を解除する', keys: [{name: 'キャプチャにフォーカス', icon: false}, {name: 'Space', icon: false}] },\n { name: 'クリップボード内の画像を
キャプチャとして取り込む', keys: [{name: 'ツイート入力
フォームにフォーカス', icon: false}, {name: Utils.CtrlOrCmd(), icon: false}, {name: 'V', icon: false}] },\n { name: 'ツイートを送信する', keys: [{name: 'Twitter タブを表示', icon: false}, {name: Utils.CtrlOrCmd(), icon: false}, {name: 'Enter', icon: false}] },\n ]\n },\n ]\n }\n }\n },\n // 開始時に実行\n async created() {\n\n // Virtual Keyboard API に対応している場合は、仮想キーボード周りの操作を自力で行うことをブラウザに伝える\n // この視聴画面のみ\n if ('virtualKeyboard' in navigator) {\n navigator.virtualKeyboard.overlaysContent = true;\n // 仮想キーボードが表示されたり閉じられたときのイベント\n navigator.virtualKeyboard.ongeometrychange = (event) => {\n if (event.target.boundingRect.width === 0 && event.target.boundingRect.height === 0) {\n this.is_virtual_keyboard_display = false;\n } else {\n this.is_virtual_keyboard_display = true;\n }\n }\n }\n\n // 再生セッションを初期化\n this.init();\n\n // RomSound を鳴らすための AudioContext を生成\n this.romsounds_context = new AudioContext();\n\n // 01 ~ 14 まですべての RomSound を読み込む\n for (let index = 1; index <= 14; index++) {\n\n // ArrayBuffer として RomSound を取得\n const url = `/assets/romsounds/${index.toString().padStart(2, '0')}.wav`;\n const audio_data = await Vue.axios.get(url, {\n baseURL: '', // BaseURL を明示的にクライアントのルートに設定\n responseType: 'arraybuffer',\n });\n\n // ArrayBuffer をデコードして AudioBuffer にし、すぐ呼び出せるように貯めておく\n // ref: https://ics.media/entry/200427/\n this.romsounds_buffers.push(await this.romsounds_context.decodeAudioData(audio_data.data));\n }\n },\n // 終了前に実行\n beforeDestroy() {\n\n // 仮想キーボード周りの操作をブラウザに戻す\n if ('virtualKeyboard' in navigator) {\n navigator.virtualKeyboard.overlaysContent = false;\n }\n\n // destroy() を実行\n // 別のページへ遷移するため、DPlayer のインスタンスを確実に破棄する\n // さもなければ、ブラウザがリロードされるまでバックグラウンドで永遠に再生され続けてしまう\n this.destroy(true);\n\n // AudioContext のリソースを解放\n this.romsounds_context.close();\n },\n // チャンネル切り替え時に実行\n // コンポーネント(インスタンス)は再利用される\n // ref: https://router.vuejs.org/ja/guide/advanced/navigation-guards.html#%E3%83%AB%E3%83%BC%E3%83%88%E5%8D%98%E4%BD%8D%E3%82%AB%E3%82%99%E3%83%BC%E3%83%88%E3%82%99\n beforeRouteUpdate(to, from, next) {\n\n // 前の再生セッションを破棄して終了する\n this.destroy();\n\n // チャンネル ID を次のチャンネルのものに切り替える\n this.channel_id = to.params.channel_id;\n\n // 既に取得済みのチャンネル情報で、前・現在・次のチャンネル情報を更新する\n [this.channel_previous, this.channel, this.channel_next]\n = ChannelUtils.getPreviousAndCurrentAndNextChannel(this.channels_list, this.channel_id);\n\n // ハッシュタグフォームのリセットがオンなら、ハッシュタグフォームを空にする\n if (Utils.getSettingsItem('reset_hashtag_when_program_switches') === true) {\n (this.$refs.Twitter as InstanceType).tweet_hashtag = '';\n }\n\n // 0.5秒だけ待ってから、新しい再生セッションを初期化する\n // 連続して押した時などに毎回再生処理を開始しないように猶予を設ける\n this.interval_ids.push(window.setTimeout(() => this.init(), 500));\n\n next();\n },\n watch: {\n // 前回視聴画面を開いた際にパネルが表示されていたかどうかを保存\n is_panel_display() {\n Utils.setSettingsItem('showed_panel_last_time', this.is_panel_display);\n }\n },\n methods: {\n\n // 再生セッションを初期化する\n init() {\n\n // ローディング中の背景画像をランダムで設定\n this.background_url = PlayerUtils.generatePlayerBackgroundURL();\n\n // コントロール表示タイマーを実行\n this.controlDisplayTimer();\n\n // チャンネル情報を取得\n this.update();\n\n // 現在時刻を1秒おきに更新\n this.interval_ids.push(window.setInterval(() => {\n this.time = dayjs().format('YYYY/MM/DD HH:mm:ss');\n }, 1 * 1000));\n\n // 00秒までの残り秒数\n // 現在 16:01:34 なら 26 (秒) になる\n const residue_second = 60 - (Math.floor(new Date().getTime() / 1000) % 60);\n\n // 00秒になるまで待ってから\n // 番組は基本1分単位で組まれているため、20秒や45秒など中途半端な秒数で更新してしまうと反映が遅れてしまう\n this.interval_ids.push(window.setTimeout(() => {\n\n // チャンネル情報を更新\n this.update();\n\n // チャンネル情報を定期的に更新\n this.interval_ids.push(window.setInterval(() => {\n this.update();\n }, 30 * 1000)); // 30秒おき\n\n }, residue_second * 1000));\n },\n\n // チャンネル情報一覧を取得し、画面を更新する\n async update() {\n\n // チャンネル ID が未定義なら実行しない(フェイルセーフ)\n if (this.$route.params.channel_id === undefined) {\n return;\n }\n\n // チャンネル情報 API にアクセス\n let channel_response: AxiosResponse;\n try {\n channel_response = await Vue.axios.get(`/channels/${this.channel_id}`);\n } catch (error) {\n\n // エラー内容を表示\n console.error(error);\n\n // ステータスコードが 422(チャンネルが存在しない)なら 404 ページにリダイレクト\n // 正確には 404 ページ自体がルートとして存在するわけじゃないけど、そもそも存在しないページなら 404 になるので\n if (error.response && error.response.status === 422 && error.response.data.detail === 'Specified channel_id was not found') {\n await this.$router.push({path: '/not-found/'});\n }\n\n // 処理を中断\n return;\n }\n\n // 取得したチャンネル情報と現在のチャンネル情報の NID-SID-EID の組み合わせが異なる場合\n // ハッシュタグフォームのリセットがオンなら、ハッシュタグフォームを空にする\n const channel_response_data = channel_response.data as IChannel;\n if ((this.channel.id !== channel_response_data.id) || // チャンネルが異なる\n (this.channel.program_present !== null && channel_response_data.program_present === null) || // 番組情報あり→番組情報なし\n (this.channel.program_present === null && channel_response_data.program_present !== null) || // 番組情報なし→番組情報あり\n ((this.channel.program_present !== null && channel_response_data.program_present !== null) &&\n (this.channel.program_present.id !== channel_response_data.program_present.id))) { // 番組情報あり→番組情報あり & 番組が異なる\n if (Utils.getSettingsItem('reset_hashtag_when_program_switches') === true) {\n const twitter_component = this.$refs.Twitter as InstanceType;\n twitter_component.tweet_hashtag = twitter_component.formatHashtag('');\n }\n }\n\n // チャンネル情報を代入\n this.channel = channel_response_data;\n\n // プレイヤーがまだ初期化されていない or 他のチャンネルからの切り替えですでにプレイヤーが初期化されているけど破棄が可能\n // update() 自体は初期化時以外にも1分ごとに定期実行されるため、その際に毎回プレイヤーを再初期化しないようにする\n if (this.player === null || this.player.KonomiTVCanDestroy === true) {\n\n // プレイヤー (DPlayer) 周りのセットアップ\n this.initPlayer();\n\n // サーバーから送られてくるメッセージのイベントハンドラーを初期化\n this.initEventHandler();\n\n // キャプチャのイベントハンドラーを初期化\n this.initCaptureHandler();\n\n // ショートカットキーのイベントハンドラーを初期化\n // 事前に前のイベントハンドラーを削除しておかないと、重複してキー操作が実行されてしまう\n // 直前で実行しないと上下キーでのチャンネル操作が動かなくなる\n document.removeEventListener('keydown', this.shortcut_key_handler);\n this.initShortcutKeyHandler();\n }\n\n // 副音声がない番組でプレイヤー上で副音声に切り替えられないように\n // 音声多重放送でもデュアルモノでもない番組のみ\n if ((this.channel.program_present === null) ||\n ((this.channel.program_present.primary_audio_type !== '1/0+1/0モード(デュアルモノ)') &&\n (this.channel.program_present.secondary_audio_type === null))) {\n\n // クラスを付与\n this.player.template.audioItem[1].classList.add('dplayer-setting-audio-item--disabled');\n\n // 現在副音声が選択されている可能性を考慮し、明示的に主音声に切り替える\n if (this.player.plugins.mpegts) {\n window.setTimeout(() => { // プレイヤーの初期化が完了するまで少し待つ\n this.player.template.audioItem[0].classList.add('dplayer-setting-audio-current');\n this.player.template.audioItem[1].classList.remove('dplayer-setting-audio-current');\n this.player.template.audioValue.textContent = this.player.tran('Primary audio');\n try {\n this.player.plugins.mpegts.switchPrimaryAudio();\n } catch (error) {\n // pass\n }\n }, 300);\n }\n\n // 音声多重放送かデュアルモノなので、副音声への切り替えを有効化\n } else {\n\n // クラスを削除\n this.player.template.audioItem[1].classList.remove('dplayer-setting-audio-item--disabled');\n }\n\n // チャンネル情報一覧 API にアクセス\n // チャンネル情報 API と同時にアクセスするとむしろレスポンスが遅くなるので、返ってくるのを待ってから実行\n let channels_response: AxiosResponse;\n try {\n channels_response = await Vue.axios.get('/channels');\n } catch (error) {\n console.error(error); // エラー内容を表示\n return;\n }\n\n // is_display が true または現在表示中のチャンネルのみに絞り込むフィルタ関数\n // 放送していないサブチャンネルを表示から除外する\n const filter = (channel: IChannel) => {\n return channel.is_display || this.channel_id === channel.channel_id;\n }\n\n // チャンネルリストを再構築\n // 1つでもチャンネルが存在するチャンネルタイプのみ表示するように\n // たとえば SKY (スカパー!プレミアムサービス) のタブは SKY に属すチャンネルが1つもない(=受信できない)なら表示されない\n this.channels_list = new Map();\n this.channels_list.set('ピン留め', []); // ピン留めタブの準備\n if (channels_response.data.GR.length > 0) this.channels_list.set('地デジ', channels_response.data.GR.filter(filter));\n if (channels_response.data.BS.length > 0) this.channels_list.set('BS', channels_response.data.BS.filter(filter));\n if (channels_response.data.CS.length > 0) this.channels_list.set('CS', channels_response.data.CS.filter(filter));\n if (channels_response.data.CATV.length > 0) this.channels_list.set('CATV', channels_response.data.CATV.filter(filter));\n if (channels_response.data.SKY.length > 0) this.channels_list.set('SKY', channels_response.data.SKY.filter(filter));\n if (channels_response.data.STARDIGIO.length > 0) this.channels_list.set('StarDigio', channels_response.data.STARDIGIO.filter(filter));\n\n // ピン留めされているチャンネルの ID を取得\n const pinned_channel_ids = Utils.getSettingsItem('pinned_channel_ids');\n\n // ピン留めされているチャンネル情報のリスト\n const pinned_channels = [] as IChannel[];\n\n // チャンネル ID が一致したチャンネルの情報を保存する\n for (const pinned_channel_id of pinned_channel_ids) {\n const pinned_channel_type = ChannelUtils.getChannelType(pinned_channel_id, true) as ChannelTypePretty;\n const pinned_channel = this.channels_list.get(pinned_channel_type).find((channel) => {\n return channel.channel_id === pinned_channel_id; // チャンネル ID がピン留めされているチャンネルのものと同じ\n });\n // チャンネル情報を取得できているときだけ\n // サブチャンネルをピン留めしたが、マルチ編成が終了して現在は放送していない場合などに備える (BS142 など)\n // 現在放送していないチャンネルは this.channels_list に入れた段階で弾いているため、チャンネル情報を取得できない\n if (pinned_channel !== undefined) {\n pinned_channels.push(pinned_channel);\n }\n }\n\n // pinned_channels に何か入っていたらピン留めタブを表示するし、そうでなければ表示しない\n if (pinned_channels.length > 0) {\n this.channels_list.set('ピン留め', pinned_channels);\n } else {\n this.channels_list.delete('ピン留め');\n }\n\n // 前と次のチャンネル ID を取得する\n [this.channel_previous, , this.channel_next] = ChannelUtils.getPreviousAndCurrentAndNextChannel(this.channels_list, this.channel_id);\n\n // MediaSession API を使い、メディア通知の表示をカスタマイズ\n if ('mediaSession' in navigator) {\n\n // アートワークとして表示するアイコン\n const artwork = [\n {src: '/assets/images/icons/icon-maskable-192px.png', sizes: '192x192', type: 'image/png'},\n {src: '/assets/images/icons/icon-maskable-512px.png', sizes: '512x512', type: 'image/png'},\n ];\n\n // メディア通知の表示をカスタマイズ\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel.program_present ? this.channel.program_present.title : '放送休止',\n artist: this.channel.channel_name,\n artwork: artwork,\n });\n\n // 再生状況のステータスを設定\n if ('setPositionState' in navigator.mediaSession) {\n navigator.mediaSession.setPositionState({\n duration: 0, // ライブなので0(長さなしを表すらしい)に設定\n playbackRate: 1, // ライブなので再生速度は常に1になる\n });\n }\n\n // メディア通知上のボタンが押されたときのイベント\n navigator.mediaSession.setActionHandler('play', () => { this.player.play() }); // 再生\n navigator.mediaSession.setActionHandler('pause', () => { this.player.pause() }); // 停止\n navigator.mediaSession.setActionHandler('previoustrack', async () => { // 前のチャンネルに切り替え\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel_previous.program_present ? this.channel_previous.program_present.title : '放送休止',\n artist: this.channel_previous.channel_name,\n artwork: artwork,\n });\n // ルーティングを前のチャンネルに置き換える\n await this.$router.push({path: `/tv/watch/${this.channel_previous.channel_id}`});\n });\n navigator.mediaSession.setActionHandler('nexttrack', async () => { // 次のチャンネルに切り替え\n navigator.mediaSession.metadata = new MediaMetadata({\n title: this.channel_next.program_present ? this.channel_next.program_present.title : '放送休止',\n artist: this.channel_next.channel_name,\n artwork: artwork,\n });\n // ルーティングを次のチャンネルに置き換える\n await this.$router.push({path: `/tv/watch/${this.channel_next.channel_id}`});\n });\n }\n },\n\n // マウスが動いたりタップされた時のイベント\n // 3秒間何も操作がなければコントロールを非表示にする\n controlDisplayTimer(event: Event | null = null, is_player_event: boolean = false) {\n\n // タッチデバイスかどうか\n // DPlayer の UA 判定コードと同一\n const is_touch_device = /iPhone|iPad|iPod|Windows|Macintosh|Android|Mobile/i.test(navigator.userAgent) && 'ontouchend' in document;\n\n // タッチデバイスで mousemove 、あるいはタッチデバイス以外で touchmove か click が発火した時は実行じない\n if (is_touch_device == true && event !== null && event.type === 'mousemove') return;\n if (is_touch_device == false && event !== null && (event.type === 'touchmove' || event.type === 'click')) return;\n\n // 以前セットされたタイマーを止める\n window.clearTimeout(this.control_interval_id);\n\n // setTimeout に渡すタイマー関数\n const timeout = () => {\n\n // コメント入力フォームが表示されているときは実行しない\n // タイマーを掛け直してから抜ける\n if (this.player !== null && this.player.template.controller.classList.contains('dplayer-controller-comment')) {\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n return;\n }\n\n // コントロールを非表示にする\n this.is_control_display = false;\n\n // プレイヤーのコントロールと設定パネルを非表示にする\n if (this.player !== null) {\n this.player.controller.hide();\n this.player.setting.hide();\n }\n }\n\n // タッチデバイスでプレイヤー画面がクリックされたとき\n if (is_touch_device === true && is_player_event === true) {\n\n // プレイヤーのコントロールの表示状態に合わせる\n if (this.player.controller.isShow()) {\n\n // コントロールを表示する\n this.is_control_display = true;\n\n // プレイヤーのコントロールを表示する\n this.player.controller.show();\n\n // 3秒間何も操作がなければコントロールを非表示にする\n // 3秒間の間一度でもマウスが動けばタイマーが解除されてやり直しになる\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n\n } else {\n\n // コントロールを非表示にする\n this.is_control_display = false;\n\n // プレイヤーのコントロールと設定パネルを非表示にする\n this.player.controller.hide();\n this.player.setting.hide();\n }\n\n // それ以外の画面がクリックされたとき\n } else {\n\n // コントロールを表示する\n this.is_control_display = true;\n\n // プレイヤーのコントロールを表示する\n if (this.player !== null) {\n this.player.controller.show();\n }\n\n // 3秒間何も操作がなければコントロールを非表示にする\n // 3秒間の間一度でもマウスが動けばタイマーが解除されてやり直しになる\n this.control_interval_id = window.setTimeout(timeout, 3 * 1000);\n }\n },\n\n // プレイヤーを初期化する\n initPlayer() {\n\n // mpegts.js を window 直下に入れる\n // こうしないと DPlayer が mpegts.js を認識できない\n (window as any).mpegts = mpegts;\n\n // すでに DPlayer が初期化されている場合は破棄する\n // チャンネル切り替え時などが該当する\n if (this.player !== null && this.player.KonomiTVCanDestroy === true) {\n try {\n this.player.destroy();\n } catch (error) {\n // mpegts.js をうまく破棄できない場合\n if (this.player.plugins.mpegts !== undefined) {\n this.player.plugins.mpegts.destroy();\n }\n }\n this.player = null;\n }\n\n // 低遅延モードであれば低遅延向けの再生バッファを、そうでなければ通常の再生バッファをセット (秒単位)\n const playback_buffer_sec = Utils.getSettingsItem('tv_low_latency_mode') ?\n PLAYBACK_BUFFER_SEC_LOW_LATENCY : PLAYBACK_BUFFER_SEC;\n\n // DPlayer を初期化\n this.player = new DPlayer({\n container: this.$el.querySelector('.watch-player__dplayer'),\n theme: '#E64F97', // テーマカラー\n lang: 'ja-jp', // 言語\n live: true, // ライブモード\n liveSyncMinBufferSize: playback_buffer_sec, // ライブモードで同期する際の最小バッファサイズ\n loop: false, // ループ再生 (ライブのため無効化)\n airplay: false, // AirPlay 機能 (うまく動かないため無効化)\n autoplay: true, // 自動再生\n hotkey: false, // ショートカットキー(こちらで制御するため無効化)\n screenshot: false, // スクリーンショット (こちらで制御するため無効化)\n volume: 1.0, // 音量の初期値\n // 映像\n video: {\n // デフォルトの品質\n // ラジオチャンネルでは常に 48KHz/192kbps に固定する\n defaultQuality: (this.channel.is_radiochannel) ? '48kHz/192kbps' : Utils.getSettingsItem('tv_streaming_quality'),\n // 品質リスト\n quality: (() => {\n const qualities = [];\n // ラジオチャンネル\n // API が受け付ける品質の値は通常のチャンネルと同じだが (手抜き…)、実際の品質は 48KHz/192kbps で固定される\n // ラジオチャンネルの場合は、1080p と渡しても 48kHz/192kbps 固定の音声だけの MPEG-TS が配信される\n if (this.channel.is_radiochannel) {\n qualities.push({\n name: '48kHz/192kbps',\n type: 'mpegts',\n url: `${Utils.api_base_url}/streams/live/${this.channel_id}/1080p/mpegts`,\n });\n // 通常のチャンネル\n } else {\n // ブラウザが H.265 / HEVC の再生に対応していて、かつ通信節約モードが有効なとき\n // API に渡す画質に -hevc のプレフィックスをつける\n let hevc_prefix = '';\n if (PlayerUtils.isHEVCVideoSupported() && Utils.getSettingsItem('tv_data_saver_mode') === true) {\n hevc_prefix = '-hevc';\n }\n for (const quality of ['1080p-60fps', '1080p', '810p', '720p', '540p', '480p', '360p', '240p']) {\n qualities.push({\n name: quality === '1080p-60fps' ? '1080p (60fps)' : quality,\n type: 'mpegts',\n url: `${Utils.api_base_url}/streams/live/${this.channel_id}/${quality}${hevc_prefix}/mpegts`,\n });\n }\n }\n return qualities;\n })(),\n },\n // コメント\n danmaku: {\n user: 'KonomiTV', // 便宜上 KonomiTV に固定\n speedRate: Utils.getSettingsItem('comment_speed_rate'), // コメントの流れる速度\n fontSize: Utils.getSettingsItem('comment_font_size'), // コメントのフォントサイズ\n },\n // コメント API バックエンド\n apiBackend: {\n // コメント受信時\n read: (options) => {\n // 成功したことにして通知を抑制\n options.success([{}]);\n },\n // コメント送信時\n send: async (options) => {\n // Comment コンポーネント内のコメント送信メソッドを呼び出す\n // ref: https://stackoverflow.com/a/65729556/17124142 ($refs への型設定)\n await (this.$refs.Comment as InstanceType).sendComment(options);\n },\n },\n // プラグイン\n pluginOptions: {\n // mpegts.js\n mpegts: {\n config: {\n // Web Worker を有効にする\n enableWorker: true,\n // HTMLMediaElement の内部バッファによるライブストリームの遅延を追跡する\n // liveBufferLatencyChasing と異なり、いきなり再生時間をスキップするのではなく、\n // 再生速度を少しだけ上げることで再生を途切れさせることなく遅延を追跡する\n liveSync: Utils.getSettingsItem('tv_low_latency_mode'),\n // 許容する HTMLMediaElement の内部バッファの最大値 (秒単位, 3秒)\n liveSyncMaxLatency: 3,\n // HTMLMediaElement の内部バッファ (遅延) が liveSyncMaxLatency を超えたとき、ターゲットとする遅延時間 (秒単位)\n liveSyncTargetLatency: playback_buffer_sec,\n // ライブストリームの遅延の追跡に利用する再生速度 (x1.1)\n // 遅延が 3 秒を超えたとき、遅延が playback_buffer_sec を下回るまで再生速度が x1.1 に設定される\n liveSyncPlaybackRate: 1.1,\n }\n },\n // aribb24.js\n aribb24: {\n // 描画フォント\n normalFont: `\"${Utils.getSettingsItem('caption_font')}\", sans-serif`,\n // 縁取りする色\n forceStrokeColor: Utils.getSettingsItem('always_border_caption_text') ? true : false,\n // 背景色\n forceBackgroundColor: Utils.getSettingsItem('specify_caption_background_color') ?\n Utils.getSettingsItem('caption_background_color') : null,\n // DRCS 文字を対応する Unicode 文字に置換\n drcsReplacement: true,\n // 高解像度の字幕 Canvas を取得できるように\n enableRawCanvas: true,\n // 縁取りに strokeText API を利用\n useStrokeText: true,\n // Unicode 領域の代わりに私用面の領域を利用 (Windows TV 系フォントのみ)\n usePUA: (() => {\n const font = Utils.getSettingsItem('caption_font') as string;\n const context = document.createElement('canvas').getContext('2d');\n context.font = `10px ${font}`;\n context.fillText('Test', 0, 0);\n if (font.startsWith('Windows TV')) {\n return true;\n } else {\n return false;\n }\n })(),\n // 文字スーパーの PRA (内蔵音再生コマンド) のコールバックを指定\n PRACallback: async (index: number) => {\n\n // 設定で文字スーパーが無効なら実行しない\n if (Utils.getSettingsItem('tv_show_superimpose') === false) return;\n\n // index に応じた内蔵音を鳴らす\n // ref: https://ics.media/entry/200427/\n // ref: https://www.ipentec.com/document/javascript-web-audio-api-change-volume\n\n // 自動再生ポリシーに引っかかったなどで AudioContext が一時停止されている場合、一度 resume() する必要がある\n // resume() するまでに何らかのユーザーのジェスチャーが行われているはず…\n // なくても動くこともあるみたいだけど、念のため\n if (this.romsounds_context.state === 'suspended') {\n await this.romsounds_context.resume();\n }\n\n // index で指定された音声データを読み込み\n const buffer_source_node = this.romsounds_context.createBufferSource();\n buffer_source_node.buffer = this.romsounds_buffers[index];\n\n // GainNode につなげる\n const gain_node = this.romsounds_context.createGain();\n buffer_source_node.connect(gain_node);\n\n // 出力につなげる\n gain_node.connect(this.romsounds_context.destination);\n\n // 音量を元の wav の3倍にする (1倍だと結構小さめ)\n gain_node.gain.value = 3;\n\n // 再生開始\n buffer_source_node.start(0);\n },\n }\n },\n // 字幕\n subtitle: {\n type: 'aribb24', // aribb24.js を有効化\n }\n });\n\n // デバッグ用にプレイヤーインスタンスも window 直下に入れる\n (window as any).player = this.player;\n\n // プレイヤー側のコントロール非表示タイマーを無効化(上書き)\n // 無効化しておかないと、controlDisplayTimer() と競合してしまう\n // 上書き元のコードは https://github.com/tsukumijima/DPlayer/blob/master/src/js/controller.js#L387-L395 にある\n this.player.controller.setAutoHide = (time: number) => {};\n\n // ***** コメント送信時のイベントハンドラー *****\n\n // コメントが送信されたときに、プレイヤーからのコメント送信から間もないかどうかのフラグを立てる (0.1秒後に解除する)\n // コメントを送信するとコメント入力フォームへのフォーカスが外れるため、ページ全体の keydown イベントでは\n // Enter キーの押下がコメント送信由来のイベントかキャプチャ拡大表示由来のイベントかを判断できない\n // そこで、コメント入力フォームフォーカス中に Enter キーが押された場合(=コメント送信時)に 0.1 秒間フラグを立てることで、\n // ショートカットキーハンドラーがコメント送信由来のイベントであることを判定できるようにしている\n this.player.template.commentInput.addEventListener('keydown', (event) => {\n if (event.code === 'Enter') {\n this.is_comment_send_just_did = true;\n setTimeout(() => this.is_comment_send_just_did = false, 100);\n }\n });\n\n // 「コメント送信後にコメント入力フォームを閉じる」がオフになっている時のために、プレイヤー側のコメント送信関数を上書き\n // 上書き部分以外の処理内容は概ね https://github.com/tsukumijima/DPlayer/blob/master/src/js/comment.js に準じる\n this.player.comment.send = () => {\n\n // コメント入力フォームへのフォーカスを外す (「コメント送信後にコメント入力フォームを閉じる」がオンのときだけ)\n if (Utils.getSettingsItem('close_comment_form_after_sending') === true) {\n this.player.template.commentInput.blur();\n }\n\n // 空コメントを弾く\n if (!this.player.template.commentInput.value.replace(/^\\s+|\\s+$/g, '')) {\n this.player.notice(this.player.tran('Please input danmaku content!'));\n return;\n }\n\n // コメントを送信\n this.player.danmaku.send(\n {\n text: this.player.template.commentInput.value,\n color: this.player.container.querySelector('.dplayer-comment-setting-color input:checked').value,\n type: this.player.container.querySelector('.dplayer-comment-setting-type input:checked').value,\n size: this.player.container.querySelector('.dplayer-comment-setting-size input:checked').value,\n },\n // 送信完了後にコメント入力フォームを閉じる ([コメント送信後にコメント入力フォームを閉じる] がオンのときだけ)\n () => {\n if (Utils.getSettingsItem('close_comment_form_after_sending') === true) {\n this.player.comment.hide();\n }\n },\n true,\n );\n\n // 重複送信を防ぐ\n this.player.template.commentInput.value = '';\n };\n\n // ***** 設定パネルのショートカット一覧へのリンクのイベントハンドラー *****\n\n // 設定パネルにショートカット一覧を表示するリンクを動的に追加する\n // タッチデバイスでは実行しない\n const is_touch_device = /iPhone|iPad|iPod|Macintosh|Android|Mobile/i.test(navigator.userAgent) && 'ontouchend' in document;\n if (is_touch_device === false) {\n this.player.template.settingOriginPanel.insertAdjacentHTML('beforeend', `\n
\n キーボードショートカット\n
\n \n \n \n
\n
`)\n\n // 設定パネルの高さを再設定\n const settingOriginPanelHeight = this.player.template.settingOriginPanel.scrollHeight;\n this.player.template.settingBox.style.clipPath = `inset(calc(100% - ${settingOriginPanelHeight}px) 0 0 round 7px)`;\n\n // 設定パネルのショートカット一覧を表示するリンクがクリックされたときのイベント\n // リアクティブではないので、手動でやらないといけない…\n this.$el.querySelector('.dplayer-setting-keyboard-shortcut').addEventListener('click', () => {\n this.player.setting.hide(); // 設定パネルを閉じる\n this.shortcut_key_modal = true;\n });\n }\n\n // ***** フルスクリーンのイベントハンドラー *****\n\n // フルスクリーンにするコンテナ要素(ページ全体)\n const fullscreen_container = document.querySelector('.v-application');\n this.fullscreen_handler = () => this.is_fullscreen = this.player.fullScreen.isFullScreen();\n if (fullscreen_container.onfullscreenchange !== undefined) {\n fullscreen_container.addEventListener('fullscreenchange', this.fullscreen_handler);\n } else {\n fullscreen_container.addEventListener('webkitfullscreenchange', this.fullscreen_handler);\n }\n\n // DPlayer のフルスクリーン関係のメソッドを無理やり上書きし、KonomiTV の UI と統合する\n // 上書き元のコードは https://github.com/tsukumijima/DPlayer/blob/master/src/js/fullscreen.js にある\n // フルスクリーンかどうか\n this.player.fullScreen.isFullScreen = (type: string) => {\n return !!(document.fullscreenElement || document.webkitFullscreenElement);\n }\n // フルスクリーンをリクエスト\n this.player.fullScreen.request = (type: string) => {\n\n // すでにフルスクリーンだったらキャンセルする\n if (this.player.fullScreen.isFullScreen()) {\n this.player.fullScreen.cancel();\n return;\n }\n\n // フルスクリーンをリクエスト\n // Safari は webkit のベンダープレフィックスが必要\n fullscreen_container.requestFullscreen = fullscreen_container.requestFullscreen || fullscreen_container.webkitRequestFullscreen;\n if (fullscreen_container.requestFullscreen) {\n fullscreen_container.requestFullscreen();\n }\n\n // 画面の向きを横に固定 (Screen Orientation API がサポートされている場合)\n if (screen.orientation) {\n screen.orientation.lock('landscape').catch(() => {});\n }\n }\n // フルスクリーンをキャンセル\n this.player.fullScreen.cancel = (type: string) => {\n\n // フルスクリーンを終了\n // Safari は webkit のベンダープレフィックスが必要\n document.exitFullscreen = document.exitFullscreen || document.webkitExitFullscreen;\n if (document.exitFullscreen) {\n document.exitFullscreen();\n }\n\n // 画面の向きの固定を解除\n if (screen.orientation) {\n screen.orientation.unlock();\n }\n }\n\n // ***** 再生/停止/画質切り替え時のイベントハンドラー *****\n\n // 再生/停止されたとき\n // 通知バーからの制御など、画面から以外の外的要因で再生/停止が行われる事もある\n const on_play_or_pause = () => {\n\n // まだ設定パネルが表示されていたら非表示にする\n this.player.setting.hide();\n\n // コントロールを表示する\n this.controlDisplayTimer();\n }\n this.player.on('play', on_play_or_pause);\n this.player.on('pause', on_play_or_pause);\n\n // 画質の切り替えが開始されたときのイベント\n this.player.on('quality_start', () => {\n\n // ローディング中の背景画像をランダムで設定\n this.background_url = PlayerUtils.generatePlayerBackgroundURL();\n\n // イベントソースを閉じる\n if (this.eventsource !== null) {\n this.eventsource.close();\n this.eventsource = null;\n }\n\n // 新しい EventSource を作成\n // 画質ごとにイベント API は異なるため、一度破棄してから作り直す\n this.initEventHandler();\n });\n\n // 停止状態でかつ再生時間からバッファが 30 秒以上離れていないかを1分おきに監視し、そうなっていたら強制的にシークする\n // mpegts.js の仕様上、MSE に未再生のバッファがたまり過ぎると SourceBuffer が追加できなくなるため、強制的に接続が切断されてしまう\n this.interval_ids.push(window.setInterval(() => {\n if (this.player.video.paused && this.player.video.buffered.end(0) - this.player.video.currentTime > 30) {\n this.player.sync();\n }\n }, 60 * 1000));\n\n // ***** 文字スーパーのイベントハンドラー *****\n\n // 設定で文字スーパーが有効\n // 字幕が非表示の場合でも、文字スーパーは表示する\n if (Utils.getSettingsItem('tv_show_superimpose') === true) {\n this.player.plugins.aribb24Superimpose.show();\n this.player.on('subtitle_hide', () => {\n this.player.plugins.aribb24Superimpose.show();\n });\n // 設定で文字スーパーが無効\n } else {\n this.player.plugins.aribb24Superimpose.hide();\n this.player.on('subtitle_show', () => {\n this.player.plugins.aribb24Superimpose.hide();\n });\n }\n },\n\n // イベントハンドラーを初期化する\n initEventHandler() {\n\n // ***** プレイヤー再生開始時のイベントハンドラー *****\n\n // 必ず最初はローディング状態とする\n this.is_loading = true;\n\n // 音量を 0 に設定\n this.player.video.volume = 0;\n\n // 再生バッファを調整し、再生準備ができた段階でプレイヤーの背景を非表示にするイベントを登録\n // 実際に再生可能になるのを待ってから実行する\n // 画質切り替え時にも実行する必要があるので、あえてこの位置に記述している\n const on_canplay = () => {\n\n // 自分自身のイベントを登録解除 (重複実行を避ける)\n this.player.video.oncanplay = null;\n this.player.video.oncanplaythrough = null;\n\n // 再生バッファ調整のため、一旦停止させる\n // this.player.video.pause() を使うとプレイヤーの UI アイコンが停止してしまうので、代わりに playbackRate を使う\n this.player.video.playbackRate = 0;\n\n // 念のためさらに少しだけ待ってから\n // あえて await で待たずに非同期コールバックで実行している\n window.setTimeout(async () => {\n\n // 再生バッファを取得する (取得に失敗した場合は 0 を返す)\n const get_playback_buffer_sec = (): number => {\n try {\n return (Math.round((this.player.video.buffered.end(0) - this.player.video.currentTime) * 1000) / 1000);\n } catch (error) {\n // まだ再生準備が整っていないなどの理由で、再生バッファの取得に失敗した場合\n return 0;\n }\n }\n\n // 低遅延モードであれば低遅延向けの再生バッファを、そうでなければ通常の再生バッファをセット (秒単位)\n const playback_buffer_sec = Utils.getSettingsItem('tv_low_latency_mode') ?\n PLAYBACK_BUFFER_SEC_LOW_LATENCY : PLAYBACK_BUFFER_SEC;\n\n // 再生バッファが playback_buffer_sec を超えるまで 0.1 秒おきに再生バッファをチェックする\n // 再生バッファが playback_buffer_sec を切ると再生が途切れやすくなるので (特に動きの激しい映像)、\n // 再生開始までの時間を若干犠牲にして、再生バッファの調整と同期に時間を割く\n // playback_buffer_sec の値は mpegts.js に渡す liveSyncTargetLatency プロパティに渡す値と共通\n let current_playback_buffer_sec = get_playback_buffer_sec();\n while (current_playback_buffer_sec < playback_buffer_sec) {\n await Utils.sleep(0.1);\n current_playback_buffer_sec = get_playback_buffer_sec();\n }\n\n // 再生開始\n this.player.video.playbackRate = 1;\n\n // 再生が一時的に止まってバッファリングしているとき/再び再生されはじめたときのイベント\n // バッファリングの Progress Circular の表示を制御する\n // 同期が終わってからの方が都合が良い\n this.player.video.addEventListener('waiting', () => this.is_video_buffering = true);\n this.player.video.addEventListener('playing', () => this.is_video_buffering = false);\n\n // ローディング状態を解除し、映像を表示する\n this.is_loading = false;\n\n // バッファリング中の Progress Circular を非表示にする\n this.is_video_buffering = false;\n\n if (this.channel.is_radiochannel) {\n // ラジオチャンネルでは引き続き映像の代わりとして背景画像を表示し続ける\n this.is_background_display = true;\n } else {\n // 背景画像をフェードアウト\n this.is_background_display = false;\n }\n\n // 再生開始時に音量を徐々に上げる\n // いきなり再生されるよりも体験が良い\n const current_volume: number = this.player.user.get('volume');\n while ((this.player.video.volume + 0.05) < current_volume) {\n // 小数第2位以下を切り捨てて、浮動小数の誤差で 1 (100%) を微妙に超えてしまいエラーになるのを避ける\n this.player.video.volume = Utils.mathFloor(this.player.video.volume + 0.05, 2);\n await Utils.sleep(0.02);\n }\n this.player.video.volume = current_volume;\n\n }, 100);\n }\n this.player.video.oncanplay = on_canplay;\n this.player.video.oncanplaythrough = on_canplay;\n\n // ***** KonomiTV サーバーのイベント API のイベントハンドラー *****\n\n // EventSource を作成\n this.eventsource = new EventSource((this.player.quality.url as string).replace('/mpegts', '/events'));\n\n // 初回接続時のイベント\n this.eventsource.addEventListener('initial_update', (event_raw: MessageEvent) => {\n\n // ステータスが Standby であれば\n const event = JSON.parse(event_raw.data);\n if (event.status === 'Standby') {\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n }\n });\n\n // ステータスが更新されたときのイベント\n this.eventsource.addEventListener('status_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n console.log(`Status: ${event.status} / Detail: ${event.detail}`);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n\n // ステータスごとに処理を振り分け\n switch (event.status) {\n\n // Status: Standby\n case 'Standby': {\n\n // ステータス詳細をプレイヤーに表示\n if (!this.player.template.notice.textContent.includes('画質を')) { // 画質切り替えの表示を上書きしない\n this.player.notice(event.detail, -1);\n }\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n break;\n }\n\n // Status: ONAir\n case 'ONAir': {\n\n // ステータス詳細をプレイヤーから削除\n if (!this.player.template.notice.textContent.includes('画質を')) { // 画質切り替えの表示を上書きしない\n this.player.notice(this.player.template.notice.textContent, 0.000001);\n }\n\n // 前のプレイヤーインスタンスの Picture-in-Picture ウインドウが残っている場合、終了させてからもう一度切り替える\n // チャンネル切り替えが完了しても前の Picture-in-Picture ウインドウは再利用されないため、一旦終了させるしかない\n if (document.pictureInPictureElement) {\n document.exitPictureInPicture();\n this.player.video.requestPictureInPicture();\n }\n break;\n }\n\n // Status: Restart\n case 'Restart': {\n\n // ステータス詳細をプレイヤーに表示\n this.player.notice(event.detail, -1);\n\n // プレイヤーを再起動する\n this.player.switchVideo({\n url: this.player.quality.url,\n type: this.player.quality.type,\n });\n\n // 再起動しただけでは自動再生されないので、明示的に\n this.player.play();\n\n // バッファリング中の Progress Circular を表示\n this.is_video_buffering = true;\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n break;\n }\n\n // Status: Offline\n case 'Offline': {\n\n // 基本的に Offline は放送休止中やエラーなどで復帰の見込みがない状態\n\n // ステータス詳細をプレイヤーに表示\n // 動画の読み込みエラーが送出された時にメッセージを上書きする\n this.player.notice(event.detail, -1);\n this.player.video.onerror = () => {\n this.player.notice(event.detail, -1);\n this.player.video.onerror = null;\n }\n\n // 描画されたコメントをクリア\n this.player.danmaku.clear()\n\n // 動画を停止する\n this.player.video.pause();\n\n // イベントソースを閉じる(復帰の見込みがないため)\n this.eventsource.close();\n\n // プレイヤーの背景を表示する\n this.is_background_display = true;\n\n // バッファリング中の Progress Circular を非表示にする\n this.is_loading = false;\n this.is_video_buffering = false;\n break;\n }\n }\n });\n\n // ステータス詳細が更新されたときのイベント\n this.eventsource.addEventListener('detail_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n console.log(`Status: ${event.status} Detail:${event.detail}`);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n\n // Standby のときだけプレイヤーに表示\n if (event.status === 'Standby') {\n this.player.notice(event.detail, -1);\n\n // プレイヤーの背景を表示する\n if (!this.is_background_display) {\n this.is_background_display = true;\n }\n }\n });\n\n // クライアント数(だけ)が更新されたときのイベント\n this.eventsource.addEventListener('clients_update', (event_raw: MessageEvent) => {\n\n // イベントを取得\n const event = JSON.parse(event_raw.data);\n\n // 視聴者数を更新\n this.channel.viewers = event.clients_count;\n });\n },\n\n // ショートカットキーを初期化する\n initShortcutKeyHandler() {\n\n const twitter_component = (this.$refs.Twitter as InstanceType);\n const tweet_form_element = twitter_component.$el.querySelector('.tweet-form__textarea');\n\n // IME 変換中の状態を保存する\n for (const element of document.querySelectorAll('input[type=text],input[type=search],textarea')) {\n element.addEventListener('compositionstart', () => this.is_ime_composing = true);\n element.addEventListener('compositionend', () => this.is_ime_composing = false);\n }\n\n // ショートカットキーハンドラー\n this.shortcut_key_handler = async (event: KeyboardEvent) => {\n\n const tag = document.activeElement.tagName.toUpperCase();\n const editable = document.activeElement.getAttribute('contenteditable');\n\n // 矢印キーのデフォルトの挙動(スクロール)を抑制\n // キーリピート周りで間引かれるイベントでも event.preventDefault() しないとスクロールしてしまうため、\n // 一番最初のタイミングでやっておく\n // input・textarea・contenteditable 状態の要素では実行しない\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code) &&\n (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true')) {\n event.preventDefault();\n }\n\n // キーリピート(押しっぱなし)状態の場合は基本実行しない\n // 押し続けると何度も同じ動作が実行されて大変な事になる…\n // ただ、キーリピートを使いたい場合もあるので、リピート状態をフラグとして保存する\n let is_repeat = false;\n if (event.repeat) is_repeat = true;\n\n // キーリピート状態は event.repeat を見る事でだいたい検知できるが、最初の何回かは検知できないこともある\n // そこで、0.05 秒以内に連続して発火したキーイベントは間引きも兼ねて実行しない\n const now = Date.now();\n if (now - this.shortcut_key_pressed_at < (0.05 * 1000)) return;\n this.shortcut_key_pressed_at = now; // 最終押下時刻を更新\n\n // 無名関数の中で実行する\n const result = await (async (): Promise => {\n\n // ***** ツイート入力フォームにフォーカスを当てる/フォーカスを外す *****\n\n // ツイート入力フォームにフォーカスしているときもこのショートカットが動くようにする\n // 以降の if 文で textarea フォーカス時のイベントをすべて弾いてしまっているため、前に持ってきている\n // Tab キーに割り当てている関係で、IME 変換中は実行しない(IME 変換中に実行すると文字変換ができなくなる)\n if (((tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') ||\n (document.activeElement === tweet_form_element)) && this.is_ime_composing === false) {\n if (event.code === 'Tab') {\n\n // ツイート入力フォームにフォーカスがすでに当たっていたら、フォーカスを外して終了\n if (document.activeElement === tweet_form_element) {\n tweet_form_element.blur();\n return true;\n }\n\n // パネルを開く\n this.is_panel_display = true;\n\n // どのタブを開いていたかに関係なく Twitter タブに切り替える\n this.tv_panel_active_tab = 'Twitter';\n\n // ツイート入力フォームの textarea 要素にフォーカスを当てる\n tweet_form_element.focus();\n\n // フォーカスを当てると勝手に横方向にスクロールされてしまうので、元に戻す\n this.$el.scrollLeft = 0;\n\n window.setTimeout(() => {\n\n // 他のタブから切り替えると一発でフォーカスが当たらないことがあるので、ちょっとだけ待ってから念押し\n // $nextTick() だと上手くいかなかった…\n tweet_form_element.focus();\n\n // フォーカスを当てると勝手に横方向にスクロールされてしまうので、元に戻す\n this.$el.scrollLeft = 0;\n\n }, 100); // 0.1秒\n\n return true;\n }\n }\n\n // ***** ツイートを送信する *****\n\n // ツイート入力フォームにフォーカスしているときもこのショートカットが動くようにする\n // Twitter タブ以外を開いているときは実行しない\n // 以降の if 文で textarea フォーカス時のイベントをすべて弾いてしまっているため、前に持ってきている\n if (((tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') ||\n (document.activeElement === tweet_form_element)) &&\n this.tv_panel_active_tab === 'Twitter' &&\n this.is_ime_composing === false) {\n // (Ctrl or Cmd or Shift) + Enter\n // Shift + Enter は隠し機能(間違えたとき用)\n if ((event.ctrlKey || event.metaKey || event.shiftKey) && event.code === 'Enter') {\n twitter_component.$el.querySelector('.tweet-button').click();\n return true;\n }\n }\n\n // ***** コメント入力フォームを閉じる *****\n\n // プレイヤーが初期化されていない時・Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.shiftKey && !event.altKey) {\n\n // コメント入力フォームが表示されているときのみ\n if (this.player.template.controller.classList.contains('dplayer-controller-comment')) {\n // Ctrl or Cmd + M\n if ((event.ctrlKey || event.metaKey) && event.code === 'KeyM') {\n this.player.comment.hide();\n return true;\n }\n }\n }\n\n // input・textarea・contenteditable 状態の要素でなければ\n // 文字入力中にショートカットキーが作動してしまわないように\n if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') {\n\n // キーリピートでない時・Ctrl / Cmd / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.altKey) {\n\n // ***** 数字キーでチャンネルを切り替える *****\n\n // Ctrl / Cmd キーが同時押しされていたら BS チャンネルの方を選局する\n const switch_channel_type = (event.shiftKey) ? 'BS' : 'GR';\n\n // 1~9キー\n let switch_remocon_id = null;\n if (event.code === 'Digit1' || event.code === 'Digit2' || event.code === 'Digit3' ||\n event.code === 'Digit4' || event.code === 'Digit5' || event.code === 'Digit6' ||\n event.code === 'Digit7' || event.code === 'Digit8' || event.code === 'Digit9') {\n switch_remocon_id = Number(event.code.replace('Digit', ''));\n }\n // 0キー: 10に割り当て\n if (event.code === 'Digit0') switch_remocon_id = 10;\n // -キー: 11に割り当て\n if (event.code === 'Minus') switch_remocon_id = 11;\n // ^キー: 12に割り当て\n if (event.code === 'Equal') switch_remocon_id = 12;\n // 1~9キー (テンキー)\n if (event.code === 'Numpad1' || event.code === 'Numpad2' || event.code === 'Numpad3' ||\n event.code === 'Numpad4' || event.code === 'Numpad5' || event.code === 'Numpad6' ||\n event.code === 'Numpad7' || event.code === 'Numpad8' || event.code === 'Numpad9') {\n switch_remocon_id = Number(event.code.replace('Numpad', ''));\n }\n // 0キー (テンキー): 10に割り当て\n if (event.code === 'Numpad0') switch_remocon_id = 10;\n\n // この時点でリモコン番号が取得できていたら実行\n if (switch_remocon_id !== null) {\n\n // 切り替え先のチャンネルを取得する\n const switch_channel = ChannelUtils.getChannelFromRemoconID(\n this.channels_list, switch_channel_type, switch_remocon_id);\n\n // チャンネルが取得できていれば、ルーティングをそのチャンネルに置き換える\n // 押されたキーに対応するリモコン番号のチャンネルがない場合や、現在と同じチャンネル ID の場合は何も起こらない\n if (switch_channel !== null && switch_channel.channel_id !== this.channel_id) {\n await this.$router.push({path: `/tv/watch/${switch_channel.channel_id}`});\n return true;\n }\n }\n }\n\n // キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // ***** キーボードショートカットの一覧を表示する *****\n\n // /(?)キー: キーボードショートカットの一覧を表示する\n if (event.code === 'Slash') {\n this.shortcut_key_modal = !this.shortcut_key_modal;\n return true;\n }\n\n // ***** パネルのタブを切り替える *****\n\n // Pキー: パネルの表示切り替え\n if (event.code === 'KeyP') {\n this.is_panel_display = !this.is_panel_display;\n return true;\n }\n // Kキー: 番組情報タブ\n if (event.code === 'KeyK') {\n this.tv_panel_active_tab = 'Program';\n return true;\n }\n // Lキー: チャンネルタブ\n if (event.code === 'KeyL') {\n this.tv_panel_active_tab = 'Channel';\n return true;\n }\n // ;(+)キー: コメントタブ\n if (event.code === 'Semicolon') {\n this.tv_panel_active_tab = 'Comment';\n return true;\n }\n // :(*)キー: Twitterタブ\n if (event.code === 'Quote') {\n this.tv_panel_active_tab = 'Twitter';\n return true;\n }\n\n // ***** Twitter タブ内のタブを切り替える *****\n\n // [(「): ツイート検索タブ\n if (event.code === 'BracketRight') {\n twitter_component.twitter_active_tab = 'Search';\n return true;\n }\n // ](」): タイムラインタブ\n if (event.code === 'Backslash') {\n twitter_component.twitter_active_tab = 'Timeline';\n return true;\n }\n // \\(¥)キー: キャプチャタブ\n if (event.code === 'IntlRo') {\n twitter_component.twitter_active_tab = 'Capture';\n return true;\n }\n }\n\n // Twitter タブ内のキャプチャタブが表示されている & Ctrl / Cmd / Shift / Alt のいずれも押されていないときだけ\n // キャプチャタブが表示されている時は、プレイヤー操作側の矢印キー/スペースキーのショートカットは動作しない(キーが重複するため)\n if (this.tv_panel_active_tab === 'Twitter' && twitter_component.twitter_active_tab === 'Capture' &&\n (!event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey)) {\n\n // ***** キャプチャにフォーカスする *****\n\n if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.code)) {\n\n // キャプチャリストに一枚もキャプチャがない\n if (twitter_component.captures.length === 0) return false;\n\n // まだどのキャプチャにもフォーカスされていない場合は、一番新しいキャプチャにフォーカスして終了\n if (twitter_component.captures.some(capture => capture.focused === true) === false) {\n twitter_component.captures[twitter_component.captures.length - 1].focused = true;\n return true;\n }\n\n // 現在フォーカスされているキャプチャのインデックスを取得\n const focused_capture_index = twitter_component.captures.findIndex(capture => capture.focused === true);\n\n // ↑キー: 2つ前のキャプチャにフォーカスする\n // キャプチャリストは2列で並んでいるので、2つ後のキャプチャが現在フォーカスされているキャプチャの直上になる\n if (event.code === 'ArrowUp') {\n // 2つ前のキャプチャがないなら実行しない\n if (focused_capture_index - 2 < 0) return false;\n twitter_component.captures[focused_capture_index - 2].focused = true;\n }\n\n // ↓キー: 2つ後のキャプチャにフォーカスする\n // キャプチャリストは2列で並んでいるので、2つ後のキャプチャが現在フォーカスされているキャプチャの直下になる\n if (event.code === 'ArrowDown') {\n // 2つ後のキャプチャがないなら実行しない\n if (focused_capture_index + 2 > (twitter_component.captures.length - 1)) return false;\n twitter_component.captures[focused_capture_index + 2].focused = true;\n }\n\n // ←キー: 1つ前のキャプチャにフォーカスする\n if (event.code === 'ArrowLeft') {\n // 1つ前のキャプチャがないなら実行しない\n if (focused_capture_index - 1 < 0) return false;\n twitter_component.captures[focused_capture_index - 1].focused = true;\n }\n\n // ←キー: 1つ後のキャプチャにフォーカスする\n if (event.code === 'ArrowRight') {\n // 1つ後のキャプチャがないなら実行しない\n if (focused_capture_index + 1 > (twitter_component.captures.length - 1)) return false;\n twitter_component.captures[focused_capture_index + 1].focused = true;\n }\n\n // 現在フォーカスされているキャプチャのフォーカスを外す\n twitter_component.captures[focused_capture_index].focused = false;\n\n // 拡大表示のモーダルが開かれている場合は、フォーカスしたキャプチャをモーダルにセット\n // こうすることで、QuickLook みたいな挙動になる\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (twitter_component.zoom_capture_modal === true) {\n twitter_component.zoom_capture = focused_capture;\n }\n\n // 現在フォーカスされているキャプチャが見える位置までスクロール\n // block: 'nearest' の指定で、上下どちらにスクロールしてもフォーカスされているキャプチャが常に表示されるようになる\n const focused_capture_element =\n twitter_component.$el.querySelector(`img[src=\"${focused_capture.image_url}\"]`).parentElement;\n if (is_repeat) {\n // キーリピート状態ではスムーズスクロールがフォーカスの移動に追いつけずスクロールの挙動がおかしくなるため、\n // スムーズスクロールは無効にしてある\n focused_capture_element.scrollIntoView({block: 'nearest', inline: 'nearest', behavior: 'auto'});\n } else {\n focused_capture_element.scrollIntoView({block: 'nearest', inline: 'nearest', behavior: 'smooth'});\n }\n return true;\n }\n\n // ***** キャプチャを拡大表示する/拡大表示を閉じる *****\n\n if (event.code === 'Enter') {\n\n // Enter キーの押下がプレイヤー側のコメント送信由来のイベントの場合は実行しない\n if (this.is_comment_send_just_did) return false;\n\n // すでにモーダルが開かれている場合は、どのキャプチャが拡大表示されているかに関わらず閉じる\n if (twitter_component.zoom_capture_modal === true) {\n twitter_component.zoom_capture_modal = false;\n return true;\n }\n\n // 現在フォーカスされているキャプチャを取得\n // まだどのキャプチャにもフォーカスされていない場合は実行しない\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (focused_capture === undefined) return false;\n\n // モーダルを開き、モーダルで拡大表示するキャプチャとしてセット\n twitter_component.zoom_capture = focused_capture;\n twitter_component.zoom_capture_modal = true;\n return true;\n }\n\n // ***** キャプチャを選択する/選択を解除する *****\n\n if (event.code === 'Space') {\n\n // 現在フォーカスされているキャプチャを取得\n // まだどのキャプチャにもフォーカスされていない場合は実行しない\n const focused_capture = twitter_component.captures.find(capture => capture.focused === true);\n if (focused_capture === undefined) return false;\n\n // 「キャプチャリスト内のキャプチャがクリックされたときのイベント」を呼ぶ\n // 選択されていなければ選択され、選択されていれば選択が解除される\n // キャプチャの枚数制限などはすべて clickCapture() の中で処理される\n twitter_component.clickCapture(focused_capture);\n return true;\n }\n }\n\n // ***** 上下キーでチャンネルを切り替える *****\n\n // キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // ↑キー: 前のチャンネルに切り替え\n if (event.code === 'ArrowUp') {\n await this.$router.push({path: `/tv/watch/${this.channel_previous.channel_id}`});\n return true;\n }\n // ↓キー: 次のチャンネルに切り替え\n if (event.code === 'ArrowDown') {\n await this.$router.push({path: `/tv/watch/${this.channel_next.channel_id}`});\n return true;\n }\n }\n\n // ***** プレイヤーのショートカットキー *****\n\n // プレイヤーが初期化されていない時・Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.shiftKey && !event.altKey) {\n\n // Ctrl / Cmd + ↑キー: プレイヤーの音量を上げる\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowUp') {\n this.player.volume(this.player.volume() + 0.05);\n return true;\n }\n // Ctrl / Cmd + ↓キー: プレイヤーの音量を下げる\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowDown') {\n this.player.volume(this.player.volume() - 0.05);\n return true;\n }\n // Ctrl / Cmd + ←キー: 停止して0.5秒巻き戻し\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowLeft') {\n if (this.player.video.paused === false) this.player.video.pause();\n this.player.video.currentTime = this.player.video.currentTime - 0.5;\n return true;\n }\n // Ctrl / Cmd + →キー: 停止して0.5秒早送り\n if ((event.ctrlKey || event.metaKey) && event.code === 'ArrowRight') {\n if (this.player.video.paused === false) this.player.video.pause();\n this.player.video.currentTime = this.player.video.currentTime + 0.5;\n return true;\n }\n }\n\n // プレイヤーが初期化されていない時・Ctrl / Cmd / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && !event.ctrlKey && !event.metaKey && !event.altKey) {\n\n // Shift + Spaceキー + キーリピートでない時 + Twitter タブ表示時 + キャプチャタブ表示時: 再生/停止\n if (event.shiftKey === true && event.code === 'Space' && is_repeat === false &&\n this.tv_panel_active_tab === 'Twitter' && twitter_component.twitter_active_tab === 'Capture') {\n this.player.toggle();\n return true;\n }\n }\n\n // プレイヤーが初期化されていない時・キーリピートでない時・Ctrl / Cmd / Shift / Alt キーが一緒に押された時に作動しないように\n if (this.player !== null && is_repeat === false && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) {\n\n // Spaceキー: 再生/停止\n if (event.code === 'Space') {\n this.player.toggle();\n return true;\n }\n // Fキー: フルスクリーンの切り替え\n if (event.code === 'KeyF') {\n this.player.fullScreen.toggle();\n return true;\n }\n // Wキー: ライブストリームの同期\n if (event.code === 'KeyW') {\n this.player.sync();\n return true;\n }\n // Eキー: Picture-in-Picture の表示切り替え\n if (event.code === 'KeyE') {\n if (document.pictureInPictureEnabled) {\n this.player.template.pipButton.click();\n }\n return true;\n }\n // Sキー: 字幕の表示切り替え\n if (event.code === 'KeyS') {\n this.player.subtitle.toggle();\n if (!this.player.subtitle.container.classList.contains('dplayer-subtitle-hide')) {\n this.player.notice(`${this.player.tran('Show subtitle')}`);\n } else {\n this.player.notice(`${this.player.tran('Hide subtitle')}`);\n }\n return true;\n }\n // Dキー: コメントの表示切り替え\n if (event.code === 'KeyD') {\n this.player.template.showDanmaku.click();\n if (this.player.template.showDanmakuToggle.checked) {\n this.player.notice(`${this.player.tran('Show comment')}`);\n } else {\n this.player.notice(`${this.player.tran('Hide comment')}`);\n }\n return true;\n }\n // Cキー: 映像をキャプチャ\n if (event.code === 'KeyC') {\n await this.capture_handler.captureAndSave(this.channel, false);\n return true;\n }\n // Vキー: 映像を実況コメントを付けてキャプチャ\n if (event.code === 'KeyV') {\n await this.capture_handler.captureAndSave(this.channel, true);\n return true;\n }\n // Mキー: コメント入力フォームにフォーカス\n if (event.code === 'KeyM') {\n this.player.controller.show();\n this.player.comment.show();\n this.controlDisplayTimer();\n window.setTimeout(() => this.player.template.commentInput.focus(), 100);\n return true;\n }\n }\n }\n return false;\n })();\n\n // 無名関数を実行した後の戻り値が true ならショートカットキーの操作を実行したことになるので、デフォルトのキー操作を封じる\n if (result === true) {\n event.preventDefault();\n }\n };\n\n // ページ上でキーが押されたときのイベントを登録\n document.addEventListener('keydown', this.shortcut_key_handler);\n },\n\n // キャプチャ関連のイベントを初期化する\n initCaptureHandler() {\n\n // キャプチャハンドラーを初期化\n this.capture_handler = new PlayerCaptureHandler(this.player, (blob: Blob, filename: string) => {\n // キャプチャが撮られたら、随時 Twitter タブのキャプチャリストに追加する\n (this.$refs.Twitter as InstanceType).addCaptureList(blob, filename);\n });\n\n // キャプチャボタンがクリックされたときのイベント\n // ショートカットからのキャプチャでも同じイベントがトリガーされる\n const capture_button = this.$el.querySelector('.dplayer-icon.dplayer-capture-icon');\n capture_button.addEventListener('click', async () => {\n await this.capture_handler.captureAndSave(this.channel, false);\n });\n\n // コメント付きキャプチャボタンがクリックされたときのイベント\n // ショートカットからのキャプチャでも同じイベントがトリガーされる\n const comment_capture_button = this.$el.querySelector('.dplayer-icon.dplayer-comment-capture-icon');\n comment_capture_button.addEventListener('click', async () => {\n await this.capture_handler.captureAndSave(this.channel, true);\n });\n },\n\n\n // 再生セッションを破棄する\n // チャンネルを切り替える際に実行される\n destroy(is_destroy_player = false) {\n\n // clearInterval() ですべての setInterval(), setTimeout() の実行を止める\n // clearInterval() と clearTimeout() は中身共通なので問題ない\n for (const interval_id of this.interval_ids) {\n window.clearInterval(interval_id);\n }\n\n // コントロール表示制御用タイマーを止める\n window.clearTimeout(this.control_interval_id);\n\n // interval_ids をクリア\n this.interval_ids = [];\n\n // 再びローディング状態にする\n this.is_loading = true;\n\n // プレイヤーの背景を隠す\n this.is_background_display = false;\n\n // プレイヤーに破棄が可能なフラグをつける\n this.player.KonomiTVCanDestroy = true;\n\n // イベントソースを閉じる\n if (this.eventsource !== null) {\n this.eventsource.close();\n this.eventsource = null;\n }\n\n // アニメーション分待ってから実行\n this.interval_ids.push(window.setTimeout(() => {\n\n // プレイヤーを停止する\n this.player.video.pause();\n\n // is_destroy_player が true の時は、ここで DPlayer 自体を破棄する\n // false の時は次の initPlayer() が実行されるまで破棄されない\n if (is_destroy_player === true && this.player !== null) {\n try {\n this.player.destroy();\n } catch (error) {\n // mpegts.js をうまく破棄できない場合\n if (this.player.plugins.mpegts !== undefined) {\n this.player.plugins.mpegts.destroy();\n }\n }\n this.player = null;\n }\n\n }, 0.4 * 1000)); // 0.4 秒\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Watch.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Watch.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Watch.vue?vue&type=template&id=49a8f767&scoped=true&\"\nimport script from \"./Watch.vue?vue&type=script&lang=ts&\"\nexport * from \"./Watch.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Watch.vue?vue&type=style&index=0&id=49a8f767&prod&lang=scss&\"\nimport style1 from \"./Watch.vue?vue&type=style&index=1&id=49a8f767&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"49a8f767\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('v-card',{staticClass:\"settings-container d-flex px-5 py-5 mx-auto background\",attrs:{\"elevation\":\"0\",\"width\":\"100%\",\"max-width\":\"1000\"}},[_c('v-navigation-drawer',{staticClass:\"settings-navigation flex-shrink-0 background\",attrs:{\"permanent\":\"\",\"width\":\"100%\",\"height\":\"auto\"}},[_c('v-list-item',{staticClass:\"px-4\"},[_c('v-list-item-content',[_c('h1',[_vm._v(\"設定\")])])],1),_c('v-list',{staticClass:\"mt-2 px-0\",attrs:{\"nav\":\"\"}},[_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/general\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 3px\"},attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"全般\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/account\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"アカウント\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/jikkyo\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 2px\"},attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"ニコニコ実況\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/twitter\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 1px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"Twitter\")])],1)],1),_c('v-divider'),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/environment\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"環境設定\")])],1)],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\nexport default Vue.extend({\n name: 'Settings-Index',\n components: {\n Header,\n Navigation,\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Index.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Index.vue?vue&type=template&id=a67629d4&scoped=true&\"\nimport script from \"./Index.vue?vue&type=script&lang=ts&\"\nexport * from \"./Index.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Index.vue?vue&type=style&index=0&id=a67629d4&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"a67629d4\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"19px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"全般\")])],1),_c('div',{staticClass:\"settings__content\"},[_c('div',{staticClass:\"settings__item settings__item--sync-disabled\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"テレビのストリーミング画質\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" テレビをライブストリーミングするときの既定の画質を設定します。\"),_c('br'),_vm._v(\" ストリーミング画質はプレイヤーの設定からいつでも切り替えられます。\"),_c('br')]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" [1080p (60fps)] は、通常 30fps (60i) の映像を補間することで、ほかの画質よりも滑らか(ぬるぬる)な映像で再生できます。ただし、再生負荷が少し高くなります。\"),_c('br'),_vm._v(\" [1080p (60fps)] で視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーでは CPU 使用率が高くなり、再生に支障が出ることがあります。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tv_streaming_quality},model:{value:(_vm.settings.tv_streaming_quality),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_streaming_quality\", $$v)},expression:\"settings.tv_streaming_quality\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\",class:{'settings__item--disabled': _vm.PlayerUtils.isHEVCVideoSupported() === false}},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_data_saver_mode\"}},[_vm._v(\"テレビを通信節約モードで視聴する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_data_saver_mode\"}},[_vm._v(\" テレビをライブストリーミングするときに、通信節約モードで視聴するかを設定します。\"),_c('br'),_vm._v(\" 通信節約モードでは、H.265 / HEVC という圧縮率の高いコーデックを使い、画質はほぼそのまま、通信量を通常の 2/3 程度に抑えながら視聴できます。ただし、再生負荷が高くなります。\"),_c('br'),_vm._v(\" 通信節約モードで視聴するときは、QSVEncC / NVEncC / VCEEncC エンコーダーの利用をおすすめします。FFmpeg エンコーダーではまともに再生できない可能性が高いです。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_data_saver_mode\",\"inset\":\"\",\"hide-details\":\"\",\"disabled\":_vm.PlayerUtils.isHEVCVideoSupported() === false},model:{value:(_vm.settings.tv_data_saver_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_data_saver_mode\", $$v)},expression:\"settings.tv_data_saver_mode\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_low_latency_mode\"}},[_vm._v(\"テレビを低遅延で視聴する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_low_latency_mode\"}},[_vm._v(\" テレビをライブストリーミングするときに、低遅延で視聴するかを設定します。\"),_c('br'),_vm._v(\" 低遅延ストリーミングがオンのときは、放送波との遅延を最短 1.9 秒に抑えて視聴できます。\"),_c('br'),_vm._v(\" また、約 3 秒以上遅延したときに少しだけ再生速度を早める (1.1x) ことで、滑らかにストリーミングの遅れを取り戻します。\"),_c('br'),_vm._v(\" 宅外視聴などのネットワークが不安定になりがちな環境では、一度低遅延ストリーミングをオフにしてみると、映像のカクつきを改善できるかもしれません。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_low_latency_mode\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.tv_low_latency_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_low_latency_mode\", $$v)},expression:\"settings.tv_low_latency_mode\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"tv_show_superimpose\"}},[_vm._v(\"テレビをみるときに文字スーパーを表示する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"tv_show_superimpose\"}},[_vm._v(\" テレビをライブストリーミングするときに、文字スーパーを表示するかを設定します。\"),_c('br'),_vm._v(\" 文字スーパーは、緊急地震速報の赤テロップや、NHK BS のニュース速報のテロップなどで利用されています。とくに理由がなければ、オンのままにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"tv_show_superimpose\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.tv_show_superimpose),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_show_superimpose\", $$v)},expression:\"settings.tv_show_superimpose\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"既定のパネルの表示状態\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 視聴画面を開いたときに、右側のパネルをどう表示するかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.panel_display_state},model:{value:(_vm.settings.panel_display_state),callback:function ($$v) {_vm.$set(_vm.settings, \"panel_display_state\", $$v)},expression:\"settings.panel_display_state\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"テレビをみるときに既定で表示されるパネルのタブ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" テレビの視聴画面を開いたときに、右側のパネルで最初に表示されるタブを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tv_panel_active_tab},model:{value:(_vm.settings.tv_panel_active_tab),callback:function ($$v) {_vm.$set(_vm.settings, \"tv_panel_active_tab\", $$v)},expression:\"settings.tv_panel_active_tab\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('label',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕のフォント\")]),_c('label',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーで字幕表示をオンにしているときの、字幕のフォントを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.caption_font},model:{value:(_vm.settings.caption_font),callback:function ($$v) {_vm.$set(_vm.settings, \"caption_font\", $$v)},expression:\"settings.caption_font\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"always_border_caption_text\"}},[_vm._v(\"字幕の文字を常に縁取って描画する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"always_border_caption_text\"}},[_vm._v(\" プレイヤーで字幕表示をオンにしているときに、字幕の文字を常に縁取って描画するかを設定します。\"),_c('br'),_vm._v(\" 字幕は縁取られていた方が視認性が良く、見た目的にもきれいです。とくに理由がなければ、オンのままにしておくことをおすすめします。\"),_c('br'),_vm._v(\" この設定をオフにしているときも、字幕データ側で明示的に縁取りするように指定されていれば、オンにしているとき同様に文字が縁取られて描画されます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"always_border_caption_text\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.always_border_caption_text),callback:function ($$v) {_vm.$set(_vm.settings, \"always_border_caption_text\", $$v)},expression:\"settings.always_border_caption_text\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"specify_caption_background_color\"}},[_vm._v(\"字幕の背景色を指定する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"specify_caption_background_color\"}},[_vm._v(\" プレイヤーで字幕表示をオンにしているときに、字幕の背景色を明示的に指定するかを設定します。\"),_c('br'),_vm._v(\" この設定をオフにしているときは、字幕データ側で指定されている背景色で描画します。とくに理由がなければ、オフのままにしておくことをおすすめします。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"specify_caption_background_color\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.specify_caption_background_color),callback:function ($$v) {_vm.$set(_vm.settings, \"specify_caption_background_color\", $$v)},expression:\"settings.specify_caption_background_color\"}})],1),_c('div',{staticClass:\"settings__item\",class:{'settings__item--disabled': _vm.settings.specify_caption_background_color === false}},[_c('label',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕の背景色\")]),_c('label',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーで字幕表示をオンにしているときの、字幕の背景色を設定します。\"),_c('br'),_vm._v(\" 上の [字幕の背景色を指定する] をオンにしているときのみ有効です。透明度 (アルファチャンネル) を 0 に設定すれば、字幕の背景を非表示にできます。\"),_c('br')]),_c('div',{ref:\"caption_background_color\",staticClass:\"settings__item-label\"},[_c('v-color-picker',{staticClass:\"settings__item-form\",attrs:{\"hide-details\":\"\",\"flat\":true,\"show-alpha\":true,\"show-swatches\":false,\"hide-inputs\":false,\"width\":690,\"canvas-height\":80,\"disabled\":_vm.settings.specify_caption_background_color === false},model:{value:(_vm.settings.caption_background_color),callback:function ($$v) {_vm.$set(_vm.settings, \"caption_background_color\", $$v)},expression:\"settings.caption_background_color\"}})],1)]),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item settings__item--switch settings__item--sync-disabled\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"capture_copy_to_clipboard\"}},[_vm._v(\"キャプチャをクリップボードにコピーする\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"capture_copy_to_clipboard\"}},[_vm._v(\" プレイヤーでキャプチャを撮ったときに、撮ったキャプチャをクリップボードにもコピーするかを設定します。\"),_c('br'),_vm._v(\" クリップボードの履歴をサポートしていない OS では、この設定をオンにした状態でキャプチャを撮ると、以前のクリップボードが上書きされます。注意してください。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"capture_copy_to_clipboard\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.capture_copy_to_clipboard),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_copy_to_clipboard\", $$v)},expression:\"settings.capture_copy_to_clipboard\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"キャプチャの保存先\")]),_c('div',{staticClass:\"settings__item-label\"},[_c('p',[_vm._v(\" キャプチャした画像をブラウザでダウンロードするか、KonomiTV サーバーにアップロードするかを設定します。\"),_c('br'),_vm._v(\" ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方同時に行うこともできます。\"),_c('br')]),_c('p',[_vm._v(\" ブラウザでダウンロードすると、視聴中のデバイスのダウンロードフォルダに保存されます。\"),_c('br'),_vm._v(\" 視聴中のデバイスにそのまま保存されるためシンプルですが、保存先のフォルダを変更できないこと、PC 版 Chrome では毎回ダウンロードバーが表示されてしまうことがデメリットです。\"),_c('br')]),_c('p',[_vm._v(\" KonomiTV サーバーにアップロードすると、環境設定で指定されたキャプチャ保存フォルダに保存されます。視聴したデバイスにかかわらず、今までに撮ったキャプチャをひとつのフォルダにまとめて保存できます。\"),_c('br'),_vm._v(\" 他のデバイスでキャプチャを見るにはキャプチャ保存フォルダをネットワークに共有する必要があること、スマホ・タブレットではネットワーク上のフォルダへのアクセスがやや面倒なことがデメリットです。\"),_c('br')])]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.capture_save_mode},model:{value:(_vm.settings.capture_save_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_save_mode\", $$v)},expression:\"settings.capture_save_mode\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"字幕が表示されているときのキャプチャの保存モード\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 字幕が表示されているときに、キャプチャした画像に字幕を合成するかを設定します。\"),_c('br'),_vm._v(\" 映像のみのキャプチャと、字幕を合成したキャプチャを両方同時に保存することもできます。\"),_c('br'),_vm._v(\" なお、字幕が表示されていない場合は、常に映像のみ (+コメント付きキャプチャではコメントを合成して) 保存されます。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.capture_caption_mode},model:{value:(_vm.settings.capture_caption_mode),callback:function ($$v) {_vm.$set(_vm.settings, \"capture_caption_mode\", $$v)},expression:\"settings.capture_caption_mode\"}})],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"設定をエクスポート\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" このデバイス(ブラウザ)に保存されている設定データをエクスポート(ダウンロード)できます。\"),_c('br'),_vm._v(\" ダウンロードした設定データ (KonomiTV-Settings.json) は、[設定をインポート] からインポートできます。異なるサーバーの KonomiTV を同じ設定で使いたいときなどに使ってください。\"),_c('br')])]),_c('v-btn',{staticClass:\"settings__save-button mt-4\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.exportSettings()}}},[_c('Icon',{staticClass:\"mr-3\",attrs:{\"icon\":\"fa6-solid:download\",\"height\":\"19px\"}}),_vm._v(\"設定をエクスポート \")],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading error--text text--lighten-1\"},[_vm._v(\"設定をインポート\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" [設定をエクスポート] でダウンロードした設定データを、このデバイス(ブラウザ)にインポートできます。\"),_c('br'),_vm._v(\" 設定をインポートすると、それまでこのデバイス(ブラウザ)に保存されていた設定が、すべてインポート先の設定データで上書きされます。元に戻すことはできません。 \")]),_c('v-file-input',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"設定データ (KonomiTV-Settings.json) を選択\",\"dense\":_vm.is_form_dense,\"accept\":\"application/json\",\"prepend-icon\":\"\",\"prepend-inner-icon\":\"mdi-paperclip\"},model:{value:(_vm.import_settings_file),callback:function ($$v) {_vm.import_settings_file=$$v},expression:\"import_settings_file\"}})],1),_c('v-btn',{staticClass:\"settings__save-button error mt-5\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.importSettings()}}},[_c('Icon',{staticClass:\"mr-3\",attrs:{\"icon\":\"fa6-solid:upload\",\"height\":\"19px\"}}),_vm._v(\"設定をインポート \")],1)],1)])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('v-card',{staticClass:\"settings-container d-flex px-5 py-5 mx-auto background\",attrs:{\"elevation\":\"0\",\"width\":\"100%\",\"max-width\":\"1000\"}},[_c('div',[_c('v-navigation-drawer',{staticClass:\"settings-navigation flex-shrink-0 background\",attrs:{\"permanent\":\"\",\"width\":\"195\",\"height\":\"auto\"}},[_c('v-list-item',{staticClass:\"px-4\"},[_c('v-list-item-content',[_c('h1',[_vm._v(\"設定\")])])],1),_c('v-list',{staticClass:\"mt-2 px-0\",attrs:{\"nav\":\"\"}},[_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/general\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 3px\"},attrs:{\"icon\":\"fa-solid:sliders-h\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"全般\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/account\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"アカウント\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/jikkyo\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 2px\"},attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"ニコニコ実況\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/twitter\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{staticStyle:{\"padding\":\"0 1px\"},attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"Twitter\")])],1)],1),_c('v-list-item',{staticClass:\"px-4\",attrs:{\"link\":\"\",\"color\":\"primary\",\"to\":\"/settings/environment\"}},[_c('v-list-item-icon',{staticClass:\"mr-4\"},[_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"26px\"}})],1),_c('v-list-item-content',[_c('v-list-item-title',[_vm._v(\"環境設定\")])],1)],1)],1)],1)],1),_c('v-card',{staticClass:\"settings ml-5 px-7 py-7 background lighten-1\",attrs:{\"width\":\"100%\"}},[_vm._t(\"default\")],2)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\n// 設定のベース画面なので、ロジックは基本置かない\nexport default Vue.extend({\n name: 'Settings-Base',\n components: {\n Header,\n Navigation,\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Base.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Base.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Base.vue?vue&type=template&id=03345d7e&scoped=true&\"\nimport script from \"./Base.vue?vue&type=script&lang=ts&\"\nexport * from \"./Base.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Base.vue?vue&type=style&index=0&id=03345d7e&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"03345d7e\",\n null\n \n)\n\nexport default component.exports","\n\nimport Vue from 'vue';\n\nimport Base from '@/views/Settings/Base.vue';\nimport Utils, { PlayerUtils } from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-General',\n components: {\n Base,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n PlayerUtils: PlayerUtils,\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // テレビのストリーミング画質の選択肢\n tv_streaming_quality: [\n {'text': '1080p (60fps) (1時間あたり約3.24GB / 7.2Mbps)', 'value': '1080p-60fps'},\n {'text': '1080p (1時間あたり約2.31GB / 5.1Mbps)', 'value': '1080p'},\n {'text': '810p (1時間あたり約1.92GB / 4.2Mbps)', 'value': '810p'},\n {'text': '720p (1時間あたり約1.33GB / 3.0Mbps)', 'value': '720p'},\n {'text': '540p (1時間あたり約1.00GB / 2.2Mbps)', 'value': '540p'},\n {'text': '480p (1時間あたり約0.74GB / 1.6Mbps)', 'value': '480p'},\n {'text': '360p (1時間あたり約0.40GB / 0.9Mbps)', 'value': '360p'},\n {'text': '240p (1時間あたり約0.23GB / 0.5Mbps)', 'value': '240p'},\n ],\n\n // 既定のパネルの表示状態の選択肢\n panel_display_state: [\n {'text': '前回の状態を復元する', 'value': 'RestorePreviousState'},\n {'text': '常に表示する', 'value': 'AlwaysDisplay'},\n {'text': '常に折りたたむ', 'value': 'AlwaysFold'},\n ],\n\n // テレビをみるときに既定で表示されるパネルのタブの選択肢\n tv_panel_active_tab: [\n {'text': '番組情報タブ', 'value': 'Program'},\n {'text': 'チャンネルタブ', 'value': 'Channel'},\n {'text': 'コメントタブ', 'value': 'Comment'},\n {'text': 'Twitter タブ', 'value': 'Twitter'},\n ],\n\n // 字幕のフォントの選択肢\n caption_font: [\n {'text': 'Windows TV ゴシック', 'value': 'Windows TV Gothic'},\n {'text': 'Windows TV 丸ゴシック', 'value': 'Windows TV MaruGothic'},\n {'text': 'Windows TV 太丸ゴシック', 'value': 'Windows TV FutoMaruGothic'},\n {'text': 'ヒラギノTV丸ゴ', 'value': 'Hiragino TV Sans Rd S'},\n {'text': '新丸ゴ ARIB', 'value': 'TT-ShinMGo-regular'},\n {'text': 'Rounded M+ 1m for ARIB', 'value': 'Rounded M+ 1m for ARIB'},\n {'text': 'Noto Sans JP', 'value': 'Noto Sans JP Caption'},\n {'text': 'デフォルトのフォント', 'value': 'sans-serif'},\n ],\n\n // キャプチャの保存先の選択肢\n capture_save_mode: [\n {'text': 'ブラウザでダウンロード', 'value': 'Browser'},\n {'text': 'KonomiTV サーバーにアップロード', 'value': 'UploadServer'},\n {'text': 'ブラウザでのダウンロードと、KonomiTV サーバーへのアップロードを両方行う', 'value': 'Both'},\n ],\n\n // 字幕が表示されているときのキャプチャの保存モードの選択肢\n capture_caption_mode: [\n {'text': '映像のみのキャプチャを保存する', 'value': 'VideoOnly'},\n {'text': '字幕を合成したキャプチャを保存する', 'value': 'CompositingCaption'},\n {'text': '映像のみのキャプチャと、字幕を合成したキャプチャを両方保存する', 'value': 'Both'},\n ],\n\n // 選択された設定データ (KonomiTV-Settings.json) が入る\n import_settings_file: null as File | null,\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {}\n const setting_keys = [\n 'tv_streaming_quality',\n 'tv_data_saver_mode',\n 'tv_low_latency_mode',\n 'tv_show_superimpose',\n 'panel_display_state',\n 'tv_panel_active_tab',\n 'caption_font',\n 'always_border_caption_text',\n 'specify_caption_background_color',\n 'caption_background_color',\n 'capture_copy_to_clipboard',\n 'capture_save_mode',\n 'capture_caption_mode',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n\n // 設定データをエクスポートする\n exportSettings() {\n\n // JSON のままの設定データを LocalStorage から直に取得\n // \"KonomiTV-Settings\" キーがないときはデフォルト設定を JSON 化したものを入れる\n const settings_json = localStorage.getItem('KonomiTV-Settings') || JSON.stringify(Utils.default_settings);\n\n // ダウンロードさせるために Blob にしてから、KonomiTV-Settings.json としてダウンロード\n const settings_json_blob = new Blob([settings_json], {type: 'application/json'});\n Utils.downloadBlobData(settings_json_blob, 'KonomiTV-Settings.json');\n this.$message.success('設定をエクスポートしました。');\n },\n\n // 設定データをインポートする\n async importSettings() {\n\n // 設定データが選択されていないときは実行しない\n if (this.import_settings_file === null) {\n this.$message.error('インポートする設定データを選択してください!');\n return;\n }\n\n try {\n\n // 選択された設定データの JSON を取得してデコード\n // そのまま突っ込んでもいいんだけど、念のため一度オブジェクトになおしておく\n const settings = JSON.parse(await this.import_settings_file.text());\n\n // LocalStorage に直に保存\n // このとき、既存の設定データはすべて上書きされる\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(settings));\n\n // 設定データをサーバーに同期する\n await Utils.syncClientSettingsToServer();\n\n // 設定を適用するためリロード\n this.$message.success('設定をインポートしました。');\n window.setTimeout(() => this.$router.go(0), 300);\n\n } catch (error) {\n this.$message.error('設定データが不正なため、インポートできませんでした。');\n return;\n }\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./General.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./General.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./General.vue?vue&type=template&id=2c52b1a4&\"\nimport script from \"./General.vue?vue&type=script&lang=ts&\"\nexport * from \"./General.vue?vue&type=script&lang=ts&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fluent:person-20-filled\",\"width\":\"25px\"}}),_c('span',{staticClass:\"ml-2\"},[_vm._v(\"アカウント\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[(_vm.user === null)?_c('div',{staticClass:\"account\"},[_c('div',{staticClass:\"account-wrapper\"},[_c('img',{staticClass:\"account__icon\",attrs:{\"src\":\"/assets/images/account-icon-default.png\"}}),_c('div',{staticClass:\"account__info\"},[_c('div',{staticClass:\"account__info-name\"},[_c('span',{staticClass:\"account__info-name-text\"},[_vm._v(\"ログインしていません\")])]),_c('span',{staticClass:\"account__info-id\"},[_vm._v(\"Not logged in\")])])]),_c('v-btn',{staticClass:\"account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"140\",\"height\":\"56\",\"depressed\":\"\",\"to\":\"/login/\"}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-in\"}}),_vm._v(\"ログイン \")],1)],1):_vm._e(),(_vm.user !== null)?_c('div',{staticClass:\"account\"},[_c('div',{staticClass:\"account-wrapper\"},[_c('img',{staticClass:\"account__icon\",attrs:{\"src\":_vm.user_icon_blob}}),_c('div',{staticClass:\"account__info\"},[_c('div',{staticClass:\"account__info-name\"},[_c('span',{staticClass:\"account__info-name-text\"},[_vm._v(_vm._s(_vm.user.name))]),(_vm.user.is_admin)?_c('span',{staticClass:\"account__info-admin\"},[_vm._v(\"管理者\")]):_vm._e()]),_c('span',{staticClass:\"account__info-id\"},[_vm._v(\"User ID: \"+_vm._s(_vm.user.id))])])]),_c('v-btn',{staticClass:\"account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"140\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logout()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-out\"}}),_vm._v(\"ログアウト \")],1)],1):_vm._e(),(_vm.is_logged_in === false)?_c('div',{staticClass:\"account-register\"},[_c('div',{staticClass:\"account-register__heading\"},[_vm._v(\" KonomiTV アカウントにログインすると、\"),_c('br'),_vm._v(\"より便利な機能が使えます! \")]),_c('div',{staticClass:\"account-register__feature\"},[_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"bi:chat-left-text-fill\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"ニコニコ実況にコメントする\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"テレビを見ながらニコニコ実況にコメントできます。別途、ニコニコアカウントとの連携が必要です。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fa-brands:twitter\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"Twitter 連携機能\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"テレビを見ながら Twitter にツイートしたり、検索したツイートをリアルタイムで表示できます。別途、Twitter アカウントとの連携が必要です。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fluent:arrow-sync-20-filled\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"設定をデバイス間で同期\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"ピン留めしたチャンネルなど、ブラウザに保存されている各種設定をブラウザやデバイスをまたいで同期できます。\")])])],1),_c('div',{staticClass:\"account-feature\"},[_c('Icon',{staticClass:\"account-feature__icon\",attrs:{\"icon\":\"fa-solid:sliders-h\"}}),_c('div',{staticClass:\"account-feature__info\"},[_c('span',{staticClass:\"account-feature__info-heading\"},[_vm._v(\"環境設定をブラウザから変更\")]),_c('span',{staticClass:\"account-feature__info-text\"},[_vm._v(\"管理者権限があれば、環境設定をブラウザから変更できます。一番最初に作成されたアカウントには、自動で管理者権限が付与されます。\")])])],1)]),_c('div',{staticClass:\"account-register__description\"},[_vm._v(\" KonomiTV アカウントの作成に必要なものはユーザー名とパスワードだけです。\"),_c('br'),_vm._v(\" アカウントはローカルにインストールした KonomiTV サーバーごとに保存されます。\"),_c('br'),_vm._v(\" 外部のサービスには保存されませんので、ご安心ください。\"),_c('br')]),_c('v-btn',{staticClass:\"account-register__button\",attrs:{\"color\":\"secondary\",\"width\":\"100%\",\"max-width\":\"250\",\"height\":\"50\",\"depressed\":\"\",\"to\":\"/register/\"}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-add-20-filled\",\"height\":\"24\"}}),_vm._v(\"アカウントを作成 \")],1)],1):_vm._e(),(_vm.is_logged_in === true)?_c('div',[_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"sync_settings\"}},[_vm._v(\"設定をデバイス間で同期する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"sync_settings\"}},[_vm._v(\" KonomiTV の設定を、同じアカウントにログインしているデバイス同士で同期するかを設定します。\"),_c('br'),_vm._v(\" 同期を有効にすると、同期が有効なデバイスすべてで同じ設定が使えます。ピン留めしたチャンネルやハッシュタグリストなども同期されます。\"),_c('br'),_vm._v(\" ストリーミング画質やコメントの遅延時間など、デバイスごとに最適な設定が異なるものは、同期を有効にしたあとも引き続きこのデバイス(ブラウザ)のみに反映されます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"sync_settings\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.sync_settings),callback:function ($$v) {_vm.sync_settings=$$v},expression:\"sync_settings\"}})],1),_c('v-dialog',{attrs:{\"max-width\":\"530\"},model:{value:(_vm.sync_settings_dialog),callback:function ($$v) {_vm.sync_settings_dialog=$$v},expression:\"sync_settings_dialog\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"justify-center\"},[_vm._v(\"設定データの競合\")]),_c('v-card-text',[_vm._v(\" このデバイスの設定と、サーバーに保存されている設定が競合しています。\"),_c('br'),_vm._v(\" 一度上書きすると、元に戻すことはできません。慎重に選択してください。\"),_c('br')]),_c('div',{staticClass:\"d-flex flex-column px-4 pb-4\"},[_c('v-btn',{staticClass:\"settings__save-button error--text text--lighten-1\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.overrideServerSettingsFromClient()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:document-arrow-up-16-filled\",\"height\":\"22px\"}}),_vm._v(\" サーバーに保存されている設定を、このデバイスの設定で上書きする \")],1),_c('v-btn',{staticClass:\"settings__save-button error--text text--lighten-1 mt-3\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.overrideClientSettingsFromServer()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:document-arrow-down-16-filled\",\"height\":\"22px\"}}),_vm._v(\" このデバイスの設定を、サーバーに保存されている設定で上書きする \")],1),_c('v-btn',{staticClass:\"settings__save-button mt-3\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.sync_settings_dialog = false}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:dismiss-16-filled\",\"height\":\"22px\"}}),_vm._v(\" キャンセル \")],1)],1)],1)],1),_c('v-form',{ref:\"settings_username\",staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ユーザー名\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントのユーザー名を設定します。アルファベットだけでなく日本語や記号も使えます。\"),_c('br'),_vm._v(\" 同じ KonomiTV サーバー上の他のアカウントと同じユーザー名には変更できません。\"),_c('br')]),_c('v-text-field',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"dense\":_vm.is_form_dense,\"rules\":[_vm.settings_username_validation]},model:{value:(_vm.settings_username),callback:function ($$v) {_vm.settings_username=$$v},expression:\"settings_username\"}})],1),_c('v-btn',{staticClass:\"settings__save-button\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountInfo('username')}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"ユーザー名を更新 \")],1),_c('v-form',{staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"アイコン画像\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントのアイコン画像を設定します。\"),_c('br'),_vm._v(\" アップロードされた画像は自動的に 400×400 の正方形にリサイズされます。\"),_c('br')]),_c('v-file-input',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"placeholder\":\"アイコン画像を選択\",\"dense\":_vm.is_form_dense,\"accept\":\"image/jpeg, image/png\",\"prepend-icon\":\"\",\"prepend-inner-icon\":\"mdi-paperclip\"},model:{value:(_vm.settings_icon),callback:function ($$v) {_vm.settings_icon=$$v},expression:\"settings_icon\"}})],1),_c('v-btn',{staticClass:\"settings__save-button mt-5\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountIcon()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"アイコン画像を更新 \")],1),_c('v-form',{ref:\"settings_password\",staticClass:\"settings__item\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"新しいパスワード\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" KonomiTV アカウントの新しいパスワードを設定します。\"),_c('br')]),_c('v-text-field',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"placeholder\":\"新しいパスワード\",\"dense\":_vm.is_form_dense,\"type\":_vm.settings_password_showing ? 'text' : 'password',\"append-icon\":_vm.settings_password_showing ? 'mdi-eye' : 'mdi-eye-off',\"rules\":[_vm.settings_password_validation]},on:{\"click:append\":function($event){_vm.settings_password_showing = !_vm.settings_password_showing}},model:{value:(_vm.settings_password),callback:function ($$v) {_vm.settings_password=$$v},expression:\"settings_password\"}})],1),_c('v-btn',{staticClass:\"settings__save-button\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){return _vm.updateAccountInfo('password')}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:save-16-filled\",\"height\":\"24px\"}}),_vm._v(\"パスワードを更新 \")],1),_c('v-divider',{staticClass:\"mt-6\"}),_c('div',{staticClass:\"settings__item mt-6\"},[_c('div',{staticClass:\"settings__item-heading error--text text--lighten-1\"},[_vm._v(\"アカウントを削除\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 現在ログインしている KonomiTV アカウントを削除します。\"),_c('br'),_vm._v(\" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。\"),_c('br')])]),_c('v-dialog',{attrs:{\"max-width\":\"385\"},scopedSlots:_vm._u([{key:\"activator\",fn:function({ on, attrs }){return [_c('v-btn',_vm._g(_vm._b({staticClass:\"settings__save-button error mt-5\",attrs:{\"depressed\":\"\"}},'v-btn',attrs,false),on),[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:delete-16-filled\",\"height\":\"24px\"}}),_vm._v(\"アカウントを削除 \")],1)]}}],null,false,974850237),model:{value:(_vm.account_delete_confirm_dialog),callback:function ($$v) {_vm.account_delete_confirm_dialog=$$v},expression:\"account_delete_confirm_dialog\"}},[_c('v-card',[_c('v-card-title',{staticClass:\"justify-center\"},[_vm._v(\"本当にアカウントを削除しますか?\")]),_c('v-card-text',[_vm._v(\" アカウントに紐づくすべてのデータが削除されます。元に戻すことはできません。\"),_c('br'),_vm._v(\" 本当にアカウントを削除しますか? \")]),_c('v-card-actions',[_c('v-spacer'),_c('v-btn',{attrs:{\"color\":\"text\",\"text\":\"\"},on:{\"click\":function($event){_vm.account_delete_confirm_dialog = false}}},[_vm._v(\"キャンセル\")]),_c('v-btn',{attrs:{\"color\":\"error\",\"text\":\"\"},on:{\"click\":function($event){return _vm.deleteAccount()}}},[_vm._v(\"削除\")])],1)],1)],1)],1):_vm._e()])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Account',\n components: {\n Base,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // ユーザーアカウントのアイコンの Blob URL\n user_icon_blob: '',\n\n // ユーザー名とパスワード\n // ログイン画面やアカウント作成画面の data と同一のもの\n settings_username: null as string | null,\n settings_username_validation: (value: string | null) => {\n if (value === '' || value === null) return 'ユーザー名を入力してください。';\n if (/^.{2,}$/.test(value) === false) return 'ユーザー名は2文字以上で入力してください。';\n return true;\n },\n settings_password: null as string | null,\n settings_password_showing: true, // アカウント情報変更時は既定でパスワードを表示する\n settings_password_validation: (value: string | null) => {\n if (value === '' || value === null) return 'パスワードを入力してください。';\n // 正規表現の参考: https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128\n if (/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(value) === false) return 'パスワードは4文字以上の半角英数記号を入力してください。';\n return true;\n },\n\n // アイコン画像\n settings_icon: null as File | null,\n\n // アカウント削除確認ダイヤログ\n account_delete_confirm_dialog: null,\n\n // 設定を同期するかの設定値\n sync_settings: Utils.getSettingsItem('sync_settings') as boolean,\n\n // 設定を同期するときのダイヤログ\n sync_settings_dialog: false,\n }\n },\n async created() {\n\n // 表示されているアカウント情報を更新\n // アクセストークンが無効化されている可能性もあるので、アクセストークンの有無に関わらず実行する\n await this.syncAccountInfo();\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // sync_settings の値の変更を監視する\n async sync_settings() {\n\n // 同期がオンになった & ダイヤログが表示されていない\n if (this.sync_settings === true && this.sync_settings_dialog === false) {\n\n try {\n\n // もし KonomiTV-Settings キーがまだない場合、あらかじめデフォルトの設定値を保存しておく\n if (localStorage.getItem('KonomiTV-Settings') === null) {\n localStorage.setItem('KonomiTV-Settings', JSON.stringify(Utils.default_settings));\n }\n\n // LocalStorage から KonomiTV-Settings を取得\n const settings: {[key: string]: any} = JSON.parse(localStorage.getItem('KonomiTV-Settings'));\n\n // 同期対象の設定キーのみで設定データをまとめ直す\n // sync_settings には同期対象外の設定は含まれない\n const sync_settings: {[key: string]: any} = {};\n for (const sync_settings_key of Utils.sync_settings_keys) {\n if (sync_settings_key in settings) {\n sync_settings[sync_settings_key] = settings[sync_settings_key];\n } else {\n // 後から追加された設定キーなどの理由で設定キーが現状の KonomiTV-Settings に存在しない場合\n // その設定キーのデフォルト値を取得する\n sync_settings[sync_settings_key] = Utils.default_settings[sync_settings_key];\n }\n }\n\n // 同期対象のこのクライアントの設定を再度 JSON にする(文字列比較のため)\n const sync_settings_json = JSON.stringify(sync_settings);\n\n // サーバーから設定データ (生の JSON) をダウンロード\n // 一度オブジェクトにしたものを再度 JSON にする(文字列比較のため)\n const server_sync_settings_json: string = JSON.stringify((await Vue.axios.get('/settings/client')).data);\n\n // このクライアントの設定とサーバーに保存されている設定が一致しない(=競合している)\n if (sync_settings_json !== server_sync_settings_json) {\n\n // 一度同期をオフにして、クライアントとサーバーどちらの設定を使うのかを選択させるダイヤログを表示\n this.sync_settings_dialog = true;\n this.sync_settings = false;\n\n // このクライアントの設定とサーバーに保存されている設定が一致する\n } else {\n\n // 特に設定の同期をオンにしても問題ないので、そのまま有効にする\n Utils.setSettingsItem('sync_settings', true);\n }\n\n } catch (error) {\n // 何らかの理由でエラーになったとき\n this.$message.error(`サーバーから設定データを取得できませんでした。(HTTP Error ${error.response.status})`);\n }\n\n // 同期がオフになった & ダイヤログが表示されていない\n } else if (this.sync_settings === false && this.sync_settings_dialog === false) {\n Utils.setSettingsItem('sync_settings', false);\n }\n }\n },\n methods: {\n\n // このクライアントの設定でサーバー上の設定を上書きする\n async overrideServerSettingsFromClient() {\n\n // 強制的にこのクライアントの設定をサーバーに同期\n await Utils.syncClientSettingsToServer(true);\n\n // 設定の同期を有効化\n this.sync_settings = true;\n Utils.setSettingsItem('sync_settings', true);\n\n // ダイヤログを閉じる\n this.sync_settings_dialog = false;\n },\n\n // サーバー上の設定でこのクライアントの設定を上書きする\n async overrideClientSettingsFromServer() {\n\n // 強制的にサーバーに保存されている設定データをこのクライアントに同期する\n // 設定の同期を有効化する前に実行しておくのが重要\n await Utils.syncServerSettingsToClient(true);\n\n // 設定の同期を有効化\n // Utils.setSettingsItem() した段階で設定データがサーバーにアップロードされてしまうので、\n // それよりも前に Utils.syncServerSettingsToClient(true) でサーバー上の設定データを同期させておく必要がある\n // さもなければ、サーバー上の設定データがこのクライアントの設定で上書きされてしまい、overrideServerSettingsFromClient() と同じ挙動になってしまう\n this.sync_settings = true;\n Utils.setSettingsItem('sync_settings', true);\n\n // ダイヤログを閉じる\n this.sync_settings_dialog = false;\n },\n\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n const response = await Vue.axios.get('/users/me');\n this.user = response.data;\n this.settings_username = this.user.name;\n\n // 表示中のアイコン画像を更新\n await this.syncAccountIcon();\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n console.log('Not logged in.');\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n // まだアクセストークンが残っているかもしれないので、明示的にログアウト\n Utils.deleteAccessToken();\n }\n }\n },\n\n async syncAccountIcon() {\n\n // ユーザーアカウントのアイコンを取得する\n // 認証が必要な URL は img タグからは直で読み込めないため\n const icon_response = await Vue.axios.get('/users/me/icon', {\n responseType: 'arraybuffer',\n });\n\n // Blob URL を生成\n this.user_icon_blob = URL.createObjectURL(new Blob([icon_response.data], {type: 'image/png'}));\n },\n\n async updateAccountInfo(update_type: 'username' | 'password') {\n\n // すべてのバリデーションが通過したときのみ\n // ref: https://qiita.com/Hijiri_Ishi/items/56cac99c8f3806a6fa24\n if (update_type === 'username') {\n if ((this.$refs.settings_username as any).validate() === false) return;\n } else {\n if ((this.$refs.settings_password as any).validate() === false) return;\n }\n\n try {\n\n // アカウント情報更新 API にリクエスト\n // レスポンスは 204 No Content なので不要\n if (update_type === 'username') {\n await Vue.axios.put('/users/me', {username: this.settings_username});\n this.$message.show('ユーザー名を更新しました。');\n } else {\n await Vue.axios.put('/users/me', {password: this.settings_password});\n this.$message.show('パスワードを更新しました。');\n }\n\n // 表示中のアカウント情報を更新\n await this.syncAccountInfo();\n\n } catch (error) {\n\n // アカウント情報の更新に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Specified username is duplicated': {\n this.$message.error('ユーザー名が重複しています。');\n break;\n }\n case 'Specified username is not accepted due to system limitations': {\n this.$message.error('ユーザー名に token と me は使えません。');\n break;\n }\n default: {\n this.$message.error(`アカウント情報を更新できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n },\n\n async updateAccountIcon() {\n\n // アイコン画像が選択されていないなら更新しない\n if (this.settings_icon === null) {\n this.$message.error('アップロードする画像を選択してください!');\n return;\n }\n\n // アイコン画像の File オブジェクト (= Blob) を FormData に入れる\n // multipart/form-data で送るために必要\n // ref: https://r17n.page/2020/02/04/nodejs-axios-file-upload-api/\n const form_data = new FormData();\n form_data.append('image', this.settings_icon);\n\n try {\n\n // アカウントアイコン画像更新 API にリクエスト\n await Vue.axios.put('/users/me/icon', form_data, {headers: {'Content-Type': 'multipart/form-data'}});\n\n // 表示中のアイコン画像を更新\n await this.syncAccountIcon();\n\n } catch (error) {\n\n // アカウント情報の更新に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Please upload JPEG or PNG image': {\n this.$message.error('JPEG または PNG 画像をアップロードしてください。');\n break;\n }\n default: {\n this.$message.error(`アイコン画像を更新できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n },\n\n async deleteAccount() {\n\n // ダイヤログを閉じる\n this.account_delete_confirm_dialog = false;\n\n // アカウント削除 API にリクエスト\n await Vue.axios.delete('/users/me');\n\n // 設定の同期を無効化\n Utils.setSettingsItem('sync_settings', false);\n\n // ブラウザからアクセストークンを削除\n Utils.deleteAccessToken();\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n this.$message.show('アカウントを削除しました。');\n },\n\n logout() {\n\n // 設定の同期を無効化\n Utils.setSettingsItem('sync_settings', false);\n\n // ブラウザからアクセストークンを削除\n // これをもってログアウトしたことになる(それ以降の Axios のリクエストにはアクセストークンが含まれなくなる)\n Utils.deleteAccessToken();\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n this.user_icon_blob = '';\n\n this.$message.success('ログアウトしました。');\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Account.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Account.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Account.vue?vue&type=template&id=12036e32&scoped=true&\"\nimport script from \"./Account.vue?vue&type=script&lang=ts&\"\nexport * from \"./Account.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Account.vue?vue&type=style&index=0&id=12036e32&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"12036e32\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"19px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"ニコニコ実況\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[(_vm.user.niconico_user_id === null)?_c('div',{staticClass:\"niconico-account niconico-account--anonymous\"},[_c('div',{staticClass:\"niconico-account-wrapper\"},[_c('Icon',{staticClass:\"flex-shrink-0\",attrs:{\"icon\":\"bi:chat-left-text-fill\",\"width\":\"45px\"}}),_c('div',{staticClass:\"niconico-account__info ml-4\"},[_c('div',{staticClass:\"niconico-account__info-name\"},[_c('span',{staticClass:\"niconico-account__info-name-text\"},[_vm._v(\"ニコニコアカウントと連携していません\")])]),_c('span',{staticClass:\"niconico-account__info-description\"},[_vm._v(\" ニコニコアカウントと連携すると、テレビを見ながらニコニコ実況にコメントできるようになります。 \")])])],1),_c('v-btn',{staticClass:\"niconico-account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"130\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.loginNiconicoAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-connected-20-filled\",\"height\":\"26\"}}),_vm._v(\"連携する \")],1)],1):_vm._e(),(_vm.user.niconico_user_id !== null)?_c('div',{staticClass:\"niconico-account\"},[_c('div',{staticClass:\"niconico-account-wrapper\"},[_c('img',{staticClass:\"niconico-account__icon\",attrs:{\"src\":this.niconico_user_icon_url}}),_c('div',{staticClass:\"niconico-account__info\"},[_c('div',{staticClass:\"niconico-account__info-name\"},[_c('span',{staticClass:\"niconico-account__info-name-text\"},[_vm._v(_vm._s(_vm.user.niconico_user_name)+\" と連携しています\")])]),_c('span',{staticClass:\"niconico-account__info-description\"},[_c('span',{staticClass:\"mr-2\"},[_vm._v(\"Niconico User ID:\")]),_c('a',{staticClass:\"mr-2\",attrs:{\"href\":`https://www.nicovideo.jp/user/${_vm.user.niconico_user_id}`,\"target\":\"_blank\"}},[_vm._v(_vm._s(_vm.user.niconico_user_id))]),(_vm.user.niconico_user_premium == true)?_c('span',{staticClass:\"secondary--text\"},[_vm._v(\"(Premium)\")]):_vm._e()])])]),_c('v-btn',{staticClass:\"niconico-account__login ml-auto\",attrs:{\"color\":\"secondary\",\"width\":\"130\",\"height\":\"56\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logoutNiconicoAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-disconnected-20-filled\",\"height\":\"26\"}}),_vm._v(\"連携解除 \")],1)],1):_vm._e(),_c('div',{staticClass:\"settings__item mt-7\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントのミュート設定\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 表示したくないコメントを、画面やコメントリストに表示しないようにミュートできます。\"),_c('br')])]),_c('v-btn',{staticClass:\"settings__save-button mt-4\",attrs:{\"depressed\":\"\"},on:{\"click\":function($event){_vm.comment_mute_settings_modal = !_vm.comment_mute_settings_modal}}},[_c('Icon',{attrs:{\"icon\":\"heroicons-solid:filter\",\"height\":\"19px\"}}),_c('span',{staticClass:\"ml-1\"},[_vm._v(\"コメントのミュート設定を開く\")])],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの速さ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーに流れるコメントの速さを設定します。\"),_c('br'),_vm._v(\" たとえば 1.2 に設定すると、コメントが 1.2 倍速く流れます。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"step\":0.1,\"min\":0.5,\"max\":2},model:{value:(_vm.settings.comment_speed_rate),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_speed_rate\", $$v)},expression:\"settings.comment_speed_rate\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの文字サイズ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーに流れるコメントの文字サイズの基準値を設定します。\"),_c('br'),_vm._v(\" 実際の文字サイズは画面の大きさに合わせて調整されます。既定の文字サイズは 34px です。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"min\":20,\"max\":60},model:{value:(_vm.settings.comment_font_size),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_font_size\", $$v)},expression:\"settings.comment_font_size\"}})],1),_c('div',{staticClass:\"settings__item settings__item--sync-disabled\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"コメントの遅延時間\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" プレイヤーやコメントリストに表示されるコメントを何秒遅らせて反映するかを設定します。\"),_c('br'),_vm._v(\" 通常は 1.75 秒程度で大丈夫です。ネットワークが遅いなどでタイムラグが大きいときだけ、映像の遅延に合わせて調整してください。\"),_c('br')]),_c('v-slider',{staticClass:\"settings__item-form\",attrs:{\"ticks\":\"always\",\"thumb-label\":\"\",\"hide-details\":\"\",\"step\":0.25,\"min\":0,\"max\":10},model:{value:(_vm.settings.comment_delay_time),callback:function ($$v) {_vm.$set(_vm.settings, \"comment_delay_time\", $$v)},expression:\"settings.comment_delay_time\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"close_comment_form_after_sending\"}},[_vm._v(\"コメント送信後にコメント入力フォームを閉じる\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"close_comment_form_after_sending\"}},[_vm._v(\" コメントを送信したあとに、コメント入力フォームを自動的に閉じるかを設定します。\"),_c('br'),_vm._v(\" 基本的にはオンのままにしておくことをおすすめします。コメント入力フォームが表示されたままだと、大部分のショートカットキーが文字入力と競合して使えないためです。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"close_comment_form_after_sending\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.close_comment_form_after_sending),callback:function ($$v) {_vm.$set(_vm.settings, \"close_comment_form_after_sending\", $$v)},expression:\"settings.close_comment_form_after_sending\"}})],1)],1),_c('CommentMuteSettings',{model:{value:(_vm.comment_mute_settings_modal),callback:function ($$v) {_vm.comment_mute_settings_modal=$$v},expression:\"comment_mute_settings_modal\"}})],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport CommentMuteSettings from '@/components/Settings/CommentMuteSettings.vue';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Jikkyo',\n components: {\n Base,\n CommentMuteSettings,\n },\n data() {\n return {\n\n // ユーティリティをテンプレートで使えるように\n Utils: Utils,\n\n // コメントのミュート設定のモーダルを表示するか\n comment_mute_settings_modal: false,\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // ニコニコアカウントのユーザーアイコンの URL\n niconico_user_icon_url: '',\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [\n 'comment_speed_rate',\n 'comment_font_size',\n 'comment_delay_time',\n 'close_comment_form_after_sending',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n // 初回描画で niconico_user_id が null かを判定するだけのためにセットしている\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n }\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n const response = await Vue.axios.get('/users/me');\n this.user = response.data;\n\n // ニコニコアカウントのユーザーアイコンの URL を生成 (ニコニコアカウントと連携されている場合のみ)\n if (this.user.niconico_user_id !== null) {\n const user_id_slice = this.user.niconico_user_id.toString().slice(0, 4);\n this.niconico_user_icon_url =\n `https://secure-dcdn.cdn.nimg.jp/nicoaccount/usericon/${user_id_slice}/${this.user.niconico_user_id}.jpg`;\n }\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n async loginNiconicoAccount() {\n\n // ログインしていない場合はエラーにする\n if (this.is_logged_in === false) {\n this.$message.warning('連携をはじめるには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // ニコニコアカウントと連携するための認証 URL を取得\n const authorization_url = (await Vue.axios.get('/niconico/auth')).data.authorization_url;\n\n // OAuth 連携のため、認証 URL をポップアップウインドウで開く\n // window.open() の第2引数はユニークなものにしておくと良いらしい\n // ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1 (大変参考になりました)\n const popup_window = window.open(authorization_url, 'KonomiTV-OAuthPopup', Utils.getWindowFeatures());\n\n // 認証完了 or 失敗後、ポップアップウインドウから送信される文字列を受信\n const onMessage = async (event) => {\n\n // すでにウインドウが閉じている場合は実行しない\n if (popup_window.closed) return;\n\n // 受け取ったオブジェクトに KonomiTV-OAuthPopup キーがない or そもそもオブジェクトではない際は実行しない\n // ブラウザの拡張機能から結構余計な message が飛んでくるっぽい…。\n if (Utils.typeof(event.data) !== 'object') return;\n if (('KonomiTV-OAuthPopup' in event.data) === false) return;\n\n // 認証は完了したので、ポップアップウインドウを閉じ、リスナーを解除する\n if (popup_window) popup_window.close();\n window.removeEventListener('message', onMessage);\n\n // ステータスコードと詳細メッセージを取得\n const authorization_status = event.data['KonomiTV-OAuthPopup']['status'] as number;\n const authorization_detail = event.data['KonomiTV-OAuthPopup']['detail'] as string;\n console.log(`NiconicoAuthCallbackAPI: Status: ${authorization_status} / Detail: ${authorization_detail}`);\n\n // OAuth 連携に失敗した\n if (authorization_status !== 200) {\n if (authorization_detail.startsWith('Authorization was denied (access_denied)')) {\n this.$message.error('ニコニコアカウントとの連携がキャンセルされました。');\n } else if (authorization_detail.startsWith('Failed to get access token (HTTP Error ')) {\n const error = authorization_detail.replace('Failed to get access token ', '');\n this.$message.error(`アクセストークンの取得に失敗しました。${error}`);\n } else if (authorization_detail.startsWith('Failed to get access token (Connection Timeout)')) {\n this.$message.error('アクセストークンの取得に失敗しました。ニコニコで障害が発生している可能性があります。');\n } else if (authorization_detail.startsWith('Failed to get user information (HTTP Error ')) {\n const error = authorization_detail.replace('Failed to get user information ', '');\n this.$message.error(`ニコニコアカウントのユーザー情報の取得に失敗しました。${error}`);\n } else if (authorization_detail.startsWith('Failed to get user information (Connection Timeout)')) {\n this.$message.error('ニコニコアカウントのユーザー情報の取得に失敗しました。ニコニコで障害が発生している可能性があります。');\n } else {\n this.$message.error(`ニコニコアカウントとの連携に失敗しました。(${authorization_detail})`);\n }\n return;\n }\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success('ニコニコアカウントと連携しました。');\n };\n\n // postMessage() を受信するリスナーを登録\n window.addEventListener('message', onMessage);\n },\n\n async logoutNiconicoAccount() {\n\n // ニコニコアカウント連携解除 API にリクエスト\n await Vue.axios.delete('/niconico/logout');\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success('ニコニコアカウントとの連携を解除しました。');\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Jikkyo.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Jikkyo.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Jikkyo.vue?vue&type=template&id=786083d5&scoped=true&\"\nimport script from \"./Jikkyo.vue?vue&type=script&lang=ts&\"\nexport * from \"./Jikkyo.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Jikkyo.vue?vue&type=style&index=0&id=786083d5&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"786083d5\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"22px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"Twitter\")])],1),_c('div',{staticClass:\"settings__content\",class:{'settings__content--loading': _vm.is_loading}},[_c('div',{staticClass:\"twitter-accounts\"},[(_vm.user.twitter_accounts.length > 0)?_c('div',{staticClass:\"twitter-accounts__heading\"},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-board-20-filled\",\"height\":\"30\"}}),_vm._v(\"連携中のアカウント \")],1):_vm._e(),(_vm.user.twitter_accounts.length === 0)?_c('div',{staticClass:\"twitter-accounts__guide\"},[_c('Icon',{staticClass:\"flex-shrink-0\",attrs:{\"icon\":\"fa-brands:twitter\",\"width\":\"45px\"}}),_c('div',{staticClass:\"ml-4\"},[_c('div',{staticClass:\"font-weight-bold text-h6\"},[_vm._v(\"Twitter アカウントと連携していません\")]),_c('div',{staticClass:\"text--text text--darken-1 text-subtitle-2 mt-1\"},[_vm._v(\" Twitter アカウントと連携すると、テレビを見ながら Twitter にツイートしたり、ほかの実況ツイートをリアルタイムで表示できるようになります。 \")])])],1):_vm._e(),_vm._l((_vm.user.twitter_accounts),function(twitter_account){return _c('div',{key:twitter_account.id,staticClass:\"twitter-account\"},[_c('img',{staticClass:\"twitter-account__icon\",attrs:{\"src\":twitter_account.icon_url}}),_c('div',{staticClass:\"twitter-account__info\"},[_c('div',{staticClass:\"twitter-account__info-name\"},[_c('span',{staticClass:\"twitter-account__info-name-text\"},[_vm._v(_vm._s(twitter_account.name))])]),_c('span',{staticClass:\"twitter-account__info-screen-name\"},[_vm._v(\"@\"+_vm._s(twitter_account.screen_name))])]),_c('v-btn',{staticClass:\"twitter-account__logout ml-auto\",attrs:{\"width\":\"124\",\"height\":\"52\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.logoutTwitterAccount(twitter_account.screen_name)}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-disconnected-20-filled\",\"height\":\"24\"}}),_vm._v(\"連携解除 \")],1)],1)}),_c('v-btn',{staticClass:\"twitter-account__login\",attrs:{\"color\":\"secondary\",\"max-width\":\"250\",\"height\":\"50\",\"depressed\":\"\"},on:{\"click\":function($event){return _vm.loginTwitterAccount()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:plug-connected-20-filled\",\"height\":\"24\"}}),_vm._v(\"連携するアカウントを追加 \")],1)],2),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"fold_panel_after_sending_tweet\"}},[_vm._v(\"ツイート送信後にパネルを閉じる\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"fold_panel_after_sending_tweet\"}},[_vm._v(\" ツイートを送信した後に、表示中のパネルを閉じる(折りたたむ)かを設定します。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"fold_panel_after_sending_tweet\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.fold_panel_after_sending_tweet),callback:function ($$v) {_vm.$set(_vm.settings, \"fold_panel_after_sending_tweet\", $$v)},expression:\"settings.fold_panel_after_sending_tweet\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"reset_hashtag_when_program_switches\"}},[_vm._v(\"番組が切り替わったときにハッシュタグフォームをリセットする\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"reset_hashtag_when_program_switches\"}},[_vm._v(\" チャンネルを切り替えたときや、視聴中の番組が終了し次の番組の放送が開始されたときに、ハッシュタグフォームをリセットするかを設定します。\"),_c('br'),_vm._v(\" この設定をオンにしておけば、「誤って前番組のハッシュタグをつけたまま次番組の実況ツイートをしてしまう」といったミスを回避できます。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"reset_hashtag_when_program_switches\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.reset_hashtag_when_program_switches),callback:function ($$v) {_vm.$set(_vm.settings, \"reset_hashtag_when_program_switches\", $$v)},expression:\"settings.reset_hashtag_when_program_switches\"}})],1),_c('div',{staticClass:\"settings__item settings__item--switch\"},[_c('label',{staticClass:\"settings__item-heading\",attrs:{\"for\":\"auto_add_watching_channel_hashtag\"}},[_vm._v(\"視聴中のチャンネルに対応する局タグを自動的に追加する\")]),_c('label',{staticClass:\"settings__item-label\",attrs:{\"for\":\"auto_add_watching_channel_hashtag\"}},[_vm._v(\" ハッシュタグフォームに、視聴中のチャンネルに対応する局タグ (#nhk, #tokyomx など) を自動的に追加するかを設定します。\"),_c('br'),_vm._v(\" 現時点で、局タグは三大首都圏の地上波・BS の一部チャンネル・AT-X にのみ対応しています。\"),_c('br')]),_c('v-switch',{staticClass:\"settings__item-switch\",attrs:{\"id\":\"auto_add_watching_channel_hashtag\",\"inset\":\"\",\"hide-details\":\"\"},model:{value:(_vm.settings.auto_add_watching_channel_hashtag),callback:function ($$v) {_vm.$set(_vm.settings, \"auto_add_watching_channel_hashtag\", $$v)},expression:\"settings.auto_add_watching_channel_hashtag\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"既定で表示される Twitter タブ内のタブ\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" 視聴画面を開いたときに、パネルの Twitter タブの中で最初に表示されるタブを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.twitter_active_tab},model:{value:(_vm.settings.twitter_active_tab),callback:function ($$v) {_vm.$set(_vm.settings, \"twitter_active_tab\", $$v)},expression:\"settings.twitter_active_tab\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ツイートにつけるハッシュタグの位置\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" ツイート本文から見て、ハッシュタグをどの位置につけてツイートするかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tweet_hashtag_position},model:{value:(_vm.settings.tweet_hashtag_position),callback:function ($$v) {_vm.$set(_vm.settings, \"tweet_hashtag_position\", $$v)},expression:\"settings.tweet_hashtag_position\"}})],1),_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"ツイートするキャプチャに番組タイトルの透かしを描画する\")]),_c('div',{staticClass:\"settings__item-label\"},[_vm._v(\" ツイートするキャプチャに、視聴中の番組タイトルの透かしを描画するかを設定します。\"),_c('br')]),_c('v-select',{staticClass:\"settings__item-form\",attrs:{\"outlined\":\"\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"items\":_vm.tweet_capture_watermark_position},model:{value:(_vm.settings.tweet_capture_watermark_position),callback:function ($$v) {_vm.$set(_vm.settings, \"tweet_capture_watermark_position\", $$v)},expression:\"settings.tweet_capture_watermark_position\"}})],1)])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport { IUser } from '@/interface';\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Twitter',\n components: {\n Base,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n // 既定で表示されるパネルのタブの選択肢\n twitter_active_tab: [\n {'text': 'ツイート検索タブ', 'value': 'Search'},\n {'text': 'タイムラインタブ', 'value': 'Timeline'},\n {'text': 'キャプチャタブ', 'value': 'Capture'},\n ],\n\n // ツイートにつけるハッシュタグの位置の選択肢\n tweet_hashtag_position: [\n {'text': 'ツイート本文の前に追加する', 'value': 'Prepend'},\n {'text': 'ツイート本文の後に追加する', 'value': 'Append'},\n {'text': 'ツイート本文の前に追加してから改行する', 'value': 'PrependWithLineBreak'},\n {'text': 'ツイート本文の後に改行してから追加する', 'value': 'AppendWithLineBreak'},\n ],\n\n // ツイートするキャプチャに番組タイトルの透かしを描画する位置の選択肢\n tweet_capture_watermark_position: [\n {'text': '透かしを描画しない', 'value': 'None'},\n {'text': '透かしをキャプチャの左上に描画する', 'value': 'TopLeft'},\n {'text': '透かしをキャプチャの右上に描画する', 'value': 'TopRight'},\n {'text': '透かしをキャプチャの左下に描画する', 'value': 'BottomLeft'},\n {'text': '透かしをキャプチャの右下に描画する', 'value': 'BottomRight'},\n ],\n\n // ローディング中かどうか\n is_loading: true,\n\n // ログイン中かどうか\n is_logged_in: Utils.getAccessToken() !== null,\n\n // ユーザーアカウントの情報\n // ログインしていない場合は null になる\n user: null as IUser | null,\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [\n 'fold_panel_after_sending_tweet',\n 'reset_hashtag_when_program_switches',\n 'auto_add_watching_channel_hashtag',\n 'twitter_active_tab',\n 'tweet_hashtag_position',\n 'tweet_capture_watermark_position',\n ];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n async created() {\n\n // ユーザーモデルの初期値\n this.user = {\n id: 0,\n name: '',\n is_admin: true,\n niconico_user_id: null,\n niconico_user_name: null,\n niconico_user_premium: null,\n twitter_accounts: [],\n created_at: '',\n updated_at: '',\n }\n\n // 表示されているアカウント情報を更新 (ログイン時のみ)\n if (this.is_logged_in === true) {\n await this.syncAccountInfo();\n }\n\n // ローディング状態を解除\n this.is_loading = false;\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n },\n methods: {\n async syncAccountInfo() {\n\n try {\n\n // ユーザーアカウントの情報を取得する\n this.user = (await Vue.axios.get('/users/me')).data;\n\n } catch (error) {\n\n // ログインされていない\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n // 未ログイン状態に設定\n this.is_logged_in = false;\n this.user = null;\n }\n }\n },\n\n async loginTwitterAccount() {\n\n // ログインしていない場合はエラーにする\n if (this.is_logged_in === false) {\n this.$message.warning('連携をはじめるには、KonomiTV アカウントにログインしてください。');\n return;\n }\n\n // Twitter アカウントと連携するための認証 URL を取得\n const authorization_url = (await Vue.axios.get('/twitter/auth')).data.authorization_url;\n\n // OAuth 連携のため、認証 URL をポップアップウインドウで開く\n // window.open() の第2引数はユニークなものにしておくと良いらしい\n // ref: https://qiita.com/catatsuy/items/babce8726ea78f5d25b1 (大変参考になりました)\n const popup_window = window.open(authorization_url, 'KonomiTV-OAuthPopup', Utils.getWindowFeatures());\n\n // 認証完了 or 失敗後、ポップアップウインドウから送信される文字列を受信\n const onMessage = async (event) => {\n\n // すでにウインドウが閉じている場合は実行しない\n if (popup_window.closed) return;\n\n // 受け取ったオブジェクトに KonomiTV-OAuthPopup キーがない or そもそもオブジェクトではない際は実行しない\n // ブラウザの拡張機能から結構余計な message が飛んでくるっぽい…。\n if (Utils.typeof(event.data) !== 'object') return;\n if (('KonomiTV-OAuthPopup' in event.data) === false) return;\n\n // 認証は完了したので、ポップアップウインドウを閉じ、リスナーを解除する\n if (popup_window) popup_window.close();\n window.removeEventListener('message', onMessage);\n\n // ステータスコードと詳細メッセージを取得\n const authorization_status = event.data['KonomiTV-OAuthPopup']['status'] as number;\n const authorization_detail = event.data['KonomiTV-OAuthPopup']['detail'] as string;\n console.log(`TwitterAuthCallbackAPI: Status: ${authorization_status} / Detail: ${authorization_detail}`);\n\n // OAuth 連携に失敗した\n if (authorization_status !== 200) {\n if (authorization_detail.startsWith('Authorization was denied by user')) {\n this.$message.error('Twitter アカウントとの連携がキャンセルされました。');\n } else if (authorization_detail.startsWith('Failed to get access token')) {\n this.$message.error('アクセストークンの取得に失敗しました。');\n } else if (authorization_detail.startsWith('Failed to get user information')) {\n this.$message.error('Twitter アカウントのユーザー情報の取得に失敗しました。');\n } else {\n this.$message.error(`Twitter アカウントとの連携に失敗しました。(${authorization_detail})`);\n }\n return;\n }\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n // ログイン中のユーザーに紐づく Twitter アカウントのうち、一番 updated_at が新しいものを取得\n // ログインすると updated_at が更新されるため、この時点で一番 updated_at が新しいアカウントが今回連携したものだと判断できる\n // ref: https://stackoverflow.com/a/12192544/17124142 (ISO8601 のソートアルゴリズム)\n const current_twitter_account = [...this.user.twitter_accounts].sort((a, b) => {\n return (a.updated_at < b.updated_at) ? 1 : ((a.updated_at > b.updated_at) ? -1 : 0);\n })[0];\n\n this.$message.success(`Twitter @${current_twitter_account.screen_name} と連携しました。`);\n };\n\n // postMessage() を受信するリスナーを登録\n window.addEventListener('message', onMessage);\n },\n\n async logoutTwitterAccount(screen_name: string) {\n\n // Twitter アカウント連携解除 API にリクエスト\n await Vue.axios.delete(`/twitter/accounts/${screen_name}`);\n\n // 表示されているアカウント情報を更新\n await this.syncAccountInfo();\n\n this.$message.success(`Twitter @${screen_name} との連携を解除しました。`);\n },\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Twitter.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Twitter.vue?vue&type=template&id=1970b264&scoped=true&\"\nimport script from \"./Twitter.vue?vue&type=script&lang=ts&\"\nexport * from \"./Twitter.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Twitter.vue?vue&type=style&index=0&id=1970b264&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"1970b264\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('Base',[_c('h2',{staticClass:\"settings__heading\"},[_c('router-link',{directives:[{name:\"ripple\",rawName:\"v-ripple\"}],staticClass:\"settings__back-button\",attrs:{\"to\":\"/settings/\"}},[_c('Icon',{attrs:{\"icon\":\"fluent:arrow-left-12-filled\",\"width\":\"25px\"}})],1),_c('Icon',{attrs:{\"icon\":\"fluent:toolbox-20-filled\",\"width\":\"22px\"}}),_c('span',{staticClass:\"ml-3\"},[_vm._v(\"環境設定\")])],1),_c('div',{staticClass:\"settings__content\"},[_c('div',{staticClass:\"settings__item\"},[_c('div',{staticClass:\"settings__item-heading\"},[_vm._v(\"鋭意開発中…\")])])])])\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Base from '@/views/Settings/Base.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Settings-Environment',\n components: {\n Base,\n },\n data() {\n return {\n\n // 設定値が保存されるオブジェクト\n // ここの値とフォームを v-model で binding する\n settings: (() => {\n // 現在の設定値を取得する\n const settings = {};\n const setting_keys = [];\n for (const setting_key of setting_keys) {\n settings[setting_key] = Utils.getSettingsItem(setting_key);\n }\n return settings;\n })(),\n }\n },\n watch: {\n // settings 内の値の変更を監視する\n settings: {\n deep: true,\n handler() {\n // settings 内の値を順に LocalStorage に保存する\n for (const [setting_key, setting_value] of Object.entries(this.settings)) {\n Utils.setSettingsItem(setting_key, setting_value);\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Environment.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../../node_modules/thread-loader/dist/cjs.js!../../../node_modules/babel-loader/lib/index.js!../../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Environment.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Environment.vue?vue&type=template&id=39075e10&\"\nimport script from \"./Environment.vue?vue&type=script&lang=ts&\"\nexport * from \"./Environment.vue?vue&type=script&lang=ts&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n null,\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"login-container-wrapper d-flex align-center w-100 mb-13\"},[_c('v-card',{staticClass:\"login-container px-10 pt-8 pb-11 mx-auto background lighten-1\",attrs:{\"elevation\":\"10\",\"width\":\"100%\",\"max-width\":\"450\"}},[_c('v-card-title',{staticClass:\"login__logo justify-center pb-7\"},[_c('v-img',{attrs:{\"max-width\":\"250\",\"src\":\"/assets/images/logo.svg\"}})],1),_c('v-divider'),_c('v-form',{ref:\"login\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('v-text-field',{staticClass:\"mt-12\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"hide-details\":\"\",\"autofocus\":\"\",\"dense\":_vm.is_form_dense},model:{value:(_vm.username),callback:function ($$v) {_vm.username=$$v},expression:\"username\"}}),_c('v-text-field',{staticClass:\"mt-8\",attrs:{\"outlined\":\"\",\"placeholder\":\"パスワード\",\"hide-details\":\"\",\"dense\":_vm.is_form_dense,\"type\":_vm.password_showing ? 'text' : 'password',\"append-icon\":_vm.password_showing ? 'mdi-eye' : 'mdi-eye-off'},on:{\"click:append\":function($event){_vm.password_showing = !_vm.password_showing}},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}}),_c('v-btn',{staticClass:\"login-button mt-5\",attrs:{\"color\":\"secondary\",\"depressed\":\"\",\"width\":\"100%\",\"height\":\"56\"},on:{\"click\":function($event){return _vm.login()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fa:sign-in\"}}),_vm._v(\"ログイン \")],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Login',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n username: '' as string,\n password: '' as string,\n password_showing: false,\n }\n },\n methods: {\n async login() {\n\n // ユーザー名またはパスワードが空\n if (this.username === '' || this.password === '') return;\n\n try {\n\n // ログインしてアクセストークンを取得する\n const response = await Vue.axios.post('/users/token', new URLSearchParams({\n username: this.username,\n password: this.password,\n }));\n\n // 取得したアクセストークンを保存\n console.log('Login successful.');\n console.log(response.data);\n Utils.saveAccessToken(response.data.access_token);\n\n // アカウントページに遷移\n this.$message.success('ログインしました。');\n await this.$router.push({path: '/settings/account'});\n\n } catch (error) {\n\n // ログインに失敗\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n console.log('Failed to login.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Incorrect username': {\n this.$message.error('ログインできませんでした。そのユーザー名のアカウントは存在しません。');\n break;\n }\n case 'Incorrect password': {\n this.$message.error('ログインできませんでした。パスワードを間違えていませんか?');\n break;\n }\n default: {\n this.$message.error(`ログインできませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Login.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Login.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Login.vue?vue&type=template&id=0c2bb32a&scoped=true&\"\nimport script from \"./Login.vue?vue&type=script&lang=ts&\"\nexport * from \"./Login.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Login.vue?vue&type=style&index=0&id=0c2bb32a&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"0c2bb32a\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_c('div',{staticClass:\"register-container-wrapper d-flex align-center w-100 mb-13\"},[_c('v-card',{staticClass:\"register-container px-10 pt-8 pb-11 mx-auto background lighten-1\",attrs:{\"elevation\":\"10\",\"width\":\"100%\",\"max-width\":\"450\"}},[_c('v-card-title',{staticClass:\"register__logo flex-column justify-center\"},[_c('v-img',{attrs:{\"max-width\":\"250\",\"src\":\"/assets/images/logo.svg\"}}),_c('h4',{staticClass:\"mt-10\"},[_vm._v(\"アカウントを作成\")])],1),_c('v-divider'),_c('v-form',{ref:\"register\",on:{\"submit\":function($event){$event.preventDefault();}}},[_c('v-text-field',{staticClass:\"mt-10\",attrs:{\"outlined\":\"\",\"placeholder\":\"ユーザー名\",\"autofocus\":\"\",\"dense\":_vm.is_form_dense,\"rules\":[_vm.username_validation]},model:{value:(_vm.username),callback:function ($$v) {_vm.username=$$v},expression:\"username\"}}),_c('v-text-field',{staticClass:\"mt-2\",attrs:{\"outlined\":\"\",\"placeholder\":\"パスワード\",\"dense\":_vm.is_form_dense,\"type\":_vm.password_showing ? 'text' : 'password',\"append-icon\":_vm.password_showing ? 'mdi-eye' : 'mdi-eye-off',\"rules\":[_vm.password_validation]},on:{\"click:append\":function($event){_vm.password_showing = !_vm.password_showing}},model:{value:(_vm.password),callback:function ($$v) {_vm.password=$$v},expression:\"password\"}}),_c('v-btn',{staticClass:\"register-button mt-5\",attrs:{\"color\":\"secondary\",\"depressed\":\"\",\"width\":\"100%\",\"height\":\"56\"},on:{\"click\":function($event){return _vm.register()}}},[_c('Icon',{staticClass:\"mr-2\",attrs:{\"icon\":\"fluent:person-add-20-filled\",\"height\":\"24\"}}),_vm._v(\"アカウントを作成 \")],1)],1)],1)],1)],1)],1)\n}\nvar staticRenderFns = []\n\nexport { render, staticRenderFns }","\n\nimport axios from 'axios';\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\nimport Utils from '@/utils';\n\nexport default Vue.extend({\n name: 'Register',\n components: {\n Header,\n Navigation,\n },\n data() {\n return {\n\n // フォームを小さくするかどうか\n is_form_dense: Utils.isSmartphoneHorizontal(),\n\n username: null as string | null,\n username_validation: (value: string | null) => {\n if (value === '' || value === null) return 'ユーザー名を入力してください。';\n if (/^.{2,}$/.test(value) === false) return 'ユーザー名は2文字以上で入力してください。';\n return true;\n },\n password: null as string | null,\n password_showing: true, // アカウント作成時は既定でパスワードを表示する\n password_validation: (value: string | null) => {\n if (value === '' || value === null) return 'パスワードを入力してください。';\n // 正規表現の参考: https://qiita.com/grrrr/items/0b35b5c1c98eebfa5128\n if (/^[a-zA-Z0-9!-/:-@¥[-`{-~]{4,}$/.test(value) === false) return 'パスワードは4文字以上の半角英数記号を入力してください。';\n return true;\n },\n }\n },\n methods: {\n async register() {\n\n // すべてのバリデーションが通過したときのみ\n // ref: https://qiita.com/Hijiri_Ishi/items/56cac99c8f3806a6fa24\n if ((this.$refs.register as any).validate() === false) return;\n\n try {\n\n // アカウント作成 API にリクエスト\n const response = await Vue.axios.post('/users', {\n username: this.username,\n password: this.password,\n });\n\n console.log('Account created.')\n console.log(response.data);\n\n } catch (error) {\n\n // アカウントの作成に失敗\n // ref: https://dev.classmethod.jp/articles/typescript-typing-exception-objects-in-axios-trycatch/\n if (axios.isAxiosError(error) && error.response && error.response.status === 422) {\n\n console.log('Failed to create account.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Specified username is duplicated': {\n this.$message.error('ユーザー名が重複しています。');\n break;\n }\n case 'Specified username is not accepted due to system limitations': {\n this.$message.error('ユーザー名に token と me は使えません。');\n break;\n }\n default: {\n this.$message.error(`アカウントを作成できませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n return; // 処理を中断\n }\n\n // ここから先の処理はログイン画面とほぼ同じ\n try {\n\n // アカウントを作成できたので、ログインしてアクセストークンを取得する\n const response = await Vue.axios.post('/users/token', new URLSearchParams({\n username: this.username,\n password: this.password,\n }));\n\n // 取得したアクセストークンを保存\n console.log('Login successful.');\n console.log(response.data);\n Utils.saveAccessToken(response.data.access_token);\n\n // アカウントページに遷移\n this.$message.success('アカウントを作成しました。');\n await this.$router.push({path: '/settings/account'});\n\n } catch (error) {\n\n // ログインに失敗\n if (axios.isAxiosError(error) && error.response && error.response.status === 401) {\n\n console.log('Failed to login.');\n console.log(error.response.data);\n\n // エラーメッセージごとに Snackbar に表示\n switch ((error.response.data as any).detail) {\n case 'Incorrect username': {\n this.$message.error('ログインできませんでした。そのユーザー名のアカウントは存在しません。');\n break;\n }\n case 'Incorrect password': {\n this.$message.error('ログインできませんでした。パスワードを間違えていませんか?');\n break;\n }\n default: {\n this.$message.error(`ログインできませんでした。(HTTP Error ${error.response.status})`);\n break;\n }\n }\n }\n }\n }\n }\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Register.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./Register.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./Register.vue?vue&type=template&id=d0eaf0ae&scoped=true&\"\nimport script from \"./Register.vue?vue&type=script&lang=ts&\"\nexport * from \"./Register.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./Register.vue?vue&type=style&index=0&id=d0eaf0ae&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"d0eaf0ae\",\n null\n \n)\n\nexport default component.exports","var render = function render(){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"route-container\"},[_c('Header'),_c('main',[_c('Navigation'),_vm._m(0)],1)],1)\n}\nvar staticRenderFns = [function (){var _vm=this,_c=_vm._self._c,_setup=_vm._self._setupProxy;return _c('div',{staticClass:\"d-flex justify-center align-center w-100\"},[_c('div',{staticClass:\"d-flex justify-center align-center flex-column\"},[_c('h1',[_vm._v(\"Not Found, or Under Development...\")]),_c('span',{staticClass:\"mt-4 text--text text--darken-1\"},[_vm._v(\"お探しのページは存在しないか、鋭意開発中です。\")])])])\n}]\n\nexport { render, staticRenderFns }","\n\nimport Vue from 'vue';\n\nimport Header from '@/components/Header.vue';\nimport Navigation from '@/components/Navigation.vue';\n\nexport default Vue.extend({\n name: 'NotFound',\n components: {\n Header,\n Navigation,\n },\n});\n\n","import mod from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts&\"; export default mod; export * from \"-!../../node_modules/cache-loader/dist/cjs.js??clonedRuleSet-41.use[0]!../../node_modules/thread-loader/dist/cjs.js!../../node_modules/babel-loader/lib/index.js!../../node_modules/ts-loader/index.js??clonedRuleSet-41.use[3]!../../node_modules/cache-loader/dist/cjs.js??ruleSet[0].use[0]!../../node_modules/@vue/vue-loader-v15/lib/index.js??vue-loader-options!./NotFound.vue?vue&type=script&lang=ts&\"","import { render, staticRenderFns } from \"./NotFound.vue?vue&type=template&id=daa4530a&scoped=true&\"\nimport script from \"./NotFound.vue?vue&type=script&lang=ts&\"\nexport * from \"./NotFound.vue?vue&type=script&lang=ts&\"\nimport style0 from \"./NotFound.vue?vue&type=style&index=0&id=daa4530a&prod&lang=scss&scoped=true&\"\n\n\n/* normalize component */\nimport normalizer from \"!../../node_modules/@vue/vue-loader-v15/lib/runtime/componentNormalizer.js\"\nvar component = normalizer(\n script,\n render,\n staticRenderFns,\n false,\n null,\n \"daa4530a\",\n null\n \n)\n\nexport default component.exports","\nimport Vue from 'vue';\nimport VueRouter from 'vue-router';\n\nimport TVHome from '@/views/TV/Home.vue';\nimport TVWatch from '@/views/TV/Watch.vue';\nimport SettingsIndex from '@/views/Settings/Index.vue';\nimport SettingsGeneral from '@/views/Settings/General.vue';\nimport SettingsAccount from '@/views/Settings/Account.vue';\nimport SettingsJikkyo from '@/views/Settings/Jikkyo.vue';\nimport SettingsTwitter from '@/views/Settings/Twitter.vue';\nimport SettingsEnvironment from '@/views/Settings/Environment.vue';\nimport Login from '@/views/Login.vue';\nimport Register from '@/views/Register.vue';\nimport NotFound from '@/views/NotFound.vue';\nimport Utils from '@/utils';\n\nVue.use(VueRouter);\n\nconst router = new VueRouter({\n\n // History API モード\n mode: 'history',\n\n // ルーティングのベース URL\n base: process.env.BASE_URL,\n\n // ルーティング設定\n routes: [\n {\n path: '/',\n redirect: '/tv/',\n },\n {\n path: '/tv/',\n name: 'TV Home',\n component: TVHome,\n },\n {\n path: '/tv/watch/:channel_id',\n name: 'TV Watch',\n component: TVWatch,\n },\n {\n path: '/settings/',\n name: 'Settings Index',\n component: SettingsIndex,\n beforeEnter: (to, from, next) => {\n\n // スマホ縦画面・スマホ横画面・タブレット縦画面では設定一覧画面を表示する(画面サイズの関係)\n if (Utils.isSmartphoneVertical() || Utils.isSmartphoneHorizontal() || Utils.isTabletVertical()) {\n next(); // 通常通り遷移\n return;\n }\n\n // それ以外の画面サイズでは全般設定にリダイレクト\n next({path: '/settings/general/'});\n }\n },\n {\n path: '/settings/general',\n name: 'Settings General',\n component: SettingsGeneral,\n },\n {\n path: '/settings/account',\n name: 'Settings Account',\n component: SettingsAccount,\n },\n {\n path: '/settings/jikkyo',\n name: 'Settings Jikkyo',\n component: SettingsJikkyo,\n },\n {\n path: '/settings/twitter',\n name: 'Settings Twitter',\n component: SettingsTwitter,\n },\n {\n path: '/settings/environment',\n name: 'Settings Environment',\n component: SettingsEnvironment,\n },\n {\n path: '/login/',\n name: 'Login',\n component: Login,\n },\n {\n path: '/register/',\n name: 'Register',\n component: Register,\n },\n {\n path: '*',\n name: 'NotFound',\n component: NotFound,\n },\n ],\n\n // ページ遷移時のスクロールの挙動の設定\n // ref: https://v3.router.vuejs.org/ja/guide/advanced/scroll-behavior.html\n scrollBehavior (to, from, savedPosition) {\n if (savedPosition) {\n // 戻る/進むボタンが押されたときは保存されたスクロール位置を使う\n return savedPosition;\n } else {\n // それ以外は常に先頭にスクロールする\n return {x: 0, y: 0};\n }\n }\n});\n\nexport default router;\n","/* eslint-disable no-console */\n\nimport { register } from 'register-service-worker';\n\nif (process.env.NODE_ENV === 'production') {\n register(`${process.env.BASE_URL}service-worker.js`, {\n ready () {\n console.log(\n 'App is being served from cache by a service worker.\\n' +\n 'For more details, visit https://goo.gl/AFskqB'\n )\n },\n registered () {\n console.log('Service worker has been registered.')\n },\n cached () {\n console.log('Content has been cached for offline use.')\n },\n updatefound () {\n console.log('New content is downloading.')\n },\n updated () {\n console.log('New content is available; please refresh.')\n },\n offline () {\n console.log('No internet connection found. App is running in offline mode.')\n },\n error (error) {\n console.error('Error during service worker registration:', error)\n }\n });\n}\n","\nimport { Icon } from '@iconify/vue2';\nimport { polyfill as SeamlessScrollPolyfill } from \"seamless-scroll-polyfill\";\nimport Vue from 'vue';\nimport VueAxios from 'vue-axios';\nimport VueVirtualScroller from 'vue-virtual-scroller';\nimport 'vue-virtual-scroller/dist/vue-virtual-scroller.css';\nimport VuetifyMessageSnackbar from 'vuetify-message-snackbar';\nimport VTooltip from 'v-tooltip';\nimport 'v-tooltip/dist/v-tooltip.css';\n\nimport App from '@/App.vue';\nimport VTabItem from '@/components/VTabItem';\nimport VTabs from '@/components/VTabs';\nimport VTabsItems from '@/components/VTabsItems';\nimport axios from '@/plugins/axios';\nimport vuetify from '@/plugins/vuetify';\nimport router from '@/router';\nimport '@/service-worker';\nimport Utils from './utils';\n\n// スムーズスクロール周りの API の polyfill を適用\n// Element.scrollInfoView() のオプション指定を使うために必要\nSeamlessScrollPolyfill();\n\n// Production Tip を非表示にする\nVue.config.productionTip = false;\n\n// 常に Vue.js devtools を有効にする\nVue.config.devtools = true;\n\n// Axios を使う\nVue.use(VueAxios, axios);\n\n// vue-virtual-scroller を使う\nVue.use(VueVirtualScroller);\n\n// vuetify-message-snackbar を使う\n// マイナーな OSS(しかも中国語…)だけど、Snackbar を関数で呼びたかったのでちょうどよかった\n// ref: https://github.com/thinkupp/vuetify-message-snackbar\nVue.use(VuetifyMessageSnackbar, {\n // 画面上に配置しない\n top: false,\n // 画面下に配置する\n bottom: true,\n // デフォルトの背景色\n color: '#433532',\n // ダークテーマを適用する\n dark: true,\n // 影 (Elevation) の設定\n elevation: 8,\n // 2.5秒でタイムアウト\n timeout: 2500,\n // 要素が非表示になった後に DOM から要素を削除する\n\tautoRemove: true,\n // 閉じるボタンのテキスト\n\tcloseButtonContent: '閉じる',\n\t// Vuetify のインスタンス\n\tvuetifyInstance: vuetify,\n});\n\n// VTooltip を使う\n// タッチデバイスでは無効化する\n// ref: https://v-tooltip.netlify.app/guide/config.html#default-values\nconst trigger = Utils.isTouchDevice() ? [] : ['hover', 'focus', 'touch'];\nVTooltip.options.themes.tooltip.showTriggers = trigger;\nVTooltip.options.themes.tooltip.hideTriggers = trigger;\nVTooltip.options.themes.tooltip.delay.show = 0;\nVTooltip.options.offset = [0, 7];\nVue.use(VTooltip);\n\n// Iconify(アイコン)のグローバルコンポーネント\nVue.component('Icon', Icon);\n\n// VTabItem の挙動を改善するグローバルコンポーネント\nVue.component('v-tab-item-fix', VTabItem);\n\n// VTabs の挙動を改善するグローバルコンポーネント\nVue.component('v-tabs-fix', VTabs);\n\n// VTabsItems の挙動を改善するグローバルコンポーネント\nVue.component('v-tabs-items-fix', VTabsItems);\n\n// Vue を初期化\nnew Vue({\n router,\n vuetify,\n render: h => h(App),\n}).$mount('#app');\n\n// ログイン時かつ設定の同期が有効なとき、ページ遷移に関わらず、常に3秒おきにサーバーから設定を取得する\n// 初回のページレンダリングに間に合わないのは想定内(同期の完了を待つこともできるが、それだと表示速度が遅くなるのでしょうがない)\nwindow.setInterval(async () => {\n if (Utils.getAccessToken() !== null && Utils.getSettingsItem('sync_settings') === true) {\n Utils.syncServerSettingsToClient();\n }\n}, 3 * 1000); // 3秒おき\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\tid: moduleId,\n\t\tloaded: false,\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n\t// Flag the module as loaded\n\tmodule.loaded = true;\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","__webpack_require__.amdO = {};","var deferred = [];\n__webpack_require__.O = function(result, chunkIds, fn, priority) {\n\tif(chunkIds) {\n\t\tpriority = priority || 0;\n\t\tfor(var i = deferred.length; i > 0 && deferred[i - 1][2] > priority; i--) deferred[i] = deferred[i - 1];\n\t\tdeferred[i] = [chunkIds, fn, priority];\n\t\treturn;\n\t}\n\tvar notFulfilled = Infinity;\n\tfor (var i = 0; i < deferred.length; i++) {\n\t\tvar chunkIds = deferred[i][0];\n\t\tvar fn = deferred[i][1];\n\t\tvar priority = deferred[i][2];\n\t\tvar fulfilled = true;\n\t\tfor (var j = 0; j < chunkIds.length; j++) {\n\t\t\tif ((priority & 1 === 0 || notFulfilled >= priority) && Object.keys(__webpack_require__.O).every(function(key) { return __webpack_require__.O[key](chunkIds[j]); })) {\n\t\t\t\tchunkIds.splice(j--, 1);\n\t\t\t} else {\n\t\t\t\tfulfilled = false;\n\t\t\t\tif(priority < notFulfilled) notFulfilled = priority;\n\t\t\t}\n\t\t}\n\t\tif(fulfilled) {\n\t\t\tdeferred.splice(i--, 1)\n\t\t\tvar r = fn();\n\t\t\tif (r !== undefined) result = r;\n\t\t}\n\t}\n\treturn result;\n};","// getDefaultExport function for compatibility with non-harmony modules\n__webpack_require__.n = function(module) {\n\tvar getter = module && module.__esModule ?\n\t\tfunction() { return module['default']; } :\n\t\tfunction() { return module; };\n\t__webpack_require__.d(getter, { a: getter });\n\treturn getter;\n};","// define getter functions for harmony exports\n__webpack_require__.d = function(exports, definition) {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.hmd = function(module) {\n\tmodule = Object.create(module);\n\tif (!module.children) module.children = [];\n\tObject.defineProperty(module, 'exports', {\n\t\tenumerable: true,\n\t\tset: function() {\n\t\t\tthrow new Error('ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id);\n\t\t}\n\t});\n\treturn module;\n};","__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }","// define __esModule on exports\n__webpack_require__.r = function(exports) {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","// no baseURI\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t143: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n__webpack_require__.O.j = function(chunkId) { return installedChunks[chunkId] === 0; };\n\n// install a JSONP callback for chunk loading\nvar webpackJsonpCallback = function(parentChunkLoadingFunction, data) {\n\tvar chunkIds = data[0];\n\tvar moreModules = data[1];\n\tvar runtime = data[2];\n\t// add \"moreModules\" to the modules object,\n\t// then flag all \"chunkIds\" as loaded and fire callback\n\tvar moduleId, chunkId, i = 0;\n\tif(chunkIds.some(function(id) { return installedChunks[id] !== 0; })) {\n\t\tfor(moduleId in moreModules) {\n\t\t\tif(__webpack_require__.o(moreModules, moduleId)) {\n\t\t\t\t__webpack_require__.m[moduleId] = moreModules[moduleId];\n\t\t\t}\n\t\t}\n\t\tif(runtime) var result = runtime(__webpack_require__);\n\t}\n\tif(parentChunkLoadingFunction) parentChunkLoadingFunction(data);\n\tfor(;i < chunkIds.length; i++) {\n\t\tchunkId = chunkIds[i];\n\t\tif(__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) {\n\t\t\tinstalledChunks[chunkId][0]();\n\t\t}\n\t\tinstalledChunks[chunkId] = 0;\n\t}\n\treturn __webpack_require__.O(result);\n}\n\nvar chunkLoadingGlobal = self[\"webpackChunkKonomiTV\"] = self[\"webpackChunkKonomiTV\"] || [];\nchunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));\nchunkLoadingGlobal.push = webpackJsonpCallback.bind(null, chunkLoadingGlobal.push.bind(chunkLoadingGlobal));","// startup\n// Load entry module and return exports\n// This entry module depends on other loaded chunks and execution need to be delayed\nvar __webpack_exports__ = __webpack_require__.O(undefined, [998], function() { return __webpack_require__(4195); })\n__webpack_exports__ = __webpack_require__.O(__webpack_exports__);\n"],"names":["_vm","_c","script","component","render","staticRenderFns","VTabItem","h","props","name","this","computedTransition","on","beforeEnter","onBeforeTransition","afterEnter","onAfterTransition","enterCancelled","onTransitionCancelled","beforeLeave","afterLeave","leaveCancelled","enter","onEnter","genWindowItem","VTabsBar","data","items","methods","register","item","activeItem","internalIndex","push","sort","a","b","index_a","$slots","default","findIndex","element","$vnode","key","index_b","$on","onClick","mandatory","selectedValues","length","updateMandatory","updateItem","indexOf","undefined","updateInternalValue","unregister","constructor","super","options","call","VTabs","genBar","slider","style","height","convertToUnit","activeClass","centerActive","dark","light","optional","mobileBreakpoint","nextIcon","prevIcon","showArrows","value","internalValue","callSlider","change","val","ref","setTextColor","computedColor","setBackgroundColor","backgroundColor","$createElement","genSlider","VTabsItems","updateReverse","oldVal","itemsLength","lastIndex","continuous","Utils","localStorage","getItem","setItem","JSON","stringify","default_settings","settings","parse","new_settings","default_settings_key","Object","keys","syncClientSettingsToServer","force","getAccessToken","sync_settings","uploading_settings","sleep","server_settings","Vue","server_settings_key","server_settings_value","entries","error","sync_settings_key","sync_settings_keys","access_token","removeItem","test","navigator","userAgent","blob","filename","blob_url","URL","createObjectURL","link","document","createElement","download","href","click","revokeObjectURL","content","html_escape_table","replace","match","popupSizeWidth","popupSizeHeight","window","screen","posTop","posLeft","width","class_name","activeElement","classList","contains","matchMedia","matches","base","Math","floor","seconds","Promise","resolve","setTimeout","Date","now","prototype","toString","slice","toLowerCase","text","escapeHTML","pattern","process","location","protocol","host","pinned_channel_ids","showed_panel_last_time","selected_twitter_account_id","saved_twitter_hashtags","tv_streaming_quality","tv_data_saver_mode","tv_low_latency_mode","tv_show_superimpose","panel_display_state","tv_panel_active_tab","caption_font","always_border_caption_text","specify_caption_background_color","caption_background_color","capture_copy_to_clipboard","capture_save_mode","capture_caption_mode","comment_speed_rate","comment_font_size","comment_delay_time","close_comment_form_after_sending","muted_comment_keywords","muted_niconico_user_ids","mute_vulgar_comments","mute_abusive_discriminatory_prejudiced_comments","mute_big_size_comments","mute_fixed_comments","mute_colored_comments","mute_consecutive_same_characters_comments","fold_panel_after_sending_tweet","reset_hashtag_when_program_switches","auto_add_watching_channel_hashtag","twitter_active_tab","tweet_hashtag_position","tweet_capture_watermark_position","ChannelUtils","channel_id","is_pretty","result","groups","channel_type","toUpperCase","channel_force","channels_list","remocon_id","channel_type_pretty","channels","get","index","channel","getChannelType","previous_index","next_index","PlayerCaptureHandler","player","captured_callback","player_container","container","querySelector","insertAdjacentHTML","comment_capture_button","capture_button","canvas","OffscreenCanvas","canvas_context","getContext","alpha","desynchronized","willReadFrequently","async","video","videoWidth","videoHeight","with_comments","total_time","time","is_radiochannel","notice","danmaku","showing","addHighlight","filename_base","dayjs","format","filename_caption","caption_canvas","plugins","aribb24Caption","getRawCanvas","superimpose_canvas","aribb24Superimpose","is_caption_showing","isShowing","isPresent","is_superimpose_showing","caption_text","getTextContent","export_and_save","program","is_caption_composited","is_comment_composited","exportToBlob","console","log","mathFloor","setEXIFDataToCapture","includes","getSettingsItem","downloadBlobData","uploadCaptureToServer","capture_normal","capture_caption","image_bitmap","createImageBitmap","promises","drawImage","comments_image","createCommentsImage","drawComments","filename_real","program_present","close","all","bitmap_canvas","transferFromImageBitmap","removeHighlight","capture","copyBlobToClipboard","convertBlobToPng","add","remove","html","svg","trim","image","Image","src","encodeURIComponent","decode","comments_html","template","outerHTML","comment","querySelectorAll","position","getBoundingClientRect","left","replaceAll","commentsHTMLtoSVGImage","offsetWidth","offsetHeight","draw_scale_ratio","draw_height","convertToBlob","type","quality","reject","toBlob","Error","captured_playback_position","diff","start_time","json","captured_at","network_id","service_id","event_id","title","description","end_time","duration","datetime","exif","piexif","TagValues","ImageIFD","XResolution","YResolution","ResolutionUnit","YCbCrPositioning","DateTime","Software","version","XPComment","Buffer","ExifIFD","ExifVersion","ComponentsConfiguration","FlashpixVersion","ColorSpace","DateTimeOriginal","DateTimeDigitized","exif_string","dump","blob_string","reader","FileReader","onload","onerror","readAsBinaryString","blob_string_new","insert","buffer","Uint8Array","charCodeAt","Blob","form_data","FormData","append","headers","PlayerUtils","background_count","random","padStart","canPlayType","ProgramUtils","mark","pattern1","RegExp","pattern2","isSameOrAfter","isSameOrBefore","isBetween","pause_time_start","hour","minute","second","pause_time_end","pause_time_start_23","pause_time_end_23","default_value","progress","is_short","axios_instance","axios","interceptors","request","use","config","baseURL","Vuetify","VSnackbar","VBtn","VIcon","theme","themes","primary","secondary","twitter","lighten1","lighten2","gray","black","background","lighten3","darken1","darken2","darken3","customProperties","staticClass","attrs","model","callback","expression","Array","from","channels_type","_v","tab","class","rawName","id","staticStyle","directives","preventDefault","$event","removePinnedChannel","stopPropagation","domProps","_s","latest_version","is_update_available","version_info","components","Header","Navigation","is_loading","interval_ids","Map","created","update","residue_second","getTime","setInterval","beforeDestroy","interval_id","clearInterval","channels_response","filter","is_display","GR","set","BS","CS","CATV","SKY","STARDIGIO","updatePinnedChannelList","addPinnedChannel","splice","is_update_tab","pinned_channels","pinned_channel_id","pinned_channel_type","pinned_channel","find","has","isPinnedChannel","controlDisplayTimer","modifiers","api_base_url","backgroundImage","background_url","is_video_buffering","channel_previous","is_panel_display","shortcut_key_modal","shortcut_key_column_name","shortcut_key_column","shortcut_keys","key_name","IProgramDefault","detail","is_free","genre","video_type","video_codec","video_resolution","primary_audio_type","primary_audio_language","primary_audio_sampling_rate","secondary_audio_type","secondary_audio_language","secondary_audio_sampling_rate","IChannelDefault","transport_stream_id","channel_number","channel_name","channel_comment","is_subchannel","viewers","program_following","_l","getProgramTime","getProgramProgress","required","comment_mute_settings_modal","is_comment_list_dropdown_display","scopedSlots","fn","active","isTouchDevice","initialize_failed_message","is_manual_scroll","scrollCommentList","_setup","muted_comment_keyword","muted_comment_keyword_match_type","muted_niconico_user_id","prop","event","Boolean","interval_timer_id","map","keyword","user_id","setting_keys","setting_key","new_muted_comment_keywords","new_muted_niconico_user_ids","watch","deep","handler","setting_value","$emit","mute_vulgar_comments_pattern","mute_abusive_discriminatory_prejudiced_comments_pattern","CommentMuteSettings","is_auto_scrolling","user","comment_list","comment_list_element","comment_list_dropdown_top","comment_list_dropdown_comment","watch_session","comment_session","vpos_base_timestamp","keep_seat_interval_id","resize_observer","resize_observer_element","destroy","unobserve","new_channel","old_channel","$el","is_user_scrolling","onmousedown","x","clientX","clientWidth","onmouseup","on_user_scrolling","is_dragging","ontouchstart","ontouchend","ontouchmove","onwheel","onscroll","scrollTop","scrollHeight","initReserveObserver","comment_session_info","initWatchSession","unix","initCommentSession","message","watch_session_info","is_success","WebSocket","audience_token","addEventListener","send","messageServer","uri","threadId","vposBaseTime","yourPostKey","readyState","keepIntervalSec","code","waitTimeSec","disconnect_reason","onclose","reason","comment_list_buffer","is_received_initial_comment","message_server","thread_id","your_post_key","event_raw","thread","resultcode","ping","chat","yourpost","isMutedComment","color","size","mail","commands","split","command","getCommentColor","getCommentPosition","shift","comment_dict","no","date","my_post","visibilityState","paused","draw","onvisibilitychange","niconico_user_id","niconico_user_premium","color_table","position_table","vpos","onmessage","success","animation_timeout_id","on_resize","video_element","comment_area_element","clientHeight","letter_box_height","threshold","comment_area_vertical_margin","comment_area_width","comment_area_height","gcd","y","gcd_result","comment_area_height_aspect","transition","setProperty","clearTimeout","removeProperty","ResizeObserver","observe","smooth","scrollTo","top","behavior","startsWith","endsWith","addMutedKeywords","addMutedNiconicoUserIDs","displayCommentListDropdown","comment_list_wrapper_rect","$refs","comment_list_wrapper","comment_list_dropdown_height","comment_button_rect","currentTarget","_self","_setupProxy","$route","params","decorateProgramInfo","getAttribute","genre_index","major","middle","getChannelForceType","detail_text","detail_heading","URLtoLink","zoom_capture_modal","clickCapture","tweet_hashtag","is_tweet_hashtag_form_focused","is_hashtag_list_display","tweet_text","is_tweet_text_form_focused","is_logged_in_twitter","tweet_letter_count","twitter_account","hashtag","updateTweetLetterCount","formatHashtag","draggable","is_virtual_keyboard_display","is_logged_in","selected_twitter_account","is_twitter_account_list_display","editing","zoom_capture","captures","captures_element","tweet_captures","is_admin","niconico_user_name","twitter_accounts","created_at","updated_at","syncAccountInfo","some","twitter_account_index","image_url","old_channel_hashtag","getChannelHashtag","_a","response","status","clickAccountButton","fullscreenElement","exitFullscreen","$router","path","pasteClipboardData","clipboard_item","clipboardData","file","getAsFile","addCaptureList","updateSelectedTwitterAccount","selected","focused","$nextTick","context","font","fillStyle","shadowColor","shadowBlur","shadowOffsetX","shadowOffsetY","textAlign","textBaseline","fillText","from_hashtag_list","tweet_hashtag_array","channel_hashtag","join","tweet_capture","drawProgramTitleOnCapture","blur","screen_name","PLAYBACK_BUFFER_SEC_LOW_LATENCY","PLAYBACK_BUFFER_SEC","Channel","Comment","Program","Twitter","is_background_display","is_control_display","is_fullscreen","is_ime_composing","is_comment_send_just_did","control_interval_id","channel_next","romsounds_context","romsounds_buffers","eventsource","fullscreen_handler","capture_handler","shortcut_key_handler","shortcut_key_pressed_at","shortcut_key_list","left_column","icon","icon_height","shortcuts","right_column","virtualKeyboard","overlaysContent","ongeometrychange","target","boundingRect","init","AudioContext","url","audio_data","responseType","decodeAudioData","beforeRouteUpdate","to","next","getPreviousAndCurrentAndNextChannel","generatePlayerBackgroundURL","channel_response","channel_response_data","twitter_component","KonomiTVCanDestroy","initPlayer","initEventHandler","initCaptureHandler","removeEventListener","initShortcutKeyHandler","audioItem","mpegts","audioValue","textContent","tran","switchPrimaryAudio","delete","artwork","sizes","mediaSession","metadata","MediaMetadata","artist","setPositionState","playbackRate","setActionHandler","play","pause","is_player_event","is_touch_device","timeout","controller","hide","setting","isShow","show","playback_buffer_sec","DPlayer","lang","live","liveSyncMinBufferSize","loop","airplay","autoplay","hotkey","screenshot","volume","defaultQuality","qualities","hevc_prefix","isHEVCVideoSupported","speedRate","fontSize","apiBackend","read","sendComment","pluginOptions","enableWorker","liveSync","liveSyncMaxLatency","liveSyncTargetLatency","liveSyncPlaybackRate","aribb24","normalFont","forceStrokeColor","forceBackgroundColor","drcsReplacement","enableRawCanvas","useStrokeText","usePUA","PRACallback","state","resume","buffer_source_node","createBufferSource","gain_node","createGain","connect","destination","gain","start","subtitle","setAutoHide","commentInput","settingOriginPanel","settingOriginPanelHeight","settingBox","clipPath","fullscreen_container","fullScreen","isFullScreen","onfullscreenchange","webkitFullscreenElement","cancel","requestFullscreen","webkitRequestFullscreen","orientation","lock","catch","webkitExitFullscreen","unlock","on_play_or_pause","buffered","end","currentTime","sync","on_canplay","oncanplay","oncanplaythrough","get_playback_buffer_sec","round","current_playback_buffer_sec","current_volume","EventSource","clients_count","pictureInPictureElement","exitPictureInPicture","requestPictureInPicture","switchVideo","clear","tweet_form_element","tag","tagName","editable","is_repeat","repeat","focus","scrollLeft","ctrlKey","metaKey","shiftKey","altKey","switch_channel_type","switch_remocon_id","Number","switch_channel","getChannelFromRemoconID","focused_capture_index","focused_capture","focused_capture_element","parentElement","scrollIntoView","block","inline","toggle","pictureInPictureEnabled","pipButton","showDanmaku","showDanmakuToggle","checked","captureAndSave","is_destroy_player","exportSettings","import_settings_file","Base","is_form_dense","settings_json","settings_json_blob","$message","go","user_icon_blob","overrideServerSettingsFromClient","settings_username","settings_icon","$$v","settings_password_showing","settings_password","account_delete_confirm_dialog","settings_username_validation","settings_password_validation","sync_settings_dialog","sync_settings_json","server_sync_settings_json","syncAccountIcon","icon_response","update_type","validate","username","password","logout","$set","niconico_user_icon_url","user_id_slice","warning","authorization_url","popup_window","open","onMessage","closed","authorization_status","authorization_detail","loginTwitterAccount","current_twitter_account","password_showing","URLSearchParams","password_validation","username_validation","_m","VueRouter","router","mode","routes","redirect","TVHome","TVWatch","SettingsIndex","SettingsGeneral","SettingsAccount","SettingsJikkyo","SettingsTwitter","SettingsEnvironment","Login","Register","NotFound","scrollBehavior","savedPosition","ready","registered","cached","updatefound","updated","offline","SeamlessScrollPolyfill","VueAxios","VueVirtualScroller","VuetifyMessageSnackbar","bottom","elevation","autoRemove","closeButtonContent","vuetifyInstance","vuetify","trigger","VTooltip","Icon","App","$mount","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","exports","module","loaded","__webpack_modules__","m","amdO","deferred","O","chunkIds","priority","notFulfilled","Infinity","i","fulfilled","j","every","r","n","getter","__esModule","d","definition","o","defineProperty","enumerable","g","globalThis","Function","e","hmd","create","children","obj","hasOwnProperty","Symbol","toStringTag","installedChunks","chunkId","webpackJsonpCallback","parentChunkLoadingFunction","moreModules","runtime","chunkLoadingGlobal","self","forEach","bind","__webpack_exports__"],"sourceRoot":""} \ No newline at end of file diff --git a/client/dist/index.html b/client/dist/index.html index a16a72c3..82d63da5 100644 --- a/client/dist/index.html +++ b/client/dist/index.html @@ -1 +1 @@ -KonomiTV
\ No newline at end of file +KonomiTV
\ No newline at end of file diff --git a/client/dist/service-worker.js b/client/dist/service-worker.js index 95e1f99a..a05c637f 100644 --- a/client/dist/service-worker.js +++ b/client/dist/service-worker.js @@ -1,2 +1,2 @@ -if(!self.define){let s,e={};const a=(a,i)=>(a=new URL(a+".js",i).href,e[a]||new Promise((e=>{if("document"in self){const s=document.createElement("script");s.src=a,s.onload=e,document.head.appendChild(s)}else s=a,importScripts(a),e()})).then((()=>{let s=e[a];if(!s)throw new Error(`Module ${a} didn’t register its module`);return s})));self.define=(i,r)=>{const o=s||("document"in self?document.currentScript.src:"")||location.href;if(e[o])return;let d={};const n=s=>a(s,o),c={module:{uri:o},exports:d,require:n};e[o]=Promise.all(i.map((s=>c[s]||n(s)))).then((s=>(r(...s),d)))}}define(["./workbox-ec677914"],(function(s){"use strict";s.setCacheNameDetails({prefix:"KonomiTV"}),self.addEventListener("message",(s=>{s.data&&"SKIP_WAITING"===s.data.type&&self.skipWaiting()})),s.precacheAndRoute([{url:"/assets/css/app.918ddfe6.css",revision:null},{url:"/assets/css/chunk-vendors.8b5bf3c2.css",revision:null},{url:"/assets/images/account-icon-default.png",revision:"3840f879e0ddf77549f4035ae72e8f6b"},{url:"/assets/images/icon.svg",revision:"63abc49a99bd463af26e73cec607771d"},{url:"/assets/images/icons/apple-touch-icon.png",revision:"a1ff224fdbecfd10c117cd6172799b94"},{url:"/assets/images/icons/favicon-16px.png",revision:"66d1179e73198777a49235a76619a093"},{url:"/assets/images/icons/favicon-32px.png",revision:"85e6e77bb3362197cf564bf9b21ebe12"},{url:"/assets/images/icons/favicon.svg",revision:"1bf40917c217fd567119c219ebabe4b9"},{url:"/assets/images/icons/icon-192px.png",revision:"cc3f0142a77651214f66f0a725253521"},{url:"/assets/images/icons/icon-512px.png",revision:"37175521e6de680e90740ead2506f9fd"},{url:"/assets/images/icons/icon-maskable-192px.png",revision:"291866775902df321181d8dbc66c0d22"},{url:"/assets/images/icons/icon-maskable-512px.png",revision:"d105aac16603bc9e5349fba31bf71cfd"},{url:"/assets/images/logo.svg",revision:"83079d38a7a118e1c80fe28d139991d8"},{url:"/assets/images/player-backgrounds/01.jpg",revision:"14d74db9eb062b39dc128daeba77cb63"},{url:"/assets/images/player-backgrounds/02.jpg",revision:"98e077363a5eec17da30acef5038f924"},{url:"/assets/images/player-backgrounds/03.jpg",revision:"e75e4fc34090286e347cebf12c74b1b8"},{url:"/assets/images/player-backgrounds/04.jpg",revision:"714dd3c050c09a16236f2424c548c83f"},{url:"/assets/images/player-backgrounds/05.jpg",revision:"717125c34121b326e8f90773565f59ca"},{url:"/assets/images/player-backgrounds/06.jpg",revision:"aa3b22785383baf67ad6d53fee94ed1c"},{url:"/assets/images/player-backgrounds/07.jpg",revision:"dc9937f7a374b99981cb0d6c9a642e56"},{url:"/assets/images/player-backgrounds/08.jpg",revision:"b6cedbf1da35814fbf784591380fde62"},{url:"/assets/images/player-backgrounds/09.jpg",revision:"e989450375d6954b37b066a1cec3ad35"},{url:"/assets/images/player-backgrounds/10.jpg",revision:"417128b6120078997139b44ee2c73dbd"},{url:"/assets/images/player-backgrounds/11.jpg",revision:"8c173e2d5980e09dc7b0e36e97b8f189"},{url:"/assets/images/player-backgrounds/12.jpg",revision:"97231a4813562229cc55d4516cb85350"},{url:"/assets/js/app.4d3619bb.js",revision:null},{url:"/assets/js/chunk-vendors.58b61fe5.js",revision:null},{url:"/assets/romsounds/01.wav",revision:"4187b218123ba4ff5de4e48ad3ee7778"},{url:"/assets/romsounds/02.wav",revision:"a6e40866a7da83a5a6a77c62686b2fa6"},{url:"/assets/romsounds/03.wav",revision:"30f5d254ec6c10bc37f0584e6cb2d0ed"},{url:"/assets/romsounds/04.wav",revision:"626bbd8f569576f18fba702740d731c5"},{url:"/assets/romsounds/05.wav",revision:"dbfbd7f4e2e7670f47dac6f52de3fd98"},{url:"/assets/romsounds/06.wav",revision:"5e68fa08d3621ab451a6daf1d52803b9"},{url:"/assets/romsounds/07.wav",revision:"b17f57be56bb2141660d2a18a497cf69"},{url:"/assets/romsounds/08.wav",revision:"88b1bed69315e657ecaa6e7cdaa032c5"},{url:"/assets/romsounds/09.wav",revision:"39dd16fc0f20240d5347448f9703e42a"},{url:"/assets/romsounds/10.wav",revision:"e0a34f995f013843fb5e552c2dc78a03"},{url:"/assets/romsounds/11.wav",revision:"4b6fd4f4bddcee2ad1987e4c82da9476"},{url:"/assets/romsounds/12.wav",revision:"e0e67a86607a7ad8457c4adefbac50e9"},{url:"/assets/romsounds/13.wav",revision:"d6c8ef577228462c7b90e677396ca652"},{url:"/assets/romsounds/14.wav",revision:"d72e6dd844260adc6db71a04d3763d07"},{url:"/index.html",revision:"1e2d1dedf1ff077e017efdc0461acad5"},{url:"/manifest.json",revision:"c1a7314b537716c7416fe56300a68a54"},{url:"/robots.txt",revision:"d98e9ec5fafad691048cbb603b5b0e71"}],{}),s.cleanupOutdatedCaches()})); +if(!self.define){let s,e={};const a=(a,i)=>(a=new URL(a+".js",i).href,e[a]||new Promise((e=>{if("document"in self){const s=document.createElement("script");s.src=a,s.onload=e,document.head.appendChild(s)}else s=a,importScripts(a),e()})).then((()=>{let s=e[a];if(!s)throw new Error(`Module ${a} didn’t register its module`);return s})));self.define=(i,r)=>{const o=s||("document"in self?document.currentScript.src:"")||location.href;if(e[o])return;let n={};const c=s=>a(s,o),d={module:{uri:o},exports:n,require:c};e[o]=Promise.all(i.map((s=>d[s]||c(s)))).then((s=>(r(...s),n)))}}define(["./workbox-ec677914"],(function(s){"use strict";s.setCacheNameDetails({prefix:"KonomiTV"}),self.addEventListener("message",(s=>{s.data&&"SKIP_WAITING"===s.data.type&&self.skipWaiting()})),s.precacheAndRoute([{url:"/assets/css/app.379ffadb.css",revision:null},{url:"/assets/css/chunk-vendors.8b5bf3c2.css",revision:null},{url:"/assets/images/account-icon-default.png",revision:"3840f879e0ddf77549f4035ae72e8f6b"},{url:"/assets/images/icon.svg",revision:"63abc49a99bd463af26e73cec607771d"},{url:"/assets/images/icons/apple-touch-icon.png",revision:"a1ff224fdbecfd10c117cd6172799b94"},{url:"/assets/images/icons/favicon-16px.png",revision:"66d1179e73198777a49235a76619a093"},{url:"/assets/images/icons/favicon-32px.png",revision:"85e6e77bb3362197cf564bf9b21ebe12"},{url:"/assets/images/icons/favicon.svg",revision:"1bf40917c217fd567119c219ebabe4b9"},{url:"/assets/images/icons/icon-192px.png",revision:"cc3f0142a77651214f66f0a725253521"},{url:"/assets/images/icons/icon-512px.png",revision:"37175521e6de680e90740ead2506f9fd"},{url:"/assets/images/icons/icon-maskable-192px.png",revision:"291866775902df321181d8dbc66c0d22"},{url:"/assets/images/icons/icon-maskable-512px.png",revision:"d105aac16603bc9e5349fba31bf71cfd"},{url:"/assets/images/logo.svg",revision:"83079d38a7a118e1c80fe28d139991d8"},{url:"/assets/images/player-backgrounds/01.jpg",revision:"14d74db9eb062b39dc128daeba77cb63"},{url:"/assets/images/player-backgrounds/02.jpg",revision:"98e077363a5eec17da30acef5038f924"},{url:"/assets/images/player-backgrounds/03.jpg",revision:"e75e4fc34090286e347cebf12c74b1b8"},{url:"/assets/images/player-backgrounds/04.jpg",revision:"714dd3c050c09a16236f2424c548c83f"},{url:"/assets/images/player-backgrounds/05.jpg",revision:"717125c34121b326e8f90773565f59ca"},{url:"/assets/images/player-backgrounds/06.jpg",revision:"aa3b22785383baf67ad6d53fee94ed1c"},{url:"/assets/images/player-backgrounds/07.jpg",revision:"dc9937f7a374b99981cb0d6c9a642e56"},{url:"/assets/images/player-backgrounds/08.jpg",revision:"b6cedbf1da35814fbf784591380fde62"},{url:"/assets/images/player-backgrounds/09.jpg",revision:"e989450375d6954b37b066a1cec3ad35"},{url:"/assets/images/player-backgrounds/10.jpg",revision:"417128b6120078997139b44ee2c73dbd"},{url:"/assets/images/player-backgrounds/11.jpg",revision:"8c173e2d5980e09dc7b0e36e97b8f189"},{url:"/assets/images/player-backgrounds/12.jpg",revision:"97231a4813562229cc55d4516cb85350"},{url:"/assets/js/app.66d563f0.js",revision:null},{url:"/assets/js/chunk-vendors.58b61fe5.js",revision:null},{url:"/assets/romsounds/01.wav",revision:"4187b218123ba4ff5de4e48ad3ee7778"},{url:"/assets/romsounds/02.wav",revision:"a6e40866a7da83a5a6a77c62686b2fa6"},{url:"/assets/romsounds/03.wav",revision:"30f5d254ec6c10bc37f0584e6cb2d0ed"},{url:"/assets/romsounds/04.wav",revision:"626bbd8f569576f18fba702740d731c5"},{url:"/assets/romsounds/05.wav",revision:"dbfbd7f4e2e7670f47dac6f52de3fd98"},{url:"/assets/romsounds/06.wav",revision:"5e68fa08d3621ab451a6daf1d52803b9"},{url:"/assets/romsounds/07.wav",revision:"b17f57be56bb2141660d2a18a497cf69"},{url:"/assets/romsounds/08.wav",revision:"88b1bed69315e657ecaa6e7cdaa032c5"},{url:"/assets/romsounds/09.wav",revision:"39dd16fc0f20240d5347448f9703e42a"},{url:"/assets/romsounds/10.wav",revision:"e0a34f995f013843fb5e552c2dc78a03"},{url:"/assets/romsounds/11.wav",revision:"4b6fd4f4bddcee2ad1987e4c82da9476"},{url:"/assets/romsounds/12.wav",revision:"e0e67a86607a7ad8457c4adefbac50e9"},{url:"/assets/romsounds/13.wav",revision:"d6c8ef577228462c7b90e677396ca652"},{url:"/assets/romsounds/14.wav",revision:"d72e6dd844260adc6db71a04d3763d07"},{url:"/index.html",revision:"07336738ec2621b6e9081cba8b850067"},{url:"/manifest.json",revision:"c1a7314b537716c7416fe56300a68a54"},{url:"/robots.txt",revision:"d98e9ec5fafad691048cbb603b5b0e71"}],{}),s.cleanupOutdatedCaches()})); //# sourceMappingURL=service-worker.js.map diff --git a/client/dist/service-worker.js.map b/client/dist/service-worker.js.map index 689b8585..e335bbb8 100644 --- a/client/dist/service-worker.js.map +++ b/client/dist/service-worker.js.map @@ -1 +1 @@ -{"version":3,"file":"service-worker.js","sources":["../../../Users/tsukumi/AppData/Local/Temp/b66819da6444b038b2d168f88bf7e87a/service-worker.js"],"sourcesContent":["import {setCacheNameDetails as workbox_core_setCacheNameDetails} from 'C:/Develop/KonomiTV/client/node_modules/workbox-core/setCacheNameDetails.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from 'C:/Develop/KonomiTV/client/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from 'C:/Develop/KonomiTV/client/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\nworkbox_core_setCacheNameDetails({prefix: \"KonomiTV\"});\n\n\nself.addEventListener('message', (event) => {\n if (event.data && event.data.type === 'SKIP_WAITING') {\n self.skipWaiting();\n }\n});\n\n\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"/assets/css/app.918ddfe6.css\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/css/chunk-vendors.8b5bf3c2.css\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/images/account-icon-default.png\",\n \"revision\": \"3840f879e0ddf77549f4035ae72e8f6b\"\n },\n {\n \"url\": \"/assets/images/icon.svg\",\n \"revision\": \"63abc49a99bd463af26e73cec607771d\"\n },\n {\n \"url\": \"/assets/images/icons/apple-touch-icon.png\",\n \"revision\": \"a1ff224fdbecfd10c117cd6172799b94\"\n },\n {\n \"url\": \"/assets/images/icons/favicon-16px.png\",\n \"revision\": \"66d1179e73198777a49235a76619a093\"\n },\n {\n \"url\": \"/assets/images/icons/favicon-32px.png\",\n \"revision\": \"85e6e77bb3362197cf564bf9b21ebe12\"\n },\n {\n \"url\": \"/assets/images/icons/favicon.svg\",\n \"revision\": \"1bf40917c217fd567119c219ebabe4b9\"\n },\n {\n \"url\": \"/assets/images/icons/icon-192px.png\",\n \"revision\": \"cc3f0142a77651214f66f0a725253521\"\n },\n {\n \"url\": \"/assets/images/icons/icon-512px.png\",\n \"revision\": \"37175521e6de680e90740ead2506f9fd\"\n },\n {\n \"url\": \"/assets/images/icons/icon-maskable-192px.png\",\n \"revision\": \"291866775902df321181d8dbc66c0d22\"\n },\n {\n \"url\": \"/assets/images/icons/icon-maskable-512px.png\",\n \"revision\": \"d105aac16603bc9e5349fba31bf71cfd\"\n },\n {\n \"url\": \"/assets/images/logo.svg\",\n \"revision\": \"83079d38a7a118e1c80fe28d139991d8\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/01.jpg\",\n \"revision\": \"14d74db9eb062b39dc128daeba77cb63\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/02.jpg\",\n \"revision\": \"98e077363a5eec17da30acef5038f924\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/03.jpg\",\n \"revision\": \"e75e4fc34090286e347cebf12c74b1b8\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/04.jpg\",\n \"revision\": \"714dd3c050c09a16236f2424c548c83f\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/05.jpg\",\n \"revision\": \"717125c34121b326e8f90773565f59ca\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/06.jpg\",\n \"revision\": \"aa3b22785383baf67ad6d53fee94ed1c\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/07.jpg\",\n \"revision\": \"dc9937f7a374b99981cb0d6c9a642e56\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/08.jpg\",\n \"revision\": \"b6cedbf1da35814fbf784591380fde62\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/09.jpg\",\n \"revision\": \"e989450375d6954b37b066a1cec3ad35\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/10.jpg\",\n \"revision\": \"417128b6120078997139b44ee2c73dbd\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/11.jpg\",\n \"revision\": \"8c173e2d5980e09dc7b0e36e97b8f189\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/12.jpg\",\n \"revision\": \"97231a4813562229cc55d4516cb85350\"\n },\n {\n \"url\": \"/assets/js/app.4d3619bb.js\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/js/chunk-vendors.58b61fe5.js\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/romsounds/01.wav\",\n \"revision\": \"4187b218123ba4ff5de4e48ad3ee7778\"\n },\n {\n \"url\": \"/assets/romsounds/02.wav\",\n \"revision\": \"a6e40866a7da83a5a6a77c62686b2fa6\"\n },\n {\n \"url\": \"/assets/romsounds/03.wav\",\n \"revision\": \"30f5d254ec6c10bc37f0584e6cb2d0ed\"\n },\n {\n \"url\": \"/assets/romsounds/04.wav\",\n \"revision\": \"626bbd8f569576f18fba702740d731c5\"\n },\n {\n \"url\": \"/assets/romsounds/05.wav\",\n \"revision\": \"dbfbd7f4e2e7670f47dac6f52de3fd98\"\n },\n {\n \"url\": \"/assets/romsounds/06.wav\",\n \"revision\": \"5e68fa08d3621ab451a6daf1d52803b9\"\n },\n {\n \"url\": \"/assets/romsounds/07.wav\",\n \"revision\": \"b17f57be56bb2141660d2a18a497cf69\"\n },\n {\n \"url\": \"/assets/romsounds/08.wav\",\n \"revision\": \"88b1bed69315e657ecaa6e7cdaa032c5\"\n },\n {\n \"url\": \"/assets/romsounds/09.wav\",\n \"revision\": \"39dd16fc0f20240d5347448f9703e42a\"\n },\n {\n \"url\": \"/assets/romsounds/10.wav\",\n \"revision\": \"e0a34f995f013843fb5e552c2dc78a03\"\n },\n {\n \"url\": \"/assets/romsounds/11.wav\",\n \"revision\": \"4b6fd4f4bddcee2ad1987e4c82da9476\"\n },\n {\n \"url\": \"/assets/romsounds/12.wav\",\n \"revision\": \"e0e67a86607a7ad8457c4adefbac50e9\"\n },\n {\n \"url\": \"/assets/romsounds/13.wav\",\n \"revision\": \"d6c8ef577228462c7b90e677396ca652\"\n },\n {\n \"url\": \"/assets/romsounds/14.wav\",\n \"revision\": \"d72e6dd844260adc6db71a04d3763d07\"\n },\n {\n \"url\": \"/index.html\",\n \"revision\": \"1e2d1dedf1ff077e017efdc0461acad5\"\n },\n {\n \"url\": \"/manifest.json\",\n \"revision\": \"c1a7314b537716c7416fe56300a68a54\"\n },\n {\n \"url\": \"/robots.txt\",\n \"revision\": \"d98e9ec5fafad691048cbb603b5b0e71\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();\n\n\n\n\n\n\n\n"],"names":["workbox_core_setCacheNameDetails","prefix","self","addEventListener","event","data","type","skipWaiting","workbox_precaching_precacheAndRoute","url","revision","workbox_precaching_cleanupOutdatedCaches"],"mappings":"0nBAkBAA,EAAAA,oBAAiC,CAACC,OAAQ,aAG1CC,KAAKC,iBAAiB,WAAYC,IAC5BA,EAAMC,MAA4B,iBAApBD,EAAMC,KAAKC,MAC3BJ,KAAKK,aACN,IAWHC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,+BACPC,SAAY,MAEd,CACED,IAAO,yCACPC,SAAY,MAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,4CACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,+CACPC,SAAY,oCAEd,CACED,IAAO,+CACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,MAEd,CACED,IAAO,uCACPC,SAAY,MAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,iBACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,qCAEb,CAjLgC,GAkLnCC,EAAAA"} \ No newline at end of file +{"version":3,"file":"service-worker.js","sources":["../../../Users/tsukumi/AppData/Local/Temp/cfa6ac42d2e08f40243521c6a05567f3/service-worker.js"],"sourcesContent":["import {setCacheNameDetails as workbox_core_setCacheNameDetails} from 'C:/Develop/KonomiTV/client/node_modules/workbox-core/setCacheNameDetails.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from 'C:/Develop/KonomiTV/client/node_modules/workbox-precaching/precacheAndRoute.mjs';\nimport {cleanupOutdatedCaches as workbox_precaching_cleanupOutdatedCaches} from 'C:/Develop/KonomiTV/client/node_modules/workbox-precaching/cleanupOutdatedCaches.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\nworkbox_core_setCacheNameDetails({prefix: \"KonomiTV\"});\n\n\nself.addEventListener('message', (event) => {\n if (event.data && event.data.type === 'SKIP_WAITING') {\n self.skipWaiting();\n }\n});\n\n\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"/assets/css/app.379ffadb.css\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/css/chunk-vendors.8b5bf3c2.css\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/images/account-icon-default.png\",\n \"revision\": \"3840f879e0ddf77549f4035ae72e8f6b\"\n },\n {\n \"url\": \"/assets/images/icon.svg\",\n \"revision\": \"63abc49a99bd463af26e73cec607771d\"\n },\n {\n \"url\": \"/assets/images/icons/apple-touch-icon.png\",\n \"revision\": \"a1ff224fdbecfd10c117cd6172799b94\"\n },\n {\n \"url\": \"/assets/images/icons/favicon-16px.png\",\n \"revision\": \"66d1179e73198777a49235a76619a093\"\n },\n {\n \"url\": \"/assets/images/icons/favicon-32px.png\",\n \"revision\": \"85e6e77bb3362197cf564bf9b21ebe12\"\n },\n {\n \"url\": \"/assets/images/icons/favicon.svg\",\n \"revision\": \"1bf40917c217fd567119c219ebabe4b9\"\n },\n {\n \"url\": \"/assets/images/icons/icon-192px.png\",\n \"revision\": \"cc3f0142a77651214f66f0a725253521\"\n },\n {\n \"url\": \"/assets/images/icons/icon-512px.png\",\n \"revision\": \"37175521e6de680e90740ead2506f9fd\"\n },\n {\n \"url\": \"/assets/images/icons/icon-maskable-192px.png\",\n \"revision\": \"291866775902df321181d8dbc66c0d22\"\n },\n {\n \"url\": \"/assets/images/icons/icon-maskable-512px.png\",\n \"revision\": \"d105aac16603bc9e5349fba31bf71cfd\"\n },\n {\n \"url\": \"/assets/images/logo.svg\",\n \"revision\": \"83079d38a7a118e1c80fe28d139991d8\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/01.jpg\",\n \"revision\": \"14d74db9eb062b39dc128daeba77cb63\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/02.jpg\",\n \"revision\": \"98e077363a5eec17da30acef5038f924\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/03.jpg\",\n \"revision\": \"e75e4fc34090286e347cebf12c74b1b8\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/04.jpg\",\n \"revision\": \"714dd3c050c09a16236f2424c548c83f\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/05.jpg\",\n \"revision\": \"717125c34121b326e8f90773565f59ca\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/06.jpg\",\n \"revision\": \"aa3b22785383baf67ad6d53fee94ed1c\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/07.jpg\",\n \"revision\": \"dc9937f7a374b99981cb0d6c9a642e56\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/08.jpg\",\n \"revision\": \"b6cedbf1da35814fbf784591380fde62\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/09.jpg\",\n \"revision\": \"e989450375d6954b37b066a1cec3ad35\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/10.jpg\",\n \"revision\": \"417128b6120078997139b44ee2c73dbd\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/11.jpg\",\n \"revision\": \"8c173e2d5980e09dc7b0e36e97b8f189\"\n },\n {\n \"url\": \"/assets/images/player-backgrounds/12.jpg\",\n \"revision\": \"97231a4813562229cc55d4516cb85350\"\n },\n {\n \"url\": \"/assets/js/app.66d563f0.js\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/js/chunk-vendors.58b61fe5.js\",\n \"revision\": null\n },\n {\n \"url\": \"/assets/romsounds/01.wav\",\n \"revision\": \"4187b218123ba4ff5de4e48ad3ee7778\"\n },\n {\n \"url\": \"/assets/romsounds/02.wav\",\n \"revision\": \"a6e40866a7da83a5a6a77c62686b2fa6\"\n },\n {\n \"url\": \"/assets/romsounds/03.wav\",\n \"revision\": \"30f5d254ec6c10bc37f0584e6cb2d0ed\"\n },\n {\n \"url\": \"/assets/romsounds/04.wav\",\n \"revision\": \"626bbd8f569576f18fba702740d731c5\"\n },\n {\n \"url\": \"/assets/romsounds/05.wav\",\n \"revision\": \"dbfbd7f4e2e7670f47dac6f52de3fd98\"\n },\n {\n \"url\": \"/assets/romsounds/06.wav\",\n \"revision\": \"5e68fa08d3621ab451a6daf1d52803b9\"\n },\n {\n \"url\": \"/assets/romsounds/07.wav\",\n \"revision\": \"b17f57be56bb2141660d2a18a497cf69\"\n },\n {\n \"url\": \"/assets/romsounds/08.wav\",\n \"revision\": \"88b1bed69315e657ecaa6e7cdaa032c5\"\n },\n {\n \"url\": \"/assets/romsounds/09.wav\",\n \"revision\": \"39dd16fc0f20240d5347448f9703e42a\"\n },\n {\n \"url\": \"/assets/romsounds/10.wav\",\n \"revision\": \"e0a34f995f013843fb5e552c2dc78a03\"\n },\n {\n \"url\": \"/assets/romsounds/11.wav\",\n \"revision\": \"4b6fd4f4bddcee2ad1987e4c82da9476\"\n },\n {\n \"url\": \"/assets/romsounds/12.wav\",\n \"revision\": \"e0e67a86607a7ad8457c4adefbac50e9\"\n },\n {\n \"url\": \"/assets/romsounds/13.wav\",\n \"revision\": \"d6c8ef577228462c7b90e677396ca652\"\n },\n {\n \"url\": \"/assets/romsounds/14.wav\",\n \"revision\": \"d72e6dd844260adc6db71a04d3763d07\"\n },\n {\n \"url\": \"/index.html\",\n \"revision\": \"07336738ec2621b6e9081cba8b850067\"\n },\n {\n \"url\": \"/manifest.json\",\n \"revision\": \"c1a7314b537716c7416fe56300a68a54\"\n },\n {\n \"url\": \"/robots.txt\",\n \"revision\": \"d98e9ec5fafad691048cbb603b5b0e71\"\n }\n], {});\nworkbox_precaching_cleanupOutdatedCaches();\n\n\n\n\n\n\n\n"],"names":["workbox_core_setCacheNameDetails","prefix","self","addEventListener","event","data","type","skipWaiting","workbox_precaching_precacheAndRoute","url","revision","workbox_precaching_cleanupOutdatedCaches"],"mappings":"0nBAkBAA,EAAAA,oBAAiC,CAACC,OAAQ,aAG1CC,KAAKC,iBAAiB,WAAYC,IAC5BA,EAAMC,MAA4B,iBAApBD,EAAMC,KAAKC,MAC3BJ,KAAKK,aACN,IAWHC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,+BACPC,SAAY,MAEd,CACED,IAAO,yCACPC,SAAY,MAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,4CACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,+CACPC,SAAY,oCAEd,CACED,IAAO,+CACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,MAEd,CACED,IAAO,uCACPC,SAAY,MAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,iBACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,qCAEb,CAjLgC,GAkLnCC,EAAAA"} \ No newline at end of file diff --git a/client/package.json b/client/package.json index 574a61fe..c5997e2c 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "KonomiTV", - "version": "0.6.0", + "version": "0.6.1", "private": true, "scripts": { "dev": "concurrently --raw \"npm:dev-https\" \"vue-cli-service serve\"", diff --git a/client/src/components/Panel/Channel.vue b/client/src/components/Panel/Channel.vue index 76b0b55c..12d16dc4 100644 --- a/client/src/components/Panel/Channel.vue +++ b/client/src/components/Panel/Channel.vue @@ -145,8 +145,8 @@ _::-webkit-full-page-media, _:future, :root background:var(--v-background-base); z-index: 1; @include smartphone-horizontal { - padding-bottom: 0px; - margin-top: 12px; + padding-bottom: 8px; + margin-top: 0px; } .channels-tab__item { diff --git a/client/src/views/Settings/Index.vue b/client/src/views/Settings/Index.vue index 398840ac..e05b711c 100644 --- a/client/src/views/Settings/Index.vue +++ b/client/src/views/Settings/Index.vue @@ -90,6 +90,8 @@ export default Vue.extend({ } .settings-navigation { + transform: none !important; + visibility: visible !important; h1 { @include smartphone-horizontal { diff --git a/installer/KonomiTV-Installer.py b/installer/KonomiTV-Installer.py index cefe66ea..494c9e0d 100644 --- a/installer/KonomiTV-Installer.py +++ b/installer/KonomiTV-Installer.py @@ -25,7 +25,7 @@ # インストール or アップデート対象の KonomiTV バージョン -INSTALLER_VERSION = '0.6.0' +INSTALLER_VERSION = '0.6.1' # main() 関数内のすべての処理は管理者権限 (Windows) / root 権限 (Linux) で実行される def main(): diff --git a/server/app/constants.py b/server/app/constants.py index 99b066c7..0ca0083e 100644 --- a/server/app/constants.py +++ b/server/app/constants.py @@ -12,7 +12,7 @@ # バージョン -VERSION = '0.6.0' +VERSION = '0.6.1' # ベースディレクトリ BASE_DIR = Path(__file__).resolve().parent.parent diff --git a/server/app/tasks/LiveEncodingTask.py b/server/app/tasks/LiveEncodingTask.py index bb5af810..557195c7 100644 --- a/server/app/tasks/LiveEncodingTask.py +++ b/server/app/tasks/LiveEncodingTask.py @@ -756,6 +756,10 @@ def SubWriter(): livestream.setStatus('Offline', 'この時間は放送を休止しています。') else: livestream.setStatus('Offline', 'チューナーへの接続に失敗しました。チューナー側に何らかの問題があるかもしれません。') + if tsreadex is not None: + tsreadex.kill() + if encoder is not None: + encoder.kill() break # エンコーダープロセスが終了していたらループを抜ける