Responsive positioning of transcript

Responsive positioning of transcript

<template>

  <MediaPlayerFullscreen
    ref="fullscreen"
    class="fill-space"
    @changeFullscreen="isFullscreen = $event"
  >
    <div
      ref="wrapper"
      :class="[
        'wrapper',
        {
          'transcript-visible': isShowingTranscript,
          'transcript-wrap': windowIsPortrait || windowIsSmall,
        },
        $computedClass(progressStyle)
      ]"
    >
      <div v-show="loading" class="loading-space fill-space">
        <KCircularLoader
          class="loader"
          :delay="true"
        />
      </div>

      <video
        v-if="isVideo"
        ref="player"
        class="video-js custom-skin vjs-big-play-centered vjs-show-big-play-button-on-pause"
      >
        <template v-for="video in videoSources">
          <source
            :key="video.storage_url"
            :src="video.storage_url"
            :type="`video/${video.extension}`"
          >
        </template>
        <template v-for="track in trackSources">
          <track
            :key="track.storage_url"
            kind="captions"
            :src="track.storage_url"
            :srclang="track.lang.id"
            :label="track.lang.lang_name"
            :default="isDefaultTrack(track.lang.id)"
          >
          <track
            :key="track.storage_url + '.metadata'"
            kind="metadata"
            :src="track.storage_url"
            :srclang="track.lang.id"
            :label="track.lang.lang_name"
          >
        </template>
      </video>

      <audio v-else ref="player" class="video-js custom-skin">
        <template v-for="audio in audioSources">
          <source
            :key="audio.storage_url"
            :src="audio.storage_url"
            :type="`audio/${audio.extension}`"
          >
        </template>
      </audio>

      <MediaPlayerTranscript
        ref="transcript"
        :defaultLangCode="defaultLangCode"
        @toggleTranscript="isShowingTranscript = $event"
      />
    </div>
  </MediaPlayerFullscreen>

</template>

<script>

  import vue from 'kolibri.lib.vue';
  import videojs from 'video.js';
  import throttle from 'lodash/throttle';

  import { languageIdToCode } from 'kolibri.utils.i18n';
  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';
  import KCircularLoader from 'kolibri.coreVue.components.KCircularLoader';
  import ResponsiveElement from 'kolibri.coreVue.mixins.responsiveElement';
  import ResponsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow';
  import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';

  import Settings from './settings';
  import { ReplayButton, ForwardButton } from './customButtons';
  import MediaPlayerFullscreen from './MediaPlayerFullscreen';
  import MimicFullscreenToggle from './MediaPlayerFullscreen/mimicFullscreenToggle';
  import MediaPlayerTranscript from './MediaPlayerTranscript';
  import TranscriptButton from './MediaPlayerTranscript/transcriptButton';

  import audioIconPoster from './audio-icon-poster.svg';

  const GlobalLangCode = vue.locale;
  const PLAYER_CONFIG = {
    fluid: false,
    fill: true,
    controls: true,
    textTrackDisplay: true,
    bigPlayButton: true,
    preload: 'metadata',
    playbackRates: [0.5, 1.0, 1.25, 1.5, 2.0],
    controlBar: {
      children: [
        { name: 'PlayToggle' },
        { name: 'ReplayButton' },
        { name: 'ForwardButton' },
        { name: 'CurrentTimeDisplay' },
        { name: 'ProgressControl' },
        { name: 'TimeDivider' },
        { name: 'DurationDisplay' },
        {
          name: 'VolumePanel',
          inline: false,
        },
        { name: 'PlaybackRateMenuButton' },
        { name: 'CaptionsButton' },
        { name: 'MimicFullscreenToggle' },
        { name: 'TranscriptButton' },
      ],
    },
    language: GlobalLangCode,
  };

  const componentsToRegister = {
    MimicFullscreenToggle,
    ReplayButton,
    ForwardButton,
    TranscriptButton,
  };

  Object.entries(componentsToRegister).forEach(([name, component]) =>
    videojs.registerComponent(name, component)
  );

  export default {
    name: 'MediaPlayerIndex',

    components: { KCircularLoader, MediaPlayerFullscreen, MediaPlayerTranscript },

    mixins: [ResponsiveWindow, ResponsiveElement, contentRendererMixin, themeMixin],

    data: () => ({
      dummyTime: 0,
      progressStartingPoint: 0,
      lastUpdateTime: 0,
      loading: true,
      playerVolume: 1.0,
      playerMuted: false,
      playerRate: 1.0,
      defaultLangCode: GlobalLangCode,
      videoLangCode: GlobalLangCode,
      updateContentStateInterval: null,
      isFullscreen: false,
      isShowingTranscript: false,
    }),

    computed: {
      posterSources() {
        const posterFileExtensions = ['png', 'jpg'];
        return this.thumbnailFiles.filter(file =>
          posterFileExtensions.some(ext => ext === file.extension)
        );
      },
      audioPoster() {
        if (this.posterSources.length) {
          return this.posterSources[0].storage_url;
        }
        return audioIconPoster;
      },
      videoSources() {
        const videoFileExtensions = ['mp4', 'webm', 'ogg'];
        return this.files.filter(file => videoFileExtensions.some(ext => ext === file.extension));
      },
      audioSources() {
        const audioFileExtensions = ['mp3'];
        return this.files.filter(file => audioFileExtensions.some(ext => ext === file.extension));
      },
      trackSources() {
        const trackFileExtensions = ['vtt'];
        return this.supplementaryFiles.filter(file =>
          trackFileExtensions.some(ext => ext === file.extension)
        );
      },
      isVideo() {
        return this.videoSources.length;
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return 0;
      },
      progressStyle() {
        return {
          '.vjs-play-progress': {
            backgroundColor: this.$coreActionNormal,
            '::before': {
              color: this.$coreActionNormal,
            },
          },
        };
      },
    },
    created() {
      this.settings = new Settings({
        playerVolume: this.playerVolume,
        playerMuted: this.playerMuted,
        playerRate: this.playerRate,
        videoLangCode: this.videoLangCode,
      });

      this.videoLangCode = this.settings.videoLangCode;
    },
    mounted() {
      this.initPlayer();
      window.addEventListener('resize', this.throttledResizePlayer);
    },
    beforeDestroy() {
      clearInterval(this.updateContentStateInterval);
      this.updateContentState();

      this.$emit('stopTracking');
      window.removeEventListener('resize', this.throttledResizePlayer);
      this.player.dispose();
    },
    methods: {
      isDefaultTrack(langCode) {
        const shortLangCode = languageIdToCode(langCode);
        const shortGlobalLangCode = languageIdToCode(this.videoLangCode);

        return shortLangCode === shortGlobalLangCode;
      },
      initPlayer() {
        this.$nextTick(() => {
          this.player = videojs(this.$refs.player, this.getPlayerConfig(), this.handleReadyPlayer);
          this.$refs.fullscreen.setPlayer(this.player);
          this.$refs.transcript.setPlayer(this.player);
        });
      },
      getPlayerConfig() {
        const videojsConfig = Object.assign({}, PLAYER_CONFIG, {
          languages: {
            [GlobalLangCode]: {
              Play: this.$tr('play'),
              Pause: this.$tr('pause'),
              Replay: this.$tr('replay'),
              Forward: this.$tr('forward'),
              'Current Time': this.$tr('currentTime'),
              'Duration Time': this.$tr('durationTime'),
              Loaded: this.$tr('loaded'),
              Progress: this.$tr('progress'),
              'Progress Bar': this.$tr('progressBar'),
              Fullscreen: this.$tr('fullscreen'),
              'Non-Fullscreen': this.$tr('nonFullscreen'),
              Mute: this.$tr('mute'),
              Unmute: this.$tr('unmute'),
              'Playback Rate': this.$tr('playbackRate'),
              Captions: this.$tr('captions'),
              'captions off': this.$tr('captionsOff'),
              Transcript: this.$tr('transcript'),
              'Transcript off': this.$tr('transcriptOff'),
              'Volume Level': this.$tr('volumeLevel'),
              'A network error caused the media download to fail part-way.': this.$tr(
                'networkError'
              ),
              'The media could not be loaded, either because the server or network failed or because the format is not supported.': this.$tr(
                'formatError'
              ),
              'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.': this.$tr(
                'corruptionOrSupportError'
              ),
              'No compatible source was found for this media.': this.$tr('sourceError'),
              'The media is encrypted and we do not have the keys to decrypt it.': this.$tr(
                'encryptionError'
              ),
            },
          },
        });

        if (!this.isVideo) {
          videojsConfig.poster = this.audioPoster;
        }

        return videojsConfig;
      },
      handleReadyPlayer() {
        const startTime = this.savedLocation >= this.player.duration() ? 0 : this.savedLocation;
        this.player.currentTime(startTime);
        this.player.play();

        this.player.on('play', () => {
          this.focusOnPlayControl();
          this.setPlayState(true);
        });
        this.player.on('pause', () => {
          this.focusOnPlayControl();
          this.setPlayState(false);
          this.updateContentState();
        });
        this.player.on('timeupdate', this.updateTime);
        this.player.on('seeking', this.handleSeek);
        this.player.on('volumechange', this.throttledUpdateVolume);
        this.player.on('ratechange', this.updateRate);
        this.player.on('texttrackchange', this.updateLang);
        this.player.on('ended', () => this.setPlayState(false));

        this.$watch('elementWidth', this.updatePlayerSizeClass);
        this.updatePlayerSizeClass();
        this.resizePlayer();

        this.useSavedSettings();
        this.loading = false;
        this.$refs.player.tabIndex = -1;

        this.updateContentStateInterval = setInterval(this.updateContentState, 30000);
      },
      resizePlayer() {
        if (this.isFullscreen) {
          this.$refs.wrapper.style.height = `100vh`;
          return;
        }

        const aspectRatio = 16 / 9;
        const adjustedHeight = this.$refs.wrapper.clientWidth * (1 / aspectRatio);

        this.$refs.wrapper.style.height = `${adjustedHeight}px`;
      },
      throttledResizePlayer: throttle(function resizePlayer() {
        this.resizePlayer();
      }, 300),

      throttledUpdateVolume: throttle(function updateVolume() {
        this.updateVolume();
      }, 1000),

      updateVolume() {
        this.settings.playerVolume = this.player.volume();
        this.settings.playerMuted = this.player.muted();
      },

      updateRate() {
        this.settings.playerRate = this.player.playbackRate();
      },

      getTextTracks() {
        return Array.from(this.player.textTracks());
      },

      updateLang() {
        const currentTrack = this.getTextTracks().find(track => track.mode === 'showing');
        if (currentTrack) {
          this.settings.videoLangCode = currentTrack.language;
        }
      },

      useSavedSettings() {
        this.playerVolume = this.settings.playerVolume;
        this.playerMuted = this.settings.playerMuted;
        this.playerRate = this.settings.playerRate;
        this.player.volume(this.playerVolume);
        this.player.muted(this.playerMuted);
        this.player.playbackRate(this.playerRate);
      },

      focusOnPlayControl() {
        this.$refs.wrapper.getElementsByClassName('vjs-play-control')[0].focus();
      },
      handleSeek() {
        // record progress before updating the times,
        // to capture any progress that happened pre-seeking
        this.recordProgress();

        // now, update all the timestamps to set the new time location
        // as the baseline starting point
        this.dummyTime = this.player.currentTime();
        this.lastUpdateTime = this.dummyTime;
        this.progressStartingPoint = this.dummyTime;
      },
      updateTime() {
        // skip out of here if we're currently seeking,
        // so we don't update this.dummyTime before calculating old progress
        if (this.player.seeking()) {
          return;
        }
        this.dummyTime = this.player.currentTime();
        if (this.dummyTime - this.lastUpdateTime >= 5) {
          this.recordProgress();
          this.lastUpdateTime = this.dummyTime;
        }
      },
      setPlayState(state) {
        // avoid recording progress if we're currently seeking,
        // as timers are in an intermediate state
        if (!this.player.seeking()) {
          this.recordProgress();
        }
        if (state === true) {
          this.$emit('startTracking');
        } else {
          this.$emit('stopTracking');
        }
      },
      recordProgress() {
        this.$emit(
          'updateProgress',
          Math.max(
            0,
            (this.dummyTime - this.progressStartingPoint) / Math.floor(this.player.duration())
          )
        );
        this.progressStartingPoint = this.dummyTime;
      },
      updatePlayerSizeClass() {
        this.player.removeClass('player-medium');
        this.player.removeClass('player-small');
        this.player.removeClass('player-tiny');

        if (this.elementWidth < 600) {
          this.player.addClass('player-medium');
        }
        if (this.elementWidth < 480) {
          this.player.addClass('player-small');
        }
        if (this.elementWidth < 360) {
          this.player.addClass('player-tiny');
        }
      },

      updateContentState() {
        const currentLocation = this.player.currentTime();
        let contentState;
        if (this.extraFields) {
          contentState = {
            ...this.extraFields.contentState,
            savedLocation: currentLocation || this.savedLocation,
          };
        } else {
          contentState = { savedLocation: currentLocation || this.savedLocation };
        }
        this.$emit('updateContentState', contentState);
      },
    },
    $trs: {
      replay: 'Go back 10 seconds',
      // Pulled from https://github.com/videojs/video.js/blob/master/lang/en.json
      forward: 'Go forward 10 seconds',
      play: 'Play',
      pause: 'Pause',
      currentTime: 'Current time',
      durationTime: 'Duration time',
      loaded: 'Loaded',
      progress: 'Progress',
      progressBar: 'Progress bar',
      fullscreen: 'Fullscreen',
      nonFullscreen: 'Non-fullscreen',
      mute: 'Mute',
      unmute: 'Unmute',
      playbackRate: 'Playback rate',
      captions: 'Captions',
      captionsOff: 'Captions off',
      transcript: 'Transcript',
      transcriptOff: 'Transcript off',
      volumeLevel: 'Volume level',
      networkError: 'A network error caused the media download to fail part-way',
      formatError:
        'The media could not be loaded, either because the server or network failed or because the format is not supported',
      corruptionOrSupportError:
        'The media playback was aborted due to a corruption problem or because the media used features your browser did not support',
      sourceError: 'No compatible source was found for this media',
      encryptionError: 'The media is encrypted and we do not have the keys to decrypt it',
    },
  };

</script>

<style lang="scss" scoped>

  // Unable to reference the videojs using require since videojs doesn't have good webpack support
  @import './videojs-style/video-js.min.css';
  // Custom build icons.
  @import './videojs-style/videojs-font/css/videojs-icons.css';
  @import '~kolibri.styles.definitions';

  /* 16:10 ratio gives video a little breathing remove, even though using it's 16:9 */
  $transcript-wrap-fill-height: 100vw * 10 / 16;
  $transcript-wrap-height: 250px;

  .wrapper {
    box-sizing: content-box;
    max-width: 100%;
    max-height: 562px;
    transition: padding-bottom $core-time ease;
  }

  .wrapper.transcript-visible.transcript-wrap {
    padding-bottom: $transcript-wrap-height;
  }

  .fill-space {
    position: relative;
    width: 100%;
    height: 100%;
  }

  .loading-space {
    box-sizing: border-box;
    padding-top: calc(100% * 9 / 16);
  }

  .loader {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }

  .media-player-transcript {
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 0;
    box-sizing: border-box;
  }

  .wrapper:not(.transcript-wrap) .media-player-transcript {
    top: 0;
    width: 33.333%;
  }

  .wrapper.transcript-wrap .media-player-transcript {
    left: 0;
    height: $transcript-wrap-height;
  }

  .normalize-fullscreen,
  .mimic-fullscreen {
    .wrapper {
      max-height: none;
    }

    .wrapper.transcript-visible.transcript-wrap .media-player-transcript {
      height: calc(100% - (#{$transcript-wrap-fill-height}));
    }

    .wrapper.transcript-visible.transcript-wrap .video-js.vjs-fill {
      height: calc(#{$transcript-wrap-fill-height});
    }
  }

  /***** PLAYER OVERRIDES *****/

  /* !!rtl:begin:ignore */

  /** COLOR PALLETTE **/
  $video-player-color: #212121;
  // tint if $video-player-color = black-ish, shade if $video-player-color = white-ish
  $video-player-color-2: tint($video-player-color, 7%);
  $video-player-color-3: tint($video-player-color, 15%);
  $video-player-font-color: white;

  $video-player-font-size: 12px;

  .video-js.vjs-fill {
    z-index: 1;
    transition: width $core-time ease;
  }

  .transcript-visible:not(.transcript-wrap) > .video-js.vjs-fill {
    width: 66.666%;
  }

  /* Hide control bar when playing & inactive */
  /deep/ .vjs-has-started.vjs-playing.vjs-user-inactive {
    .vjs-control-bar {
      visibility: hidden;
    }
  }

  /*** CUSTOM VIDEOJS SKIN ***/
  /deep/ .custom-skin {
    $button-height-normal: 40px;
    $button-font-size-normal: 24px;

    @include font-family-noto;

    font-size: $video-player-font-size;
    color: $video-player-font-color;

    /* Sliders */
    .vjs-slider {
      background-color: $video-player-color-2;
    }

    /* Seek Bar */
    .vjs-progress-control {
      height: initial;
      visibility: inherit;
      opacity: inherit;

      .vjs-progress-holder {
        height: 8px;
        margin-right: 16px;
        margin-left: 16px;

        .vjs-load-progress {
          div {
            background: $video-player-color-3;
          }
        }

        .vjs-play-progress {
          &::before {
            top: -5px;
            font-size: 18px;
          }
        }
      }
    }

    /* Control Bar */
    .vjs-control-bar {
      display: flex;
      height: $button-height-normal;
      background-color: $video-player-color;
    }

    /* Fixes volume panel appearing on hover. */
    .vjs-volume-vertical {
      display: none;
    }

    .vjs-volume-panel-vertical {
      &:hover {
        .vjs-volume-vertical {
          display: block;
        }
      }
    }

    .vjs-volume-level {
      background-color: $video-player-font-color;
    }

    /* Buttons */
    .vjs-button {
      .vjs-icon-placeholder {
        &::before {
          font-size: $button-font-size-normal;
          line-height: $button-height-normal;
        }
      }
    }

    /* Replay & Forward Buttons */
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      font-family: VideoJS; // override our global noto fonts with more specificity
      &::before {
        font-size: $button-font-size-normal;
        line-height: $button-height-normal;
      }
    }

    .vjs-big-play-button {
      position: absolute;
      top: 50%;
      left: 50%;
      width: $button-height-normal * 2;
      height: $button-height-normal * 2;
      margin: 0;
      font-size: $button-font-size-normal * 2;
      line-height: $button-height-normal * 2;
      background-color: $video-player-color;
      border: 0;
      border-radius: 50%;
      transform: translate(-50%, -50%);
    }

    .vjs-volume-panel {
      margin-left: auto;
    }

    /* Transcript button */
    .vjs-button-transcript img {
      max-width: 20px;
    }

    .vjs-transcript-visible > .vjs-transcript {
      display: block;
    }

    .vjs-transcript-visible > .vjs-tech,
    .vjs-transcript-visible > .vjs-modal-dialog,
    .vjs-transcript-visible > .vjs-text-track-display,
    .vjs-transcript-visible > .vjs-text-track-settings,
    .vjs-transcript-visible > .vjs-control-bar {
      right: auto;
      width: calc(100% - 330px);
    }

    /* Menus */
    .vjs-menu {
      li {
        padding: 8px;
        font-size: $video-player-font-size;
        background-color: $video-player-color;

        &:focus,
        &:hover {
          background-color: $video-player-color-3;
        }
      }

      li.vjs-selected {
        font-weight: bold;
        color: $video-player-font-color;
        background-color: $video-player-color-2;

        &:focus,
        &:hover {
          background-color: $video-player-color-3;
        }
      }
    }

    .vjs-menu-content {
      @include font-family-noto;
    }

    .vjs-volume-control {
      background-color: $video-player-color;
    }

    .vjs-playback-rate .vjs-menu {
      min-width: 4em;
    }

    /* Time */
    .vjs-current-time {
      display: block;
      padding-right: 0;

      .vjs-current-time-display {
        font-size: $video-player-font-size;
        line-height: $button-height-normal;
      }
    }

    .vjs-duration {
      display: block;
      padding-left: 0;
      .vjs-duration-display {
        font-size: $video-player-font-size;
        line-height: $button-height-normal;
      }
    }

    .vjs-time-divider {
      padding: 0;
      text-align: center;
    }

    /* Rate Button */
    .vjs-playback-rate-value {
      font-size: 20px;
      line-height: $button-height-normal;
    }

    /* Captions Settings */
    .vjs-texttrack-settings {
      display: none;
    }
  }

  /*** MEDIUM: < 600px ***/
  /deep/ .player-medium {
    /* Seek bar moves up. */
    .vjs-progress-control {
      position: absolute;
      top: -16px;
      right: 0;
      left: 0;
      width: auto;
    }

    /* Time divider is displayed. */
    .vjs-time-divider {
      display: block;
    }
  }

  /*** SMALL: < 480px ***/
  /deep/ .player-small {
    $button-height-small: 40px;
    $button-font-size-normal: 24px;

    /* Control bar buttons increase size. */
    .vjs-control-bar {
      height: $button-height-small;
    }

    .vjs-button {
      .vjs-icon-placeholder {
        &::before {
          line-height: $button-height-small;
        }
      }
    }

    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      &::before {
        line-height: $button-height-small;
      }
    }

    /* Play, replay, and forward buttons move up. */
    .vjs-play-control,
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      position: absolute;
      top: -75px;
      width: $button-height-small;
      height: $button-height-small;
      background-color: $video-player-color;
      border-radius: 50%;
      transform: translate(-50%, -50%);
    }

    .vjs-big-play-button {
      display: none;
    }

    &.vjs-show-big-play-button-on-pause {
      .vjs-big-play-button {
        display: none;
      }
    }

    /* Play button in center. */
    .vjs-play-control {
      left: 50%;
    }

    /* Replay play button on left. */
    .vjs-icon-replay_10 {
      left: 33%;
    }

    /* Forward button on right */
    .vjs-icon-forward_10 {
      left: 66%;
    }

    /* Adjust rate button text */
    .vjs-playback-rate-value {
      line-height: $button-height-small;
    }

    /* Adjust time text */
    .vjs-current-time {
      .vjs-current-time-display {
        line-height: $button-height-small;
      }
    }

    .vjs-duration {
      .vjs-duration-display {
        line-height: $button-height-small;
      }
    }

    .vjs-time-divider {
      line-height: $button-height-small;
    }
  }

  /*** TINY: < 360px ***/
  /deep/ .player-tiny {
    /* Time divider is hidden */
    .vjs-time-divider {
      display: none;
    }

    /* Time duration is hidden */
    .vjs-duration {
      display: none;
    }

    /* Adjust play, replay, and forward buttons positioning. */
    .vjs-play-control,
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      top: -45px;
    }

    /* Adjust replay button position. */
    .vjs-icon-replay_10 {
      left: 25%;
    }

    /* Adjust forward button position. */
    .vjs-icon-forward_10 {
      left: 75%;
    }
  }

  /* !!rtl:end:ignore */

</style>

<template>

  <MediaPlayerFullscreen
    ref="fullscreen"
    class="fill-space"
    @changeFullscreen="isFullscreen = $event"
  >
    <div
      ref="wrapper"
      :class="[
        'wrapper',
        {
          'transcript-visible': isShowingTranscript,
          'transcript-wrap': windowIsPortrait || windowIsSmall,
        },
        $computedClass(progressStyle)
      ]"
    >
      <div v-show="loading" class="loading-space fill-space">
        <KCircularLoader
          class="loader"
          :delay="true"
        />
      </div>

      <video
        v-if="isVideo"
        ref="player"
        class="video-js custom-skin vjs-big-play-centered vjs-show-big-play-button-on-pause"
      >
        <template v-for="video in videoSources">
          <source
            :key="video.storage_url"
            :src="video.storage_url"
            :type="`video/${video.extension}`"
          >
        </template>
        <template v-for="track in trackSources">
          <track
            :key="track.storage_url"
            kind="captions"
            :src="track.storage_url"
            :srclang="track.lang.id"
            :label="track.lang.lang_name"
            :default="isDefaultTrack(track.lang.id)"
          >
          <track
            :key="track.storage_url + '.metadata'"
            kind="metadata"
            :src="track.storage_url"
            :srclang="track.lang.id"
            :label="track.lang.lang_name"
          >
        </template>
      </video>

      <audio v-else ref="player" class="video-js custom-skin">
        <template v-for="audio in audioSources">
          <source
            :key="audio.storage_url"
            :src="audio.storage_url"
            :type="`audio/${audio.extension}`"
          >
        </template>
      </audio>

      <MediaPlayerTranscript
        ref="transcript"
        :defaultLangCode="defaultLangCode"
        @toggleTranscript="isShowingTranscript = $event"
      />
    </div>
  </MediaPlayerFullscreen>

</template>

<script>

  import vue from 'kolibri.lib.vue';
  import videojs from 'video.js';
  import throttle from 'lodash/throttle';

  import { languageIdToCode } from 'kolibri.utils.i18n';
  import themeMixin from 'kolibri.coreVue.mixins.themeMixin';
  import KCircularLoader from 'kolibri.coreVue.components.KCircularLoader';
  import ResponsiveElement from 'kolibri.coreVue.mixins.responsiveElement';
  import ResponsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow';
  import contentRendererMixin from 'kolibri.coreVue.mixins.contentRendererMixin';

  import Settings from './settings';
  import { ReplayButton, ForwardButton } from './customButtons';
  import MediaPlayerFullscreen from './MediaPlayerFullscreen';
  import MimicFullscreenToggle from './MediaPlayerFullscreen/mimicFullscreenToggle';
  import MediaPlayerTranscript from './MediaPlayerTranscript';
  import TranscriptButton from './MediaPlayerTranscript/transcriptButton';

  import audioIconPoster from './audio-icon-poster.svg';

  const GlobalLangCode = vue.locale;
  const PLAYER_CONFIG = {
    fluid: false,
    fill: true,
    controls: true,
    textTrackDisplay: true,
    bigPlayButton: true,
    preload: 'metadata',
    playbackRates: [0.5, 1.0, 1.25, 1.5, 2.0],
    controlBar: {
      children: [
        { name: 'PlayToggle' },
        { name: 'ReplayButton' },
        { name: 'ForwardButton' },
        { name: 'CurrentTimeDisplay' },
        { name: 'ProgressControl' },
        { name: 'TimeDivider' },
        { name: 'DurationDisplay' },
        {
          name: 'VolumePanel',
          inline: false,
        },
        { name: 'PlaybackRateMenuButton' },
        { name: 'CaptionsButton' },
        { name: 'MimicFullscreenToggle' },
        { name: 'TranscriptButton' },
      ],
    },
    language: GlobalLangCode,
  };

  const componentsToRegister = {
    MimicFullscreenToggle,
    ReplayButton,
    ForwardButton,
    TranscriptButton,
  };

  Object.entries(componentsToRegister).forEach(([name, component]) =>
    videojs.registerComponent(name, component)
  );

  export default {
    name: 'MediaPlayerIndex',

    components: { KCircularLoader, MediaPlayerFullscreen, MediaPlayerTranscript },

    mixins: [ResponsiveWindow, ResponsiveElement, contentRendererMixin, themeMixin],

    data: () => ({
      dummyTime: 0,
      progressStartingPoint: 0,
      lastUpdateTime: 0,
      loading: true,
      playerVolume: 1.0,
      playerMuted: false,
      playerRate: 1.0,
      defaultLangCode: GlobalLangCode,
      videoLangCode: GlobalLangCode,
      updateContentStateInterval: null,
      isFullscreen: false,
      isShowingTranscript: false,
    }),

    computed: {
      posterSources() {
        const posterFileExtensions = ['png', 'jpg'];
        return this.thumbnailFiles.filter(file =>
          posterFileExtensions.some(ext => ext === file.extension)
        );
      },
      audioPoster() {
        if (this.posterSources.length) {
          return this.posterSources[0].storage_url;
        }
        return audioIconPoster;
      },
      videoSources() {
        const videoFileExtensions = ['mp4', 'webm', 'ogg'];
        return this.files.filter(file => videoFileExtensions.some(ext => ext === file.extension));
      },
      audioSources() {
        const audioFileExtensions = ['mp3'];
        return this.files.filter(file => audioFileExtensions.some(ext => ext === file.extension));
      },
      trackSources() {
        const trackFileExtensions = ['vtt'];
        return this.supplementaryFiles.filter(file =>
          trackFileExtensions.some(ext => ext === file.extension)
        );
      },
      isVideo() {
        return this.videoSources.length;
      },
      savedLocation() {
        if (this.extraFields && this.extraFields.contentState) {
          return this.extraFields.contentState.savedLocation;
        }
        return 0;
      },
      progressStyle() {
        return {
          '.vjs-play-progress': {
            backgroundColor: this.$coreActionNormal,
            '::before': {
              color: this.$coreActionNormal,
            },
          },
        };
      },
    },
    created() {
      this.settings = new Settings({
        playerVolume: this.playerVolume,
        playerMuted: this.playerMuted,
        playerRate: this.playerRate,
        videoLangCode: this.videoLangCode,
      });

      this.videoLangCode = this.settings.videoLangCode;
    },
    mounted() {
      this.initPlayer();
      window.addEventListener('resize', this.throttledResizePlayer);
    },
    beforeDestroy() {
      clearInterval(this.updateContentStateInterval);
      this.updateContentState();

      this.$emit('stopTracking');
      window.removeEventListener('resize', this.throttledResizePlayer);
      this.player.dispose();
    },
    methods: {
      isDefaultTrack(langCode) {
        const shortLangCode = languageIdToCode(langCode);
        const shortGlobalLangCode = languageIdToCode(this.videoLangCode);

        return shortLangCode === shortGlobalLangCode;
      },
      initPlayer() {
        this.$nextTick(() => {
          this.player = videojs(this.$refs.player, this.getPlayerConfig(), this.handleReadyPlayer);
          this.$refs.fullscreen.setPlayer(this.player);
          this.$refs.transcript.setPlayer(this.player);
        });
      },
      getPlayerConfig() {
        const videojsConfig = Object.assign({}, PLAYER_CONFIG, {
          languages: {
            [GlobalLangCode]: {
              Play: this.$tr('play'),
              Pause: this.$tr('pause'),
              Replay: this.$tr('replay'),
              Forward: this.$tr('forward'),
              'Current Time': this.$tr('currentTime'),
              'Duration Time': this.$tr('durationTime'),
              Loaded: this.$tr('loaded'),
              Progress: this.$tr('progress'),
              'Progress Bar': this.$tr('progressBar'),
              Fullscreen: this.$tr('fullscreen'),
              'Non-Fullscreen': this.$tr('nonFullscreen'),
              Mute: this.$tr('mute'),
              Unmute: this.$tr('unmute'),
              'Playback Rate': this.$tr('playbackRate'),
              Captions: this.$tr('captions'),
              'captions off': this.$tr('captionsOff'),
              Transcript: this.$tr('transcript'),
              'Transcript off': this.$tr('transcriptOff'),
              'Volume Level': this.$tr('volumeLevel'),
              'A network error caused the media download to fail part-way.': this.$tr(
                'networkError'
              ),
              'The media could not be loaded, either because the server or network failed or because the format is not supported.': this.$tr(
                'formatError'
              ),
              'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.': this.$tr(
                'corruptionOrSupportError'
              ),
              'No compatible source was found for this media.': this.$tr('sourceError'),
              'The media is encrypted and we do not have the keys to decrypt it.': this.$tr(
                'encryptionError'
              ),
            },
          },
        });

        if (!this.isVideo) {
          videojsConfig.poster = this.audioPoster;
        }

        return videojsConfig;
      },
      handleReadyPlayer() {
        const startTime = this.savedLocation >= this.player.duration() ? 0 : this.savedLocation;
        this.player.currentTime(startTime);
        this.player.play();

        this.player.on('play', () => {
          this.focusOnPlayControl();
          this.setPlayState(true);
        });
        this.player.on('pause', () => {
          this.focusOnPlayControl();
          this.setPlayState(false);
          this.updateContentState();
        });
        this.player.on('timeupdate', this.updateTime);
        this.player.on('seeking', this.handleSeek);
        this.player.on('volumechange', this.throttledUpdateVolume);
        this.player.on('ratechange', this.updateRate);
        this.player.on('texttrackchange', this.updateLang);
        this.player.on('ended', () => this.setPlayState(false));

        this.$watch('elementWidth', this.updatePlayerSizeClass);
        this.updatePlayerSizeClass();
        this.resizePlayer();

        this.useSavedSettings();
        this.loading = false;
        this.$refs.player.tabIndex = -1;

        this.updateContentStateInterval = setInterval(this.updateContentState, 30000);
      },
      resizePlayer() {
        if (this.isFullscreen) {
          this.$refs.wrapper.style.height = `100vh`;
          return;
        }

        const aspectRatio = 16 / 9;
        const adjustedHeight = this.$refs.wrapper.clientWidth * (1 / aspectRatio);

        this.$refs.wrapper.style.height = `${adjustedHeight}px`;
      },
      throttledResizePlayer: throttle(function resizePlayer() {
        this.resizePlayer();
      }, 300),

      throttledUpdateVolume: throttle(function updateVolume() {
        this.updateVolume();
      }, 1000),

      updateVolume() {
        this.settings.playerVolume = this.player.volume();
        this.settings.playerMuted = this.player.muted();
      },

      updateRate() {
        this.settings.playerRate = this.player.playbackRate();
      },

      getTextTracks() {
        return Array.from(this.player.textTracks());
      },

      updateLang() {
        const currentTrack = this.getTextTracks().find(track => track.mode === 'showing');
        if (currentTrack) {
          this.settings.videoLangCode = currentTrack.language;
        }
      },

      useSavedSettings() {
        this.playerVolume = this.settings.playerVolume;
        this.playerMuted = this.settings.playerMuted;
        this.playerRate = this.settings.playerRate;
        this.player.volume(this.playerVolume);
        this.player.muted(this.playerMuted);
        this.player.playbackRate(this.playerRate);
      },

      focusOnPlayControl() {
        this.$refs.wrapper.getElementsByClassName('vjs-play-control')[0].focus();
      },
      handleSeek() {
        // record progress before updating the times,
        // to capture any progress that happened pre-seeking
        this.recordProgress();

        // now, update all the timestamps to set the new time location
        // as the baseline starting point
        this.dummyTime = this.player.currentTime();
        this.lastUpdateTime = this.dummyTime;
        this.progressStartingPoint = this.dummyTime;
      },
      updateTime() {
        // skip out of here if we're currently seeking,
        // so we don't update this.dummyTime before calculating old progress
        if (this.player.seeking()) {
          return;
        }
        this.dummyTime = this.player.currentTime();
        if (this.dummyTime - this.lastUpdateTime >= 5) {
          this.recordProgress();
          this.lastUpdateTime = this.dummyTime;
        }
      },
      setPlayState(state) {
        // avoid recording progress if we're currently seeking,
        // as timers are in an intermediate state
        if (!this.player.seeking()) {
          this.recordProgress();
        }
        if (state === true) {
          this.$emit('startTracking');
        } else {
          this.$emit('stopTracking');
        }
      },
      recordProgress() {
        this.$emit(
          'updateProgress',
          Math.max(
            0,
            (this.dummyTime - this.progressStartingPoint) / Math.floor(this.player.duration())
          )
        );
        this.progressStartingPoint = this.dummyTime;
      },
      updatePlayerSizeClass() {
        this.player.removeClass('player-medium');
        this.player.removeClass('player-small');
        this.player.removeClass('player-tiny');

        if (this.elementWidth < 600) {
          this.player.addClass('player-medium');
        }
        if (this.elementWidth < 480) {
          this.player.addClass('player-small');
        }
        if (this.elementWidth < 360) {
          this.player.addClass('player-tiny');
        }
      },

      updateContentState() {
        const currentLocation = this.player.currentTime();
        let contentState;
        if (this.extraFields) {
          contentState = {
            ...this.extraFields.contentState,
            savedLocation: currentLocation || this.savedLocation,
          };
        } else {
          contentState = { savedLocation: currentLocation || this.savedLocation };
        }
        this.$emit('updateContentState', contentState);
      },
    },
    $trs: {
      replay: 'Go back 10 seconds',
      // Pulled from https://github.com/videojs/video.js/blob/master/lang/en.json
      forward: 'Go forward 10 seconds',
      play: 'Play',
      pause: 'Pause',
      currentTime: 'Current time',
      durationTime: 'Duration time',
      loaded: 'Loaded',
      progress: 'Progress',
      progressBar: 'Progress bar',
      fullscreen: 'Fullscreen',
      nonFullscreen: 'Non-fullscreen',
      mute: 'Mute',
      unmute: 'Unmute',
      playbackRate: 'Playback rate',
      captions: 'Captions',
      captionsOff: 'Captions off',
      transcript: 'Transcript',
      transcriptOff: 'Transcript off',
      volumeLevel: 'Volume level',
      networkError: 'A network error caused the media download to fail part-way',
      formatError:
        'The media could not be loaded, either because the server or network failed or because the format is not supported',
      corruptionOrSupportError:
        'The media playback was aborted due to a corruption problem or because the media used features your browser did not support',
      sourceError: 'No compatible source was found for this media',
      encryptionError: 'The media is encrypted and we do not have the keys to decrypt it',
    },
  };

</script>

<style lang="scss" scoped>

  // Unable to reference the videojs using require since videojs doesn't have good webpack support
  @import './videojs-style/video-js.min.css';
  // Custom build icons.
  @import './videojs-style/videojs-font/css/videojs-icons.css';
  @import '~kolibri.styles.definitions';

  $transcript-wrap-height: 250px;

  .wrapper {
    box-sizing: content-box;
    max-width: 100%;
    max-height: 562px;
    transition: padding-bottom $core-time ease;
  }

  .normalize-fullscreen .wrapper,
  .mimic-fullscreen .wrapper {
    max-height: none;
  }

  .wrapper.transcript-visible.transcript-wrap {
    padding-bottom: $transcript-wrap-height;
  }

  .fill-space {
    position: relative;
    width: 100%;
    height: 100%;
  }

  .loading-space {
    box-sizing: border-box;
    padding-top: calc(100% * 9 / 16);
  }

  .loader {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }

  .media-player-transcript {
    position: absolute;
    right: 0;
    bottom: 0;
    z-index: 0;
    box-sizing: border-box;
  }

  .wrapper:not(.transcript-wrap) .media-player-transcript {
    top: 0;
    width: 33.333%;
  }

  .wrapper.transcript-wrap .media-player-transcript {
    left: 0;
    height: $transcript-wrap-height;
  }

  /***** PLAYER OVERRIDES *****/

  /* !!rtl:begin:ignore */

  /** COLOR PALLETTE **/
  $video-player-color: #212121;
  // tint if $video-player-color = black-ish, shade if $video-player-color = white-ish
  $video-player-color-2: tint($video-player-color, 7%);
  $video-player-color-3: tint($video-player-color, 15%);
  $video-player-font-color: white;

  $video-player-font-size: 12px;

  .video-js.vjs-fill {
    z-index: 1;
    transition: width $core-time ease;
  }

  .transcript-visible:not(.transcript-wrap) > .video-js.vjs-fill {
    width: 66.666%;
  }

  /* Hide control bar when playing & inactive */
  /deep/ .vjs-has-started.vjs-playing.vjs-user-inactive {
    .vjs-control-bar {
      visibility: hidden;
    }
  }

  /*** CUSTOM VIDEOJS SKIN ***/
  /deep/ .custom-skin {
    $button-height-normal: 40px;
    $button-font-size-normal: 24px;

    @include font-family-noto;

    font-size: $video-player-font-size;
    color: $video-player-font-color;

    /* Sliders */
    .vjs-slider {
      background-color: $video-player-color-2;
    }

    /* Seek Bar */
    .vjs-progress-control {
      height: initial;
      visibility: inherit;
      opacity: inherit;

      .vjs-progress-holder {
        height: 8px;
        margin-right: 16px;
        margin-left: 16px;

        .vjs-load-progress {
          div {
            background: $video-player-color-3;
          }
        }

        .vjs-play-progress {
          &::before {
            top: -5px;
            font-size: 18px;
          }
        }
      }
    }

    /* Control Bar */
    .vjs-control-bar {
      display: flex;
      height: $button-height-normal;
      background-color: $video-player-color;
    }

    /* Fixes volume panel appearing on hover. */
    .vjs-volume-vertical {
      display: none;
    }

    .vjs-volume-panel-vertical {
      &:hover {
        .vjs-volume-vertical {
          display: block;
        }
      }
    }

    .vjs-volume-level {
      background-color: $video-player-font-color;
    }

    /* Buttons */
    .vjs-button {
      .vjs-icon-placeholder {
        &::before {
          font-size: $button-font-size-normal;
          line-height: $button-height-normal;
        }
      }
    }

    /* Replay & Forward Buttons */
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      font-family: VideoJS; // override our global noto fonts with more specificity
      &::before {
        font-size: $button-font-size-normal;
        line-height: $button-height-normal;
      }
    }

    .vjs-big-play-button {
      position: absolute;
      top: 50%;
      left: 50%;
      width: $button-height-normal * 2;
      height: $button-height-normal * 2;
      margin: 0;
      font-size: $button-font-size-normal * 2;
      line-height: $button-height-normal * 2;
      background-color: $video-player-color;
      border: 0;
      border-radius: 50%;
      transform: translate(-50%, -50%);
    }

    .vjs-volume-panel {
      margin-left: auto;
    }

    /* Transcript button */
    .vjs-button-transcript img {
      max-width: 20px;
    }

    .vjs-transcript-visible > .vjs-transcript {
      display: block;
    }

    .vjs-transcript-visible > .vjs-tech,
    .vjs-transcript-visible > .vjs-modal-dialog,
    .vjs-transcript-visible > .vjs-text-track-display,
    .vjs-transcript-visible > .vjs-text-track-settings,
    .vjs-transcript-visible > .vjs-control-bar {
      right: auto;
      width: calc(100% - 330px);
    }

    /* Menus */
    .vjs-menu {
      li {
        padding: 8px;
        font-size: $video-player-font-size;
        background-color: $video-player-color;

        &:focus,
        &:hover {
          background-color: $video-player-color-3;
        }
      }

      li.vjs-selected {
        font-weight: bold;
        color: $video-player-font-color;
        background-color: $video-player-color-2;

        &:focus,
        &:hover {
          background-color: $video-player-color-3;
        }
      }
    }

    .vjs-menu-content {
      @include font-family-noto;
    }

    .vjs-volume-control {
      background-color: $video-player-color;
    }

    .vjs-playback-rate .vjs-menu {
      min-width: 4em;
    }

    /* Time */
    .vjs-current-time {
      display: block;
      padding-right: 0;

      .vjs-current-time-display {
        font-size: $video-player-font-size;
        line-height: $button-height-normal;
      }
    }

    .vjs-duration {
      display: block;
      padding-left: 0;
      .vjs-duration-display {
        font-size: $video-player-font-size;
        line-height: $button-height-normal;
      }
    }

    .vjs-time-divider {
      padding: 0;
      text-align: center;
    }

    /* Rate Button */
    .vjs-playback-rate-value {
      font-size: 20px;
      line-height: $button-height-normal;
    }

    /* Captions Settings */
    .vjs-texttrack-settings {
      display: none;
    }
  }

  /*** MEDIUM: < 600px ***/
  /deep/ .player-medium {
    /* Seek bar moves up. */
    .vjs-progress-control {
      position: absolute;
      top: -16px;
      right: 0;
      left: 0;
      width: auto;
    }

    /* Time divider is displayed. */
    .vjs-time-divider {
      display: block;
    }
  }

  /*** SMALL: < 480px ***/
  /deep/ .player-small {
    $button-height-small: 40px;
    $button-font-size-normal: 24px;

    /* Control bar buttons increase size. */
    .vjs-control-bar {
      height: $button-height-small;
    }

    .vjs-button {
      .vjs-icon-placeholder {
        &::before {
          line-height: $button-height-small;
        }
      }
    }

    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      &::before {
        line-height: $button-height-small;
      }
    }

    /* Play, replay, and forward buttons move up. */
    .vjs-play-control,
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      position: absolute;
      top: -75px;
      width: $button-height-small;
      height: $button-height-small;
      background-color: $video-player-color;
      border-radius: 50%;
      transform: translate(-50%, -50%);
    }

    .vjs-big-play-button {
      display: none;
    }

    &.vjs-show-big-play-button-on-pause {
      .vjs-big-play-button {
        display: none;
      }
    }

    /* Play button in center. */
    .vjs-play-control {
      left: 50%;
    }

    /* Replay play button on left. */
    .vjs-icon-replay_10 {
      left: 33%;
    }

    /* Forward button on right */
    .vjs-icon-forward_10 {
      left: 66%;
    }

    /* Adjust rate button text */
    .vjs-playback-rate-value {
      line-height: $button-height-small;
    }

    /* Adjust time text */
    .vjs-current-time {
      .vjs-current-time-display {
        line-height: $button-height-small;
      }
    }

    .vjs-duration {
      .vjs-duration-display {
        line-height: $button-height-small;
      }
    }

    .vjs-time-divider {
      line-height: $button-height-small;
    }
  }

  /*** TINY: < 360px ***/
  /deep/ .player-tiny {
    /* Time divider is hidden */
    .vjs-time-divider {
      display: none;
    }

    /* Time duration is hidden */
    .vjs-duration {
      display: none;
    }

    /* Adjust play, replay, and forward buttons positioning. */
    .vjs-play-control,
    .vjs-icon-replay_10,
    .vjs-icon-forward_10 {
      top: -45px;
    }

    /* Adjust replay button position. */
    .vjs-icon-replay_10 {
      left: 25%;
    }

    /* Adjust forward button position. */
    .vjs-icon-forward_10 {
      left: 75%;
    }
  }

  /* !!rtl:end:ignore */

</style>


docs(DocExample): clean-up file

docs(DocExample): clean-up file

<template>
  <v-card
    :loading="loading"
    :min-height="loading ? 200 : undefined"
    class="mb-12"
    outlined
  >
    <v-toolbar
      color="grey lighten-3"
      dense
      flat
    >
      <v-chip
        v-if="newIn"
        color="warning"
        small
      >
        <v-icon left>mdi-star</v-icon>
        New in {{ newIn }}
      </v-chip>

      <v-spacer />

      <v-tooltip bottom>
        <template v-slot:activator="{ on }">
          <v-btn
            aria-label="Invert example colors"
            icon
            @click="dark = !dark"
            v-on="on"
          >
            <v-icon>mdi-invert-colors</v-icon>
          </v-btn>
        </template>

        Invert example colors
      </v-tooltip>

      <v-tooltip bottom>
        <template v-slot:activator="{ on }">
          <v-btn
            aria-label="Edit in Codepen"
            icon
            @click="sendToCodepen"
            v-on="on"
          >
            <v-icon>mdi-codepen</v-icon>
          </v-btn>
        </template>

        Edit in Codepen
      </v-tooltip>

      <v-tooltip bottom>
        <template v-slot:activator="{ on }">
          <v-btn
            aria-label="View on Github"
            :href="`https://github.com/vuetifyjs/vuetify/tree/${branch}/packages/docs/src/examples/${file}.vue`"
            icon
            target="_blank"
            v-on="on"
          >
            <v-icon>mdi-github-circle</v-icon>
          </v-btn>
        </template>

        View on Github
      </v-tooltip>

      <v-tooltip bottom>
        <template v-slot:activator="{ on }">
          <v-btn
            aria-label="View source"
            icon
            @click="expand = !expand"
            v-on="on"
          >
            <v-icon>mdi-code-tags</v-icon>
          </v-btn>
        </template>

        View source
      </v-tooltip>
    </v-toolbar>

    <v-expand-transition v-if="parsed">
      <v-card
        v-show="expand"
        color="#2d2d2d"
        dark
        flat
        tile
      >
        <v-item-group
          v-model="selected"
          class="pa-2"
          mandatory
        >
          <template v-for="(section, i) in sections">
            <v-item
              :key="`item-${i}`"
              :value="section"
            >
              <v-btn
                slot-scope="{ active, toggle }"
                :color="!active ? 'transparent' : ''"
                :input-value="active"
                active-class="grey darken-2 white--text"
                class="mr-2"
                depressed
                rounded
                @click="toggle"
              >
                {{ section }}
              </v-btn>
            </v-item>
          </template>
        </v-item-group>

        <v-divider />

        <v-window v-model="selected">
          <template v-for="(section, i) in sections">
            <v-window-item
              :key="`window-${i}`"
              :value="section"
              eager
            >

                <doc-markup
                  :value="file"
                  :filename="false"
                  class="mb-0"
                >{{ parsed[section] }}</doc-markup>

            </v-window-item>
          </template>
        </v-window>
      </v-card>
    </v-expand-transition>

    <doc-codepen
      v-if="parsed"
      ref="codepen"
      :pen="parsed"
    />

    <v-fade-transition>
      <v-sheet
        v-if="component"
        :dark="dark"
        tile
        flat
      >
        <v-card-text>

            <component :is="component" />

        </v-card-text>
      </v-sheet>
    </v-fade-transition>
  </v-card>
</template>


Update external link component to open external links in new tabs by default.

<template>

  <!-- no extra whitespace inside link -->
  <a
    :class="buttonClasses"
    :href="href"
    :download="download"
    :target="download ? '_self' : '_blank'"
    dir="auto"
  >
    {{ text }}
  </a>

</template>

<script>

  import { validator } from './appearances.js';
  import buttonClassesMixin from './buttonClassesMixin.js';

  /**
   * KExternalLink creates a styled external link
   */
  export default {
    name: 'KExternalLink',
    mixins: [buttonClassesMixin],
    props: {
      /**
       * Link text
       */
      text: {
        type: String,
        required: true,
      },
      /**
       * URL string
       */
      href: {
        type: String,
        required: true,
      },
      /**
       * Link appearance: 'raised-button', 'flat-button', or 'basic-link'
       */
      appearance: {
        type: String,
        default: 'basic-link',
        validator,
      },
      /**
       * For 'raised-button' and 'flat-button' appearances: show as primary or secondary style
       */
      primary: {
        type: Boolean,
        default: false,
      },
      /**
       * Specifies that the file is meant to be downloaded, not displayed in a separate tab.
       */
      download: {
        type: String,
        required: false,
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import './buttons';

</style>

<template>

  <!-- no extra whitespace inside link -->
  <a
    :class="buttonClasses"
    :href="href"
    :download="download"
    dir="auto"
  >
    {{ text }}
  </a>

</template>

<script>

  import { validator } from './appearances.js';
  import buttonClassesMixin from './buttonClassesMixin.js';

  /**
   * KExternalLink creates a styled external link
   */
  export default {
    name: 'KExternalLink',
    mixins: [buttonClassesMixin],
    props: {
      /**
       * Link text
       */
      text: {
        type: String,
        required: true,
      },
      /**
       * URL string
       */
      href: {
        type: String,
        required: true,
      },
      /**
       * Link appearance: 'raised-button', 'flat-button', or 'basic-link'
       */
      appearance: {
        type: String,
        default: 'basic-link',
        validator,
      },
      /**
       * For 'raised-button' and 'flat-button' appearances: show as primary or secondary style
       */
      primary: {
        type: Boolean,
        default: false,
      },
      /**
       * Specifies that the file is meant to be downloaded, not displayed in a separate tab.
       */
      download: {
        type: String,
        required: false,
      },
    },
  };

</script>

<style lang="scss" scoped>

  @import './buttons';

</style>


Removing red status column fixes #1356

Removing red status column
fixes #1356

<template>

        {{ $tr('examTakenby', { num: takenBy }) }}

 0">
        {{ $tr('averageScore', { num: averageScore }) }}

{{$tr('examReport')}}

            {{ $tr('name') }}
            {{ $tr('status') }}
            {{ $tr('score') }}
            {{ $tr('group') }}

              <router-link :to="examDetailPageLink(examTaker.id)" class="table-name">
                {{examTaker.name}}
              </router-link>

                {{ $tr('completed') }}

                {{ $tr('incomplete', { num: examTaker.progress, outOf: exam.question_count }) }}

                {{ $tr('notstarted') }}

              —
              {{ $tr('scorePercentage', { num: examTaker.score / exam.question_count }) }}

            {{ examTaker.group.name || $tr('ungrouped') }}

{{ $tr('noExamData') }}

</template>


Update ItemListTree.vue

Update ItemListTree.vue

<template>

{{ $t(title) }}

<slot name="add-button"></slot>

            <slot name="buttons"></slot>

 1">
                <button
                    class="btn btn-light dropdown-toggle"
                    type="button"
                    id="dropdownLangSwitcher"
                    data-toggle="dropdown"
                    aria-haspopup="true"
                    aria-expanded="false"
                >
                    {{ locales.find(item => item.short === currentLocale).long }}
                </button>

                    <button
                        class="dropdown-item"
                        :class="{ active: locale === currentLocale }"
                        type="button"
                        v-for="locale in locales"
                        @click="switchLocale(locale.short)"
                    >
                        {{ locale.long }}
                    </button>

        <sl-vue-tree v-model="models" :allowMultiselect="false" ref="slVueTree" @drop="drop" @toggle="toggle">
            <template slot="title" slot-scope="{ node }">

                {{ $t('Edit') }}

{{ node.data.title_translated }}

                    {{ node.data.module }}

            </template>

            <template slot="toggle" slot-scope="{ node }">

            </template>
        </sl-vue-tree>

</template>

<template>

{{ $t(title) }}

<slot name="add-button"></slot>

            <slot name="buttons"></slot>

                <button
                    class="btn btn-light dropdown-toggle"
                    type="button"
                    id="dropdownLangSwitcher"
                    data-toggle="dropdown"
                    aria-haspopup="true"
                    aria-expanded="false"
                >
                    {{ locales.find(item => item.short === currentLocale).long }}
                </button>

                    <button
                        class="dropdown-item"
                        :class="{ active: locale === currentLocale }"
                        type="button"
                        v-for="locale in locales"
                        @click="switchLocale(locale.short)"
                    >
                        {{ locale.long }}
                    </button>

        <sl-vue-tree v-model="models" :allowMultiselect="false" ref="slVueTree" @drop="drop" @toggle="toggle">
            <template slot="title" slot-scope="{ node }">

                {{ $t('Edit') }}

{{ node.data.title_translated }}

                    {{ node.data.module }}

            </template>

            <template slot="toggle" slot-scope="{ node }">

            </template>
        </sl-vue-tree>

</template>


Autolookup version number

Autolookup version number

<template>

          <router-link class='nav-item title is-4 logo' to=''>
            V-Calendar
          </router-link>

              Github

V-Calendar

— A clean, lively calendar plugin for Vue.js —

      {{ version }}

Calendar

Attributes

DatePicker

API

Usage

</template>

<template>

          <router-link class='nav-item title is-4 logo' to=''>
            V-Calendar
          </router-link>

              Github

V-Calendar

— A clean, lively calendar plugin for Vue.js —

      v0.0.1

Calendar

Attributes

DatePicker

API

Usage

</template>


debugger

debugger

<template>

    <Menu class="action_bar" @on-select="action" mode="horizontal" theme="dark" active-name="1">

        <MenuItem name="4">
          <Icon type="code"></Icon>
          code
        </MenuItem>
        <MenuItem name="5">
          <Icon type="wrench"></Icon>
          layout
        </MenuItem>
        <MenuItem v-if="!opModel.createBy ||  me.username === opModel.createBy" name="6">
          <Icon type="document-text"></Icon>
          save
        </MenuItem>
        <MenuItem name="9">
          <Icon type="ios-undo"></Icon>
          undo
        </MenuItem>
        <MenuItem name="12">
          <Icon type="ios-redo"></Icon>
          redo
        </MenuItem>
        <MenuItem name="3">
          <Icon type="ios-eye"></Icon>
          no bord
        </MenuItem>

    </Menu>

      <Row>

          <transition name="index-soul-control-class-fade">

              <Collapse v-model="open" :key="classIndex" v-for="(controlClass, classIndex) in controlClazzes">
                <Panel :name="classIndex+1+''">
                  {{controlClass.name}}

                    <Control
                      v-for="(control,controlIndex) in controlClass.controls"
                      :controlConfig="control"
                      :key="controlIndex">

                        <MenuItem
                          :name="classIndex + '-' + controlIndex">{{control.name}}
                        </MenuItem>

                    </Control>

                </Panel>
              </Collapse>

          </transition>

          <RenderDev v-if="!showCode" :soul="soul"></RenderDev>

          <Button v-if="showCode"
                  @click="copyCode"
                  type="ghost"
                  size="small"
                  style="position: absolute;right: 0;top: 0;opacity:0.5">
            <Icon type="ios-copy-outline"></Icon>
            copy
          </Button>

          <ModelEditor
            :pageName="opModel.name"
            :editSoul="editSoul">
          </ModelEditor>

      </Row>

    <Modal
      :mask-closable="false"
      v-model="showConfirmPageNameModal"
      title="confirmPageName"
      @on-ok="okPageName">

        <FormItem label="PageName">

        </FormItem>

    </Modal>

    <Modal
      style="width: 600px"
      :mask-closable="false"
      v-model="showEditScriptModal"
      title="script">

      <CodeEditor
        :editorStyle="editorStyle"
        :code="editControlSoul.scriptString"
        @save="saveCode">
      </CodeEditor>
    </Modal>

      {{editLayer.name}}

      <Dropdown trigger="custom" visible>
        <DropdownMenu slot="list">
          <DropdownItem @click.native="editControl">editScript</DropdownItem>
          <DropdownItem @click.native="deleteControl">delete</DropdownItem>
        </DropdownMenu>
      </Dropdown>

    <textarea id="copy" style="width: 1px;height: 1px;border: 0;outline:0" />

</template>

<template>

    <Menu class="action_bar" @on-select="action" mode="horizontal" theme="dark" active-name="1">

        <MenuItem name="4">
          <Icon type="code"></Icon>
          code
        </MenuItem>
        <MenuItem name="5">
          <Icon type="wrench"></Icon>
          layout
        </MenuItem>
        <MenuItem v-if="!opModel.createBy ||  me.username === opModel.createBy" name="6">
          <Icon type="document-text"></Icon>
          save
        </MenuItem>
        <MenuItem name="9">
          <Icon type="ios-undo"></Icon>
          undo
        </MenuItem>
        <MenuItem name="12">
          <Icon type="ios-redo"></Icon>
          redo
        </MenuItem>
        <MenuItem name="3">
          <Icon type="ios-eye"></Icon>
          no bord
        </MenuItem>
        <MenuItem name="15">
          <Icon type="arrow-expand"></Icon>
          fullScreen
        </MenuItem>

    </Menu>

      <Row>

          <transition name="index-soul-control-class-fade">

              <Collapse v-model="open" :key="classIndex" v-for="(controlClass, classIndex) in controlClazzes">
                <Panel :name="classIndex+1+''">
                  {{controlClass.name}}

                    <Control
                      v-for="(control,controlIndex) in controlClass.controls"
                      :controlConfig="control"
                      :key="controlIndex">

                        <MenuItem
                          :name="classIndex + '-' + controlIndex">{{control.name}}
                        </MenuItem>

                    </Control>

                </Panel>
              </Collapse>

          </transition>

          <RenderDev v-if="!showCode" :soul="soul"></RenderDev>

          <Button v-if="showCode"
                  @click="copyCode"
                  type="ghost"
                  size="small"
                  style="position: absolute;right: 0;top: 0;opacity:0.5">
            <Icon type="ios-copy-outline"></Icon>
            copy
          </Button>

          <ModelEditor
            :pageName="opModel.name"
            :editSoul="editSoul">
          </ModelEditor>

      </Row>

    <Modal
      :mask-closable="false"
      v-model="showConfirmPageNameModal"
      title="confirmPageName"
      @on-ok="okPageName">

        <FormItem label="PageName">

        </FormItem>

    </Modal>

    <Modal
      style="width: 600px"
      :mask-closable="false"
      v-model="showEditScriptModal"
      title="script">

      <CodeEditor
        :editorStyle="editorStyle"
        :code="editControlSoul.scriptString"
        @save="saveCode">
      </CodeEditor>
    </Modal>

      {{editLayer.name}}

      <Dropdown trigger="custom" visible>
        <DropdownMenu slot="list">
          <DropdownItem @click.native="editControl">editScript</DropdownItem>
          <DropdownItem @click.native="deleteControl">delete</DropdownItem>
        </DropdownMenu>
      </Dropdown>

    <textarea id="copy" style="width: 1px;height: 1px;border: 0;outline:0" />

</template>


List outputs for tasks

List outputs for tasks

<template>

    <select :value="selectedExecutor" @input="setSelectedExecutor($event.target.value)">
      <option value="" selected disabled hidden>-</option>
      <option v-for="(executor, index) in executors">{{ executor.identifier }}</option>
    </select>
    <button class="ui button" @click="addTask">Add task</button>

      <input type="checkbox" name="public" :checked="job.active" @change="updateActive($event.target.checked)">
      On / Off

        {{ task.id }}
        {{ task.executor }}

          <input :id="'value' + inputID" :value="getInputByID(inputID).value" @input="setInputValue({inputID: inputID, value: $event.target.value})" />
          <button class="mini green ui icon button" :disabled="!getInputByID(inputID).changed" @click="saveInput(inputID)">

          </button>

          {{ output.name }}

</template>

<template>

    <select :value="selectedExecutor" @input="setSelectedExecutor($event.target.value)">
      <option value="" selected disabled hidden>-</option>
      <option v-for="(executor, index) in executors">{{ executor.identifier }}</option>
    </select>
    <button class="ui button" @click="addTask">Add task</button>

      <input type="checkbox" name="public" :checked="job.active" @change="updateActive($event.target.checked)">
      On / Off

        {{ task.id }}
        {{ task.executor }}

          <input :id="'value' + inputID" :value="getInputByID(inputID).value" @input="setInputValue({inputID: inputID, value: $event.target.value})" />
          <button class="mini green ui icon button" :disabled="!getInputByID(inputID).changed" @click="saveInput(inputID)">

          </button>

</template>


Delete beratung_und_softwareentwicklung.vue

<template lang="md">

Thomas Wener
============
*IT‑Beratung* und *Softwareentwicklung*
----------------------------------------------

### Eckdaten
**Ort:**
: Metropolregion Frankfurt/Rhein-Main, Deutschland.

*Verfügbarkeit:*
: ab **Juli 2019**, bis zu 5 Tage/Woche

*Kontakt:*
: ✉ E-Mail-Adresse spam-geschützt: die Minus-Zeichen und Sternchen bitte weglassen und den Bereich zwischen "--" rückwärts lesen:   -- renew ** @ ** samoht -- .it oder [XING](https://www.xing.com/profile/Thomas_Wener)

### Technologien und Tools

**Sprachen:**
: 	* Java
	* Javascript

*Frontend:*
:  	* HTML5
	* CSS3
	* jQuery

*Backend:*
:   * Coremedia WCM
	* OpenCMS
	* Sophora

*Agile Methoden*
: 	* Scrum Developer
	* Clean Code
	* Refactoring
	* Pair Programming
	* CI
	* Requirements Engineering
	* TDD
	* Scrum Master

*Sonstiges:*
: 	* Git
	* SVN
	* Jenkins
	* Docker
	* Checkstyle
	* EasyMock
	* xUnit
	* maven
	* ant
	* Sonar
	* Spring

</template>


[#61] Disable client side sharing check if autocomplete disabled

<template>

        <input type="text" v-model="search" class="share-add-user" :placeholder="placeholder" @keypress="submitAction($event)"/>

            <share :share="share" v-on:delete="deleteShare($event)"></share>

                 {{match.name}}

</template>


fix: wrong logined ref

fix: wrong logined ref

Former-commit-id: f9058bacfbe27a24f02f5899679c56204b337922

<template>
	<vue-particles v-if="loading" color="#dedede" style="position: absolute; width: 100%; height: 100%;"></vue-particles>
	<v-layout v-else fill-height>
		<v-navigation-drawer :value="true" mini-variant stateless dark style="border-radius: 2px 0 0 2px;"
		  class="moveable">
			<v-toolbar flat class="transparent">
				<v-list class="pa-0 non-moveable">
					<v-list-tile avatar @click="goBack">
						<v-list-tile-avatar>
							<v-icon dark>arrow_back</v-icon>
						</v-list-tile-avatar>
					</v-list-tile>
				</v-list>
			</v-toolbar>
			<v-list class="non-moveable">
				<v-divider dark style="display: block !important;"></v-divider>
				<v-list-tile :disabled="!logined" replace to="/">
					<v-list-tile-action>
						<v-icon>home</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-list-tile :disabled="!logined" replace to="/profiles">
					<v-list-tile-action>
						<v-icon>apps</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-list-tile :disabled="!logined" replace to="/user">
					<v-list-tile-action>
						<v-icon>person</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-spacer></v-spacer>
			</v-list>
			<v-list class="non-moveable" style="position: absolute; bottom: 0px;">
				<v-list-tile replace to="/setting">
					<v-list-tile-action>
						<v-icon dark>settings</v-icon>
					</v-list-tile-action>
				</v-list-tile>
			</v-list>
		</v-navigation-drawer>
		<v-layout style="padding: 0; background: transparent; max-height: 100vh;" fill-height>
			<v-card class="main-body" color="grey darken-4">
				<vue-particles color="#dedede" style="position: absolute; width: 100%; height: 100%;" clickMode="repulse"></vue-particles>
				<transition name="fade-transition" mode="out-in">
					<router-view></router-view>
				</transition>
				<notifier></notifier>
			</v-card>
		</v-layout>
	</v-layout>
</template>

<script>
import Notifier from './Notifier';

export default {
  data: () => ({
    loading: false, // disable for now, but it'll be abled if the loading process is too slow..
    localHistory: [],
    timeTraveling: false,
  }),
  components: { Notifier },
  computed: {
    logined() {
      return this.$repo.getters['user/logined'];
    },
  },
  created() {
    this.$router.afterEach((to, from) => {
      if (!this.timeTraveling) this.localHistory.push(from.fullPath);
    });
  },
  mounted() {
    this.$electron.ipcRenderer.once('vuex-sync', () => {
      this.loading = false;
    });
    if (!this.logined) {
      this.$router.push('/login');
    }
  },
  watch: {},
  methods: {
    close() {
      this.$store.dispatch('exit');
    },
    goBack() {
      if (!this.logined && this.$route.path === '/login') {
        return;
      }
      this.timeTraveling = true;
      const before = this.localHistory.pop();
      if (before) {
        this.$router.replace(before);
      }
      this.timeTraveling = false;
    },
  },
}
</script>

<style>
.clip-head {
  clip-path: inset(0px 30px 30px 0px) !important;
  width: 64px;
  height: auto; /*to preserve the aspect ratio of the image*/
}
.moveable {
  -webkit-app-region: drag;
  user-select: none;
}

.non-moveable {
  -webkit-app-region: no-drag;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
.v-input__icon--prepend {
  margin-right: 7px;
}
</style>

<style scoped=true>
.main-body {
  max-width: 690px;
  width: 100%;
  border-radius: 0px 2px 2px 0;
}
</style>

<template>
	<vue-particles v-if="loading" color="#dedede" style="position: absolute; width: 100%; height: 100%;"></vue-particles>
	<v-layout v-else fill-height>
		<v-navigation-drawer :value="true" mini-variant stateless dark style="border-radius: 2px 0 0 2px;"
		  class="moveable">
			<v-toolbar flat class="transparent">
				<v-list class="pa-0 non-moveable">
					<v-list-tile avatar @click="goBack">
						<v-list-tile-avatar>
							<v-icon dark>arrow_back</v-icon>
						</v-list-tile-avatar>
					</v-list-tile>
				</v-list>
			</v-toolbar>
			<v-list class="non-moveable">
				<v-divider dark style="display: block !important;"></v-divider>
				<v-list-tile :disabled="!logined" replace to="/">
					<v-list-tile-action>
						<v-icon>home</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-list-tile :disabled="!logined" replace to="/profiles">
					<v-list-tile-action>
						<v-icon>apps</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-list-tile :disabled="!logined" replace to="/user">
					<v-list-tile-action>
						<v-icon>person</v-icon>
					</v-list-tile-action>
				</v-list-tile>
				<v-spacer></v-spacer>
			</v-list>
			<v-list class="non-moveable" style="position: absolute; bottom: 0px;">
				<v-list-tile replace to="/setting">
					<v-list-tile-action>
						<v-icon dark>settings</v-icon>
					</v-list-tile-action>
				</v-list-tile>
			</v-list>
		</v-navigation-drawer>
		<v-layout style="padding: 0; background: transparent; max-height: 100vh;" fill-height>
			<v-card class="main-body" color="grey darken-4">
				<vue-particles color="#dedede" style="position: absolute; width: 100%; height: 100%;" clickMode="repulse"></vue-particles>
				<transition name="fade-transition" mode="out-in">
					<router-view></router-view>
				</transition>
				<notifier></notifier>
			</v-card>
		</v-layout>
	</v-layout>
</template>

<script>
import Notifier from './Notifier';

export default {
  data: () => ({
    loading: false, // disable for now, but it'll be abled if the loading process is too slow..
    localHistory: [],
    timeTraveling: false,
  }),
  components: { Notifier },
  computed: {
    logined() {
      return this.$repo.getters['user/logined'];
    },
  },
  created() {
    this.$router.afterEach((to, from) => {
      if (!this.timeTraveling) this.localHistory.push(from.fullPath);
    });
  },
  mounted() {
    this.$electron.ipcRenderer.once('vuex-sync', () => {
      this.loading = false;
    });
    if (!this.logined) {
      this.$router.push('/login');
    }
  },
  watch: {},
  methods: {
    close() {
      this.$store.dispatch('exit');
    },
    goBack() {
      if (!this.login && this.$route.path === '/login') {
        return;
      }
      this.timeTraveling = true;
      if (this.localHistory.length !== 0) {
        const before = this.localHistory.pop();
        this.$router.replace(before);
      }
      this.timeTraveling = false;
    },
  },
}
</script>

<style>
.clip-head {
  clip-path: inset(0px 30px 30px 0px) !important;
  width: 64px;
  height: auto; /*to preserve the aspect ratio of the image*/
}
.moveable {
  -webkit-app-region: drag;
  user-select: none;
}

.non-moveable {
  -webkit-app-region: no-drag;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
.v-input__icon--prepend {
  margin-right: 7px;
}
</style>

<style scoped=true>
.main-body {
  max-width: 690px;
  width: 100%;
  border-radius: 0px 2px 2px 0;
}
</style>


feat: add link to admin area on welcome page (#4035)

* feat: Add link to admin area on welcome page

See: #1259

* fix: remove trailing slash from welcome admin button

* fix: welcome page admin button link

Co-authored-by: Nicolas Giard <64b2b6d12bfe4baae7dad3d018f8cbf6b0e7a044@ngpixel.com>

<template lang='pug'>
  v-app
    .onboarding
      .onboarding-content
        img.animated.fadeIn(src='/_assets/svg/logo-wikijs.svg', alt='Wiki.js')
        .headline.animated.fadeInUp {{ $t('welcome.title') }}
        .subtitle-1.mt-3.animated.fadeInUp.wait-p1s {{ $t('welcome.subtitle') }}
        div
          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p2s(color='primary', :href='`/e/` + locale + `/home`', x-large)
            v-icon(left) mdi-plus
            span {{ $t('welcome.createhome') }}
          v-btn.mt-5.mx-3.animated.fadeInUp.wait-p3s(color='primary', href='/a', x-large)
            v-icon(left) mdi-view-dashboard
            span {{ $t('welcome.goadmin') }}

</template>

<script>

export default {
  props: {
    locale: {
      type: String,
      default: 'en'
    }
  },
  data() {
    return { }
  }
}
</script>

<style lang='scss'>

</style>

<template lang='pug'>
  v-app
    .onboarding
      .onboarding-content
        img.animated.fadeIn(src='/_assets/svg/logo-wikijs.svg', alt='Wiki.js')
        .headline.animated.fadeInUp {{ $t('welcome.title') }}
        .subtitle-1.mt-3.animated.fadeInUp.wait-p1s {{ $t('welcome.subtitle') }}
        v-btn.mt-5.animated.fadeInUp.wait-p2s(color='primary', :href='`/e/` + locale + `/home`', x-large)
          v-icon(left) mdi-plus
          span {{ $t('welcome.createhome') }}
</template>

<script>

export default {
  props: {
    locale: {
      type: String,
      default: 'en'
    }
  },
  data() {
    return { }
  }
}
</script>

<style lang='scss'>

</style>


Search on enter

Search on enter

<template>

        <form v-on:submit="update_filter">

                <input type="text" class="form-control" placeholder="Find username" v-model="username_filter" />

                    <button class="btn btn-default searchbarbutton" type="button" v-on:click="update_filter"></button>

        </form>

                    #
                    Username
                    Organization
                    Level
                    Language
                    Points
                    Tier

                    {{ player.rank }}
                    {{ player.username }}
                    {{ player.organization }}
                    {{ player.level }}
                    {{ player.language }}
                    {{ Math.round(100 * player.score) / 100 }}
                    {{ player.tier }}

</template>

<template>

            <input type="text" class="form-control" placeholder="Find username" v-model="username_filter" />

                <button class="btn btn-default searchbarbutton" type="button" v-on:click="update_filter" ></button>

                    #
                    Username
                    Organization
                    Level
                    Language
                    Points
                    Tier

                    {{ player.rank }}
                    {{ player.username }}
                    {{ player.organization }}
                    {{ player.level }}
                    {{ player.language }}
                    {{ Math.round(100 * player.score) / 100 }}
                    {{ player.tier }}

</template>


most recent builds by default

most recent builds by default

<template>

Builds

Existing configurations

                {{conf_name}}

                        Create new build

                        {{readonly ? 'View' : 'Edit' }} configuration

                        Delete configuration

Other actions

New configuration

{{include_archived_configs ? "Hide" : "Show"}}  archived configurations

                            Menu

                            <select class="ui filterbuilds dropdown" v-model="showSelection">
                                <option value="">Date Filter</option>
                                <optgroup label="view builds from">
                                    <option value="today">from today</option>
                                    <option value="1 day ago">from 1 day ago</option>
                                    <option value="2 days ago">from 2 days ago</option>
                                    <option value="3 days ago">from 3 days ago</option>
                                    <option value="all time">view all (most recent)</option>
                                </optgroup>
                            </select>

                            <button class="ui clearconffilter button" v-if="conf_filter" @click="clearFilter">
                                Clear
                            </button>
                            <select class="ui filterbuilds dropdown" v-model="conf_filter">
                                <option value="">Source Filter</option>
                                <option :value="name" v-for="(conf,name) in build_configs">

                                    {{name}}
                                </option>
                            </select>

                                <input type="checkbox" name="includearchived" v-model="only_archived">
                                Show archived builds only

                        Showing <b v-text="shown < builds.length ? shown : builds.length"> out of  from
                        <button v-if="shown < builds.length && shown !== 10" class="ui button mini m-1" @click="shown = shown-10">
                          -10
                        </button>
                        <button v-if="shown < builds.length" class="ui button mini m-1" @click="shown = shown+10">
                          +10
                        </button>

                        <template v-for="(build,i) in builds">
                            <div class="ui five wide column" v-bind:key="i" v-if="i < shown">
                                <build v-bind:pbuild="build" v-bind:color="build_colors[build.build_config.name]"></build>

                        </template>

                    Create/edit build configuration

                                    Error in build configuration

{{buildconf_error}}

                                Enter a build configuration name
                                <input type="text" id="conf_name" placeholder="Configuration name" autofocus>

                                Enter a name for the type of stored documents ("gene", "variant", ...)
                                <input type="text" id="doc_type" placeholder="Document type" autofocus>

                                Select the sources used to create merged data
                                <select class="ui fluid sources dropdown" id="selected_sources" multiple="">
                                    <option value="">Available sources</option>
                                    <option v-for="_id in sources">{{_id}}</option>
                                </select>

                                Once sources are selected, choose sources providing root documents
                                <select class="ui fluid rootsources dropdown" id="root_sources" multiple="">
                                    <option value="">Root sources</option>
                                </select>

                                Select a builder type
                                <select class="ui fluid builders dropdown" id="builders">
                                    <option v-for="val,classpath in builder_classes" :value="classpath">{{classpath}} (default)</option>
                                </select>

                                    About this builder

{{builder_doc}}

Optional parameters can be added to the configuration (usefull to customize builder behavior). Enter a JSON object structure

                                <textarea id="optionals">{}</textarea>

                                    Note: sources providing root documents, or root sources, are sources allowed to
                                    create a new document in a build (merged data). If a root source is declared, data from other sources will only be merged if
                                    documents previously exist with same IDs (documents coming from root sources). If not, data is discarded. Finally, if no root source
                                    is declared, any data sources can generate a new document in the merged data.

                                    Remove this configuration from list by default

You can still access it by clicking on "show/hide archived configuration" in the menu on the side

                                        <input type="checkbox" name="archive_conf">
                                        Archive

{{err}}

                        Cancel

                        OK

                    Delete configuration

Are you sure you want to delete this build configuration ?

                        No

                        Yes

                    Create new build

Enter a name for the merged data collection or leave it empty to generate a random one

                            <input type="text" id="target_name" placeholder="Collection name" autofocus>

                                <input type="checkbox" name="force_build" id="force_build">
                                Skip sanity checks and force build (not recommended)

                        Cancel

                        OK

    </template>

<template>

Builds

Existing configurations

                {{conf_name}}

                        Create new build

                        {{readonly ? 'View' : 'Edit' }} configuration

                        Delete configuration

Other actions

New configuration

{{include_archived_configs ? "Hide" : "Show"}}  archived configurations

                            Menu

                            <select class="ui filterbuilds dropdown" v-model="showSelection">
                                <option value="">Date Filter</option>
                                <optgroup label="view builds from">
                                    <option value="today">from today</option>
                                    <option value="1 day ago">from 1 day ago</option>
                                    <option value="2 days ago">from 2 days ago</option>
                                    <option value="3 days ago">from 3 days ago</option>
                                    <option value="all time">view all</option>
                                </optgroup>
                            </select>

                            <button class="ui clearconffilter button" v-if="conf_filter" @click="clearFilter">
                                Clear
                            </button>
                            <select class="ui filterbuilds dropdown" v-model="conf_filter">
                                <option value="">Source Filter</option>
                                <option :value="name" v-for="(conf,name) in build_configs">

                                    {{name}}
                                </option>
                            </select>

                                <input type="checkbox" name="includearchived" v-model="only_archived">
                                Show archived builds only

                        Showing <b v-text="shown < builds.length ? shown : builds.length"> out of  from
                        <button v-if="shown < builds.length" class="ui button mini m-1" @click="shown = shown-10">
                          -10
                        </button>
                        <button v-if="shown < builds.length" class="ui button mini m-1" @click="shown = shown+10">
                          +10
                        </button>

                        <template v-for="(build,i) in builds">
                            <div class="ui five wide column" v-bind:key="i" v-if="i < shown">
                                <build v-bind:pbuild="build" v-bind:color="build_colors[build.build_config.name]"></build>

                        </template>

                    Create/edit build configuration

                                    Error in build configuration

{{buildconf_error}}

                                Enter a build configuration name
                                <input type="text" id="conf_name" placeholder="Configuration name" autofocus>

                                Enter a name for the type of stored documents ("gene", "variant", ...)
                                <input type="text" id="doc_type" placeholder="Document type" autofocus>

                                Select the sources used to create merged data
                                <select class="ui fluid sources dropdown" id="selected_sources" multiple="">
                                    <option value="">Available sources</option>
                                    <option v-for="_id in sources">{{_id}}</option>
                                </select>

                                Once sources are selected, choose sources providing root documents
                                <select class="ui fluid rootsources dropdown" id="root_sources" multiple="">
                                    <option value="">Root sources</option>
                                </select>

                                Select a builder type
                                <select class="ui fluid builders dropdown" id="builders">
                                    <option v-for="val,classpath in builder_classes" :value="classpath">{{classpath}} (default)</option>
                                </select>

                                    About this builder

{{builder_doc}}

Optional parameters can be added to the configuration (usefull to customize builder behavior). Enter a JSON object structure

                                <textarea id="optionals">{}</textarea>

                                    Note: sources providing root documents, or root sources, are sources allowed to
                                    create a new document in a build (merged data). If a root source is declared, data from other sources will only be merged if
                                    documents previously exist with same IDs (documents coming from root sources). If not, data is discarded. Finally, if no root source
                                    is declared, any data sources can generate a new document in the merged data.

                                    Remove this configuration from list by default

You can still access it by clicking on "show/hide archived configuration" in the menu on the side

                                        <input type="checkbox" name="archive_conf">
                                        Archive

{{err}}

                        Cancel

                        OK

                    Delete configuration

Are you sure you want to delete this build configuration ?

                        No

                        Yes

                    Create new build

Enter a name for the merged data collection or leave it empty to generate a random one

                            <input type="text" id="target_name" placeholder="Collection name" autofocus>

                                <input type="checkbox" name="force_build" id="force_build">
                                Skip sanity checks and force build (not recommended)

                        Cancel

                        OK

    </template>


Add exception for matterbridge integration code

Signed-off-by: Marco Ambrosini <3397c7e210a9d1588196cfebb12b2c6b133f6934@pm.me>

<template>

				{{ type.name }}

			<Actions
				:force-menu="false">
				<ActionButton v-if="editable"
					:icon="editing ? 'icon-checkmark' : 'icon-rename'"
					@click="onEditClick">
					{{ editing ? t('spreed', 'Save'): t('spreed', 'Edit') }}
				</ActionButton>
			</Actions>
			<Actions
				class="actions"
				:force-menu="true"
				placement="bottom">
				<ActionLink
					icon="icon-info"
					target="_blank"
					:title="t('spreed', 'More information')"
					:href="type.infoTarget"
					:close-after-click="true" />
				<ActionButton v-if="editable"
					icon="icon-delete"
					:close-after-click="true"
					@click="$emit('delete-part')">
					{{ t('spreed', 'Delete') }}
				</ActionButton>
			</Actions>

				<input
					:id="key + '-' + num"
					:ref="key"
					v-model="part[key]"
					:type="field.type"
					:class="classesOf(key)"
					:disabled="!editing">

					{{ field.labelText }}

					{{ field.placeholder }}

				<input
					:id="key + '-' + num"
					:ref="key"
					v-model="part[key]"
					:type="field.type"
					:class="classesOf(key)"
					:placeholder="field.placeholder"
					:readonly="readonly || !editing"
					@focus="readonly = false">

</template>


Sort on track number

Sort on track number

<template>

{{album.name}}

{{album.getSongs()[0].artist}} {{album.year}}

                        #
                        Title
                        Artist
                        Album
                        Year

                        {{song.track}}
                        {{song.name}}
                        {{song.artist}}
                        {{song.album}}
                        {{song.year}}

</template>

<template>

{{album.name}}

{{album.getSongs()[0].artist}} {{album.year}}

                        #
                        Title
                        Artist
                        Album
                        Year

                        {{$index+1}}
                        {{song.name}}
                        {{song.artist}}
                        {{song.album}}
                        {{song.year}}

</template>


Label component fixes to conditionally add required indicator along with the tooltip.

<template>

    <slot></slot>

      *

</template>

<template>
  <label>
    <slot></slot>
  </label>
</template>

<style scoped>
label {
  width: 100%;
  margin-bottom: 5px;
  display: inline-block;
  font-weight: 700;
}

label.underlined {
  border-bottom: 2px solid #ddd;
  padding: 8px 0px;
}
</style>


chore: 支持流式布局

chore: 支持流式布局

<template>

        <slot></slot>

</template>

<template>

        <slot></slot>

</template>


add navigation via keyboard

add navigation via keyboard

<template>
  <label :tabindex="`${disabled ? -1 : 0}`" :class="['ctrl-select', { 'disabled': disabled }]" @keydown="onUpDown" @click="onMouseClick" @keyup.enter="onEnterClick" ref="triggerSelect">
    {{ selectedOption.title }}

    <transition name="fade">
      <ul v-if="isOpen" class="options-list scroll-box" ref="optionsListElement" :style="{ width: `${triggerSelect.clientWidth}px`, top: `${optionsListOffsetTop}px`, left: `${optionsListOffsetLeft}px` }">

{{ option.title }}

    </transition>

</template>

<template>
  <label :tabindex="`${disabled ? -1 : 0}`" :class="['ctrl-select', { 'disabled': disabled }]" @click="onSelectClick" ref="triggerSelect">
    {{ selectedOption.title }}

    <transition name="fade">
      <ul v-if="isOpen" class="options-list scroll-box" ref="optionsList" :style="{ width: `${triggerSelect.clientWidth}px`, top: `${optionsListOffsetTop}px`, left: `${optionsListOffsetLeft}px` }">

{{ option.title }}

    </transition>

</template>


[qa] code scrub

[qa] code scrub

z<template>

{{ $t('main.column_visibility') }}

        <input
          type="checkbox"
          :id="descriptor.field_name"
          :checked="metadataDisplayHeaders[descriptor.field_name] !== false"
          @change="setMetadataDisplayValue(
            descriptor.field_name, $event.target.checked
          )"
        >
        {{ descriptor.name }}

</template>


install

install

<template>

    <common-header type="information"></common-header>

      <navigator menu="repository"></navigator>

          您的当前位置 : 管理驾驶舱

</template>


disabled company input for now

disabled company input for now

<template>

      <api-error :error="error" resetter="SET_ERROR"></api-error>

Add a Well Driller or Well Pump Installer

                 Add a company

                    Well Driller
                    Well Pump Installer

             x === 'DRILL' || x === 'PUMP')">
               x === 'DRILL')">

               x === 'PUMP')">

                Submit
                Reset

</template>

<template>

      <api-error :error="error" resetter="SET_ERROR"></api-error>

Add a Well Driller or Well Pump Installer

                  <v-select
                    v-model="drillerForm.person.organization"
                    :options="companies"
                    placeholder="Begin typing a company name"/>

                 Add a company

                    Well Driller
                    Well Pump Installer

             x === 'DRILL' || x === 'PUMP')">
               x === 'DRILL')">

               x === 'PUMP')">

                Submit
                Reset

</template>


Fix dapp stats css

Fix dapp stats css

<template>

            Total ÐApps

{{ statDappCount.toLocaleString() }}

            Daily active users
            <Help text="Unique source addresses in transactions to ÐApp contracts"/>

{{ statDappDau | abbreviateNumber(2) || 0 }}

            24 hr transactions
            <Help text="Number of transactions to ÐApp contracts"/>

{{ statDappTx24Hr | abbreviateNumber(2) || 0 }}

Smart contracts

{{ statDappContractCount | abbreviateNumber(2) || 0 }}

Platforms

            Platform
            Total ÐApps

              Daily active users
              <Help text="Unique source addresses in transactions to ÐApp contracts" :bottom="true"/>

              24 hr transactions
              <Help text="Number of transactions to ÐApp contracts" :bottom="true"/>

              24 hr volume
              <Help text="Transaction volume to ÐApp contracts" :bottom="true"/>

            # of contracts

            {{ platform.platform }}
            {{ platform.dappCount.toLocaleString() }}
            {{ platform.dappDau | abbreviateNumber(2) || 0  }}
            {{ platform.dappTx24Hr | abbreviateNumber(2) || 0  }}
            {{ platform.dappVol24Hr | abbreviateNumber(2) || 0  }}
            {{ platform.dappContractCount | abbreviateNumber(2) || 0 }}

Categories

      <StatsBarChart/>

</template>


Update alert.vue

Update alert.vue

<template>

    <button type="button"
            class="close"
            data-dismiss="alert"
            aria-label="Close"
            v-if="dismissible"
            @click.stop.prevent="dismiss"
    >
      ×
    </button>
    <slot></slot>

</template>