[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\n\n[*.{js,ts,vue,json}]\nindent_size = 2\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\npackage-lock.json\nnode_modules\npublic/dist\n.DS_store\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n- 10.15.3\naddons:\n  ssh_known_hosts: 52.76.67.104\nbefore_deploy:\n- openssl aes-256-cbc -K $encrypted_7892896c2e12_key -iv $encrypted_7892896c2e12_iv\n  -in light-sail-cuckoo-plus.pem.enc -out light-sail-cuckoo-plus.pem -d\n- eval \"$(ssh-agent -s)\"\n- chmod 600 light-sail-cuckoo-plus.pem\n- ssh-add light-sail-cuckoo-plus.pem\ndeploy:\n  provider: script\n  script: bash ./deploy.sh\n  skip_cleanup: true\nbranches:\n  only:\n  - master"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Morse_Guo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Cuckoo.Plus [![Build Status](https://travis-ci.com/NanaMorse/Cuckoo.Plus.svg?branch=master)](https://travis-ci.com/NanaMorse/Cuckoo.Plus)\n\nalpha view link: [Cuckoo.Social](http://www.cuckoo.social)\n\nStart project.\n```\nnpm i\nnpm run dev\n```\nthen open [localhost:3000](http://localhost:3000) in the browser.\n\nBuild project.\n```\nnpm run build\n```\nthe files located in public folder are all you need\n\n项目完成状态可以在 [Cuckoo.Social](http://www.cuckoo.social) 中自行体验，没有的就是没做，点不动的一般也是没做。\n\n## Licence\nMIT.\n"
  },
  {
    "path": "deploy.sh",
    "content": "npm run build\nscp -i \"light-sail-cuckoo-plus.pem\" -r public ubuntu@52.76.67.104:projects/Cuckoo.Plus/"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"cuckoo.plus\",\n  \"version\": \"0.3.27\",\n  \"description\": \"A third-party client for mastodon\",\n  \"scripts\": {\n    \"test\": \"mocha -r ts-node/register src/**/*.spec.ts\",\n    \"dev\": \"webpack-dev-server\",\n    \"build\": \"webpack --env=production\",\n    \"start\": \"node server.js\"\n  },\n  \"dependencies\": {\n    \"autosize\": \"^4.0.2\",\n    \"crypto-js\": \"^3.1.9-1\",\n    \"express\": \"^4.16.4\",\n    \"file-saver\": \"^2.0.1\",\n    \"jquery\": \"^3.4.1\",\n    \"less\": \"^3.9.0\",\n    \"masonry-layout\": \"^4.2.2\",\n    \"moment\": \"^2.22.2\",\n    \"muse-ui\": \"^3.0.1\",\n    \"muse-ui-loading\": \"^0.2.0\",\n    \"muse-ui-message\": \"^0.2.1\",\n    \"muse-ui-progress\": \"^0.1.0\",\n    \"muse-ui-toast\": \"^0.3.0\",\n    \"resize-observer-polyfill\": \"^1.5.1\",\n    \"sha1\": \"^1.1.1\",\n    \"textarea-caret\": \"^3.1.0\",\n    \"underscore\": \"^1.9.1\",\n    \"vue\": \"^2.5.13\",\n    \"vue-color\": \"^2.7.0\",\n    \"vue-i18n\": \"^8.1.0\",\n    \"vue-resource\": \"^1.5.1\",\n    \"vue-router\": \"^3.0.1\",\n    \"vuex\": \"^3.0.1\"\n  },\n  \"devDependencies\": {\n    \"@types/chai\": \"^4.1.6\",\n    \"@types/crypto-js\": \"^3.1.43\",\n    \"@types/jquery\": \"^3.3.31\",\n    \"@types/less\": \"^3.0.0\",\n    \"@types/mocha\": \"^5.2.5\",\n    \"@types/node\": \"^10.11.7\",\n    \"@types/sha1\": \"^1.1.1\",\n    \"@types/underscore\": \"^1.8.9\",\n    \"@types/vue-color\": \"^2.4.2\",\n    \"@types/vue-resource\": \"^0.9.34\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-loader\": \"^7.1.2\",\n    \"babel-minify-webpack-plugin\": \"^0.3.1\",\n    \"babel-plugin-transform-class-properties\": \"^6.24.1\",\n    \"babel-preset-es2015\": \"^6.24.1\",\n    \"chai\": \"^4.2.0\",\n    \"css-loader\": \"^0.28.9\",\n    \"husky\": \"^1.1.2\",\n    \"less\": \"^3.8.1\",\n    \"less-loader\": \"^4.1.0\",\n    \"mocha\": \"^5.2.0\",\n    \"node-sass\": \"^4.9.3\",\n    \"raw-loader\": \"^0.5.1\",\n    \"sass-loader\": \"^7.1.0\",\n    \"style-loader\": \"^0.23.1\",\n    \"ts-loader\": \"^3.5.0\",\n    \"ts-node\": \"^7.0.1\",\n    \"typescript\": \"^2.7.1\",\n    \"vue-loader\": \"^14.2.3\",\n    \"vue-property-decorator\": \"^7.2.0\",\n    \"vue-template-compiler\": \"^2.5.13\",\n    \"vuex-class\": \"^0.3.1\",\n    \"webpack\": \"^3.11.0\",\n    \"webpack-bundle-analyzer\": \"^3.0.3\",\n    \"webpack-dev-server\": \"^2.11.3\",\n    \"yargs\": \"^12.0.2\"\n  },\n  \"babel\": {\n    \"presets\": [\n      \"es2015\"\n    ]\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-push\": \"npm run test\"\n    }\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/NanaMorse/Cuckoo.Plus.git\"\n  },\n  \"author\": \"Nana.Morse, KTachibanaM, roytam1, cutls, hakaba-hitoyo\",\n  \"thanks\": \"roytam1, fhoshino\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/NanaMorse/Cuckoo.Plus/issues\"\n  },\n  \"homepage\": \"https://github.com/NanaMorse/Cuckoo.Plus#readme\"\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <!-- Global site tag (gtag.js) - Google Analytics -->\n  <script async src=\"https://www.googletagmanager.com/gtag/js?id=UA-135462687-1\"></script>\n  <script>\n    window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n\n    gtag('config', 'UA-135462687-1');\n  </script>\n\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n  <!-- ignore user setting for uc browser -->\n  <meta name=\"layoutmode\" content=\"standard\"/>\n  <meta name=\"renderer\" content=\"webkit\">\n  <meta name=\"force-rendering\" content=\"webkit\">\n  <meta name=\"theme-color\" content=\"#db4437\">\n  <link rel=\"manifest\" href=\"manifest.json\">\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon/google_plus/48x48.png\" sizes=\"48x48\">\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon/google_plus/72x72.png\" sizes=\"72x72\">\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon/google_plus/96x96.png\" sizes=\"96x96\">\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon/google_plus/144x144.png\" sizes=\"144x144\">\n  <link rel=\"icon\" type=\"image/png\" href=\"favicon/google_plus/192x192.png\" sizes=\"192x192\">\n  <link rel=\"apple-touch-icon\" href=\"favicon/apple-touch-icon.jpg\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <link href='https://fonts.loli.net/css?family=Open+Sans' rel='stylesheet'>\n  <link href='https://fonts.loli.net/icon?family=Material+Icons' rel='stylesheet'>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/moment.min.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-cn.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-hk.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-tw.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/ja.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/de.js\"></script>\n  <script src=\"https://cdnjs.loli.net/ajax/libs/underscore.js/1.9.1/underscore-min.js\"></script>\n  <link rel=\"stylesheet\" href=\"https://unpkg.com/muse-ui/dist/muse-ui.css\">\n  <title>Cuckoo+</title>\n</head>\n<body>\n<div id=\"app\"></div>\n<script src=\"dist/bundle.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"name\"              : \"Cuckoo Social\",\n  \"short_name\"        : \"Cuckoo+\",\n  \"start_url\"         : \"/\",\n  \"display\"           : \"standalone\",\n  \"icons\": [\n    {\n      \"src\"           : \"favicon/google_plus/48x48.png\",\n      \"sizes\"         : \"48x48\",\n      \"type\"          : \"image/png\"\n    },\n    {\n      \"src\"           : \"favicon/google_plus/72x72.png\",\n      \"sizes\"         : \"72x72\",\n      \"type\"          : \"image/png\"\n    },\n    {\n      \"src\"           : \"favicon/google_plus/96x96.png\",\n      \"sizes\"         : \"96x96\",\n      \"type\"          : \"image/png\"\n    },\n    {\n      \"src\"           : \"favicon/google_plus/144x144.png\",\n      \"sizes\"         : \"144x144\",\n      \"type\"          : \"image/png\"\n    },\n    {\n      \"src\"           : \"favicon/google_plus/192x192.png\",\n      \"sizes\"         : \"192x192\",\n      \"type\"          : \"image/png\"\n    }\n  ]\n}\n"
  },
  {
    "path": "public/sw.js",
    "content": "const version = '0.3.27'\nconst CACHE = version + ':CP'\nconst cacheFilePaths = [\n  '/',\n  '/manifest.json',\n  '/dist/bundle.js',\n\n  'https://fonts.loli.net/css?family=Open+Sans',\n  'https://fonts.loli.net/icon?family=Material+Icons',\n  'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/moment.min.js',\n  'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-cn.js',\n  'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-hk.js',\n  'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-tw.js',\n  'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/ja.js',\n  'https://cdnjs.loli.net/ajax/libs/underscore.js/1.9.1/underscore-min.js',\n  'https://unpkg.com/muse-ui/dist/muse-ui.css',\n  'https://gstatic.loli.net/s/materialicons/v46/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2',\n]\n\nconst swContext = this\n\nclass SW {\n\n  constructor () {\n    this.initInstallEventListener()\n    this.initActivateEventListener()\n    this.initFetchEventListener()\n  }\n\n  initInstallEventListener () {\n    swContext.addEventListener('install', event => {\n      event.waitUntil(this.installFiles().then(() => swContext.skipWaiting()))\n    })\n  }\n\n  installFiles () {\n    return caches.open(CACHE).then(cache => {\n      return cache.addAll(cacheFilePaths)\n    })\n  }\n\n  initActivateEventListener () {\n    swContext.addEventListener('activate', event => {\n      // delete old caches\n      event.waitUntil(this.clearOldCaches().then(() => swContext.clients.claim()))\n    });\n  }\n\n  clearOldCaches () {\n    return caches.keys().then(keylist => {\n      return Promise.all(keylist.filter(key => key !== CACHE).map(key => caches.delete(key)))\n    })\n  }\n\n  initFetchEventListener () {\n    swContext.addEventListener('fetch', event => {\n      // abandon non-GET requests\n      if (event.request.method !== 'GET') return\n\n      const request = event.request\n      const url = request.url\n\n      const isRequestImage = event.request.destination === 'image'\n\n      if (!this.isCacheFilePath(url) && !isRequestImage) return\n\n      return event.respondWith(caches.open(CACHE).then(cache => {\n        return cache.match(request).then(response => {\n          if (response) return response\n\n          return fetch(request).then(response => {\n            if (response.ok) cache.put(request, response.clone())\n            return response\n          }).catch()\n        })\n      }))\n\n    })\n  }\n\n  isCacheFilePath (url) {\n    return cacheFilePaths.some(filePath => url.endsWith(filePath))\n  }\n}\n\nnew SW()\n"
  },
  {
    "path": "server.js",
    "content": "const express = require('express')\nconst http = require('http')\nconst https = require('https')\nconst fs = require('fs')\n\nconst httpApp = express()\nconst httpsApp = express()\n\nconst httpPort = 80\nconst httpsPort = 443\n\nhttpApp.all(\"*\", (req, res) => {\n  let host = req.headers.host\n  host = host.replace(/:\\d+$/, '')\n  res.redirect(301, `https://${host}${req.path}`)\n})\n\nhttp.createServer(httpApp).listen(httpPort)\n\nhttpsApp.use(express.static('public'))\nhttps.createServer({\n  key: fs.readFileSync('../ssl/private.key'),\n  cert: fs.readFileSync('../ssl/certificate.pem')\n}, httpsApp).listen(httpsPort)\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <cuckoo-plus-header v-if=\"!$route.meta.hideHeader\"/>\n    <cuckoo-plus-drawer v-if=\"!$route.meta.hideDrawer && isOAuthUser\"/>\n    <mu-container :fluid=\"true\" class=\"app-content\" :style=\"appContentStyle\">\n      <keep-alive>\n        <router-view v-if=\"$route.meta.keepAlive\" />\n      </keep-alive>\n\n      <router-view v-if=\"!$route.meta.keepAlive\" />\n    </mu-container>\n    <theme-edit-panel v-if=\"appStatus.isEditingThemeMode\"/>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { Mutation, State, Getter } from 'vuex-class'\n  import * as _ from 'underscore'\n  import { UiWidthCheckConstants, TimeLineTypes, TITLE } from '@/constant'\n  import Header from '@/components/Header.vue'\n  import Drawer from '@/components/Drawer'\n  import ThemeEditPanel from '@/components/ThemeEditPanel'\n\n  @Component({\n    components: {\n      'cuckoo-plus-header': Header,\n      'cuckoo-plus-drawer': Drawer,\n      'theme-edit-panel': ThemeEditPanel\n    }\n  })\n  class App extends Vue {\n\n    $route\n\n    @State('appStatus') appStatus\n    @State('timelines') timelines\n    @State('contextMap') contextMap\n    @State('statusMap') statusMap\n    @State('cardMap') cardMap\n\n    @Mutation('updateDocumentWidth') updateDocumentWidth\n\n    @Getter('isOAuthUser') isOAuthUser\n    @Getter('isMobileMode') isMobileMode\n\n    mounted () {\n      window.addEventListener('resize', _.debounce(() => this.updateDocumentWidth(), 200))\n      this.listenToWindowUnload()\n    }\n\n    @Watch('appStatus.unreadNotificationCount')\n    onUnreadNotificationCountChanged () {\n      document.querySelector('title').innerText = this.appStatus.unreadNotificationCount > 0 ?\n        `(${this.appStatus.unreadNotificationCount}) ${TITLE}` : `${TITLE}`\n    }\n\n    get appContentStyle () {\n      if (this.appStatus.isDrawerOpened &&\n        !this.$route.meta.hideDrawer &&\n        this.isOAuthUser && !this.isMobileMode) {\n        return {\n          paddingLeft: `${UiWidthCheckConstants.DRAWER_DESKTOP_WIDTH}px`\n        }\n      }\n    }\n\n    listenToWindowUnload () {\n      window.addEventListener('unload', () => {\n        // save timelines\n        localStorage.setItem(TimeLineTypes.HOME, JSON.stringify(this.timelines[TimeLineTypes.HOME]))\n\n        // save contextMap\n        localStorage.setItem('contextMap', JSON.stringify(this.contextMap))\n\n        // save statusMap\n        localStorage.setItem('statusMap', JSON.stringify(this.statusMap))\n\n        localStorage.setItem('cardMap', JSON.stringify(this.cardMap))\n      })\n    }\n  }\n\n  export default App\n</script>\n\n<style lang=\"less\" scoped>\n  .app-content {\n    padding: 56px 0 0 0;\n    -webkit-transition: padding-left .45s cubic-bezier(.23,1,.32,1);\n    -moz-transition: padding-left .45s cubic-bezier(.23,1,.32,1);\n    -ms-transition: padding-left .45s cubic-bezier(.23,1,.32,1);\n    -o-transition: padding-left .45s cubic-bezier(.23,1,.32,1);\n    transition: padding-left .45s cubic-bezier(.23,1,.32,1);\n  }\n\n  @media (min-width: 600px) {\n    .app-content {\n      padding: 64px 0 0 0;\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  body {\n    height: 100%;\n    font-family: Roboto,RobotoDraft,Helvetica,Arial,sans-serif;\n  }\n\n  a, .mu-load-more {\n    -webkit-user-select: auto;\n    -moz-user-select: auto;\n    -ms-user-select: auto;\n    user-select: auto;\n  }\n\n  // header z-index 20141223\n  // drawer z-index 20141224\n\n  .mu-loading-wrap {\n    z-index: 20141222 !important;\n  }\n\n  .drag-over-layer {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .custom-emoji {\n    width: 20px;\n    height: 20px;\n    vertical-align: text-bottom;\n  }\n\n  .netease-music-iframe {\n    display: block;\n    width: 100%;\n  }\n\n  .youtube-video-iframe {\n    display: block;\n    width: 100%;\n  }\n\n  @keyframes fadein {\n    from { opacity: 0; }\n    to   { opacity: 1; }\n  }\n</style>\n"
  },
  {
    "path": "src/api/accounts.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri } from '@/util'\n\ninterface updateAccountFormData {\n  // The name to display in the user's profile\n  display_name?: string\n  // A new biography for the user\n  note?: string\n  // An avatar for the user (encoded using multipart/form-data)\n  avatar?: FormData\n  // A header image for the user (encoded using multipart/form-data)\n  header?: FormData\n  // Manually approve followers?\n  locked?: boolean\n  // (2.4 or later) extra source attribute from verify_credentials\n  source?: {\n    privacy?: string\n    sensitive?: boolean\n    note?: string\n    fields?: Array<any>\n  }\n}\n\nasync function fetchAccountInfoById () {\n\n}\n\nasync function fetchCurrentUserAccountInfo (): Promise<{ data: mastodonentities.AuthenticatedAccount }> {\n  return Vue.http.get(patchApiUri('/api/v1/accounts/verify_credentials')) as any\n}\n\nasync function updateUserAccountInfo (formData: updateAccountFormData): Promise<{ data: mastodonentities.AuthenticatedAccount }> {\n  return Vue.http.patch(patchApiUri('/api/v1/accounts/update_credentials'), formData) as any\n}\n\nasync function fetchRelationships (idList: Array<string>) {\n  return Vue.http.get(patchApiUri('/api/v1/accounts/relationships'), {\n    params: {\n      id: idList\n    }\n  }) as any\n}\n\nasync function followAccountById (id: string) {\n  return Vue.http.post(patchApiUri(`/api/v1/accounts/${id}/follow`)) as any\n}\n\nasync function unFollowAccountById (id: string) {\n  return Vue.http.post(patchApiUri(`/api/v1/accounts/${id}/unfollow`)) as any\n}\n\nexport {\n  fetchAccountInfoById,\n  fetchCurrentUserAccountInfo,\n  updateUserAccountInfo,\n  fetchRelationships,\n  followAccountById,\n  unFollowAccountById\n}\n"
  },
  {
    "path": "src/api/apps.ts",
    "content": "import Vue from 'vue'\nimport { patchApiUri } from '@/util'\n\nconst clientName = 'Cuckoo.Plus'\nconst scopes = 'read write follow'\n\nnamespace Apps {\n\n  export interface registerApplicationFormData {\n    // Name of your application\n    client_name?: string\n    // Where the user should be redirected after authorization\n    // (for no redirect, use urn:ietf:wg:oauth:2.0:oob)\n    redirect_uris?: string\n    // This can be a space-separated list of the following items: \"read\", \"write\" and \"follow\"\n    scopes?: string\n    // URL to the homepage of your app\n    website?: string\n  }\n\n  export interface registerApplicationReturnData {\n    data: {\n      client_id: string,\n      client_secret: string,\n      id: string,\n      name: string,\n      redirect_uri: string,\n      website: string | null\n    }\n  }\n\n}\n\nasync function registerApplication (): Promise<Apps.registerApplicationReturnData> {\n  const formData: Apps.registerApplicationFormData = {\n    client_name: clientName,\n    redirect_uris: location.origin,\n    scopes: scopes\n  }\n\n  return Vue.http.post(patchApiUri('/api/v1/apps'), formData) as any\n}\n\nexport {\n  registerApplication\n}\n"
  },
  {
    "path": "src/api/index.ts",
    "content": "import * as apps from './apps'\nimport * as oauth from './oauth'\nimport * as accounts from './accounts'\nimport * as lists from './lists'\nimport * as timelines from './timelines'\nimport * as statuses from './statuses'\nimport * as notifications from './notifications'\nimport * as media from './media'\nimport * as instances from './instances'\nimport * as search from './search'\nimport streaming from './streaming'\n\nexport {\n  apps,\n  oauth,\n  accounts,\n  lists,\n  timelines,\n  statuses,\n  notifications,\n  media,\n  streaming,\n  instances,\n  search\n}\n"
  },
  {
    "path": "src/api/instances.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri } from '@/util'\n\nasync function getCustomEmojis (): Promise<{ data: Array<mastodonentities.Emoji> }> {\n  return Vue.http.get(patchApiUri('/api/v1/custom_emojis')) as any\n}\n\nexport {\n  getCustomEmojis\n}\n"
  },
  {
    "path": "src/api/lists.ts",
    "content": "import Vue from 'vue'\nimport { patchApiUri } from '@/util'\n\nasync function receiveLists () {\n  return Vue.http.get(patchApiUri('/api/v1/instance'))\n}\n\nexport {\n  receiveLists\n}\n"
  },
  {
    "path": "src/api/media.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri } from '@/util'\n\nasync function postMediaFile (formData): Promise<{ data: mastodonentities.Attachment }> {\n  return Vue.http.post(patchApiUri('/api/v1/media'), formData) as any\n}\n\nexport {\n  postMediaFile\n}\n"
  },
  {
    "path": "src/api/notifications.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri } from '@/util'\n\ninterface getNotificationsQueryParams {\n  // Get a list of notifications with ID less than this value\n  max_id?: string\n  // Get a list of notifications with ID greater than this value\n  since_id?: string\n  // Maximum number of notifications to get (Default 15, Max 30)\n  limit?: number\n  // Array of notifications to exclude (Allowed values: \"follow\", \"favourite\", \"reblog\", \"mention\")\n  exclude_types?: Array<mastodonentities.NotificationType>\n}\n\nasync function getNotifications(queryParams: getNotificationsQueryParams): Promise<{ data: Array<mastodonentities.Notification> }> {\n  queryParams.limit = 30\n  const config = {\n    params: queryParams\n  }\n\n  return Vue.http.get(patchApiUri('/api/v1/notifications'), config) as any\n}\n\nexport {\n  getNotifications\n}\n"
  },
  {
    "path": "src/api/oauth.ts",
    "content": "import Vue from 'vue'\nimport store from '@/store'\nimport { patchApiUri } from '@/util'\nimport HttpResponse = vuejs.HttpResponse;\n\ninterface fetchOAuthTokenReturnData extends HttpResponse {\n  data: {\n    access_token: string\n  }\n}\n\nasync function fetchOAuthToken (): Promise<fetchOAuthTokenReturnData> {\n  const OAuthInfo = store.state.OAuthInfo\n\n  const formData = {\n    client_id: OAuthInfo.clientId,\n    client_secret: OAuthInfo.clientSecret,\n    redirect_uri: location.origin,\n    grant_type: \"authorization_code\",\n    code: OAuthInfo.code\n  }\n\n  return await Vue.http.post(patchApiUri('/oauth/token'), formData) as any\n}\n\nexport {\n  fetchOAuthToken\n}\n"
  },
  {
    "path": "src/api/search.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri } from '@/util'\n\nlet preSearchRequest\n\n/**\n * @param q The search query\n * @param resolve Whether to resolve non-local accounts (default: don't resolve)\n * */\nasync function getSearchResults (q: string, resolve: boolean = false): Promise<{ data: mastodonentities.SearchResults }> {\n  return Vue.http.get(patchApiUri('/api/v1/search'), {\n    params: {\n      q, resolve\n    },\n    before(request) {\n      abortSearch()\n      preSearchRequest = request\n    }\n  }) as any\n}\n\nfunction abortSearch () {\n  if (preSearchRequest) preSearchRequest.abort()\n}\n\nexport {\n  getSearchResults,\n  abortSearch\n}\n"
  },
  {
    "path": "src/api/statuses.ts",
    "content": "import Vue from 'vue'\nimport { mastodonentities } from '@/interface'\nimport { patchApiUri, generateUniqueKey } from '@/util'\nimport { VisibilityTypes } from '@/constant'\n\nasync function getStatusById (id: string): Promise<{ data: mastodonentities.Status }> {\n  return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}`)) as any\n}\n\ninterface postStatusFormData {\n  // The text of the status\n  status: string\n  // local ID of the status you want to reply to\n  inReplyToId?: string\n  // Array of media IDs to attach to the status (maximum 4)\n  mediaIds?: Array<string>\n  // Set this to mark the media of the status as NSFW\n  sensitive?: boolean\n  // Text to be shown as a warning before the actual content\n  spoilerText?: string\n  // Either \"direct\", \"private\", \"unlisted\" or \"public\"\n  visibility?: string\n  // ISO 639-2 language code of the toot, to skip automatic detection\n  language?: string\n}\n\nasync function postStatus (formData: postStatusFormData): Promise<{ data: mastodonentities.Status }> {\n  const apiFormData: any = {}\n\n  apiFormData.status = formData.status\n  apiFormData.in_reply_to_id = formData.inReplyToId\n  apiFormData.media_ids = formData.mediaIds\n  apiFormData.sensitive = formData.sensitive\n  apiFormData.spoiler_text = formData.spoilerText\n  apiFormData.visibility = formData.visibility || VisibilityTypes.PUBLIC\n  apiFormData.language = formData.language\n\n  const config = {\n    headers: {\n      'Idempotency-Key': generateUniqueKey()\n    }\n  }\n\n  return Vue.http.post(patchApiUri(`/api/v1/statuses`), apiFormData, config) as any\n}\n\nasync function getStatusContextById (id: string): Promise<{ data: mastodonentities.Context }> {\n  return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/context`)) as any\n}\n\nasync function getReBloggedAccountsById (id: string): Promise<{ data: Array<mastodonentities.Account> }> {\n  return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/reblogged_by`)) as any\n}\n\nasync function getFavouritedAccountsById (id: string): Promise<{ data: Array<mastodonentities.Account> }> {\n  return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/favourited_by`)) as any\n}\n\nasync function favouriteStatusById (id: string): Promise<{ data: mastodonentities.Status }> {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/favourite`)) as any\n}\n\nasync function unFavouriteStatusById (id: string): Promise<{ data: mastodonentities.Status }> {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unfavourite`)) as any\n}\n\nasync function reblogStatusById (id: string): Promise<{ data: mastodonentities.Status }> {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/reblog`)) as any\n}\n\nasync function unReblogStatusById (id: string): Promise<{ data: mastodonentities.Status }> {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unreblog`)) as any\n}\n\nasync function deleteStatusById (id: string) {\n  return Vue.http.delete(patchApiUri(`/api/v1/statuses/${id}`)) as any\n}\n\nasync function muteStatusById (id: string) {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/mute`)) as any\n}\n\nasync function unMuteStatusById (id: string) {\n  return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unmute`)) as any\n}\n\nasync function getStatusCardInfoById (id: string): Promise<{ data: mastodonentities.Card }> {\n  return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/card`)) as any\n}\n\nexport {\n  getStatusById,\n  postStatus,\n  getStatusContextById,\n  getReBloggedAccountsById,\n  getFavouritedAccountsById,\n  favouriteStatusById,\n  unFavouriteStatusById,\n  reblogStatusById,\n  unReblogStatusById,\n  deleteStatusById,\n  muteStatusById,\n  unMuteStatusById,\n  getStatusCardInfoById\n}\n"
  },
  {
    "path": "src/api/streaming.ts",
    "content": "import store from '@/store'\nimport { StreamingEventTypes, TimeLineTypes, NotificationTypes, RoutersInfo, I18nTags } from '@/constant'\nimport { mastodonentities } from \"@/interface\"\nimport router from '@/router'\nimport { extractText, prepareRootStatus } from \"@/util\"\nimport i18n from '@/i18n'\n\nclass NotificationHandler {\n  public emit (newNotification: mastodonentities.Notification) {\n    switch (newNotification.type) {\n      case NotificationTypes.MENTION : {\n        return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.mentioned_you))\n      }\n\n      case NotificationTypes.REBLOG : {\n        return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.boosted_your_status))\n      }\n\n      case NotificationTypes.FAVOURITE : {\n        // update status info\n        store.dispatch('fetchStatusById', newNotification.status.id)\n\n        return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.favourited_your_status))\n      }\n\n      case NotificationTypes.FOLLOW : {\n        store.dispatch('updateRelationships', { idList: [newNotification.account.id] })\n\n        return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.someone_followed_you))\n      }\n    }\n  }\n\n  private emitStatusOperateNotification (newNotification: mastodonentities.Notification, operateTypeString) {\n    const title = `${this.getFromName(newNotification)} ${operateTypeString}`\n    const bodyText =  newNotification.status ? extractText(newNotification.status.content) : ''\n\n    // ignore all muted status's notification\n    if (newNotification.status && (store.state.appStatus.settings.muteMap.statusList.indexOf(newNotification.status) !== -1)) return\n\n    if (store.state.appStatus.settings.muteMap.userList.indexOf(newNotification.account.id) !== -1) return\n\n    const nativeNotification = new Notification(title, { body: bodyText, icon: this.getImageUrl(newNotification) })\n\n    nativeNotification.addEventListener('click', () => {\n      if (store.state.appStatus.unreadNotificationCount > 0) {\n        store.commit('updateUnreadNotificationCount', store.state.appStatus.unreadNotificationCount - 1)\n      }\n\n      this.routeToTargetStatus(newNotification)\n    })\n  }\n\n  private getFromName (newNotification: mastodonentities.Notification): string {\n    // account's display name have been formatted\n    return store.getters['getAccountDisplayName'](newNotification.account)\n      .replace('<span>', '').replace('</span>', '')\n  }\n\n  private getImageUrl (newNotification: mastodonentities.Notification): string {\n    return newNotification.account.avatar\n  }\n\n  private async routeToTargetStatus (newNotification: mastodonentities.Notification) {\n    const targetStatus = await prepareRootStatus(newNotification.status)\n\n    router.push({\n      name: RoutersInfo.statuses.name,\n      params: {\n        statusId: targetStatus.id\n      }\n    })\n  }\n\n  private routeToTargetAccount () {\n\n  }\n}\n\nconst notificationHandler = new NotificationHandler()\n\nclass Streaming {\n\n  private userStreamWs: WebSocket\n\n  private localStreamWs: WebSocket\n\n  private publicStreamWs: WebSocket\n\n  private createWsUrl (streamName: string) {\n    return `wss://${new URL(store.state.mastodonServerUri).hostname}/api/v1/streaming/?stream=${streamName}&access_token=${store.state.OAuthInfo.accessToken}`\n  }\n\n  public openUserConnection () {\n    const wsUrl = this.createWsUrl('user')\n\n    this.userStreamWs = new WebSocket(wsUrl)\n\n    this.initEventListener(this.userStreamWs, TimeLineTypes.HOME)\n  }\n\n  public openLocalConnection () {\n    const wsUrl = this.createWsUrl('public:local')\n\n    this.localStreamWs = new WebSocket(wsUrl)\n\n    this.initEventListener(this.localStreamWs, TimeLineTypes.LOCAL)\n  }\n\n  public openPublicConnection () {\n    const wsUrl = this.createWsUrl('public')\n\n    this.publicStreamWs = new WebSocket(wsUrl)\n\n    this.initEventListener(this.publicStreamWs, TimeLineTypes.PUBLIC)\n  }\n\n  public closeConnection (timeLineType: string) {\n    const typeToWsMap = {\n      [TimeLineTypes.HOME]: this.userStreamWs,\n      [TimeLineTypes.LOCAL]: this.localStreamWs,\n      [TimeLineTypes.PUBLIC]: this.publicStreamWs\n    }\n\n    typeToWsMap[timeLineType].close()\n  }\n\n  private initEventListener (targetWs: WebSocket, timeLineType, hashName?) {\n    targetWs.onmessage = (message) => {\n     if(message.data.length) {\n      const parsedMessage = JSON.parse(message.data)\n\n      switch (parsedMessage.event) {\n        case StreamingEventTypes.UPDATE : {\n          return this.updateStatus(JSON.parse(parsedMessage.payload), timeLineType, hashName)\n        }\n\n        case StreamingEventTypes.DELETE : {\n          return this.deleteStatus(parsedMessage.payload)\n        }\n\n        case StreamingEventTypes.NOTIFICATION : {\n          return this.emitNotification(JSON.parse(parsedMessage.payload))\n        }\n      }\n     }\n    }\n  }\n\n  private updateStatus (newStatus: mastodonentities.Status, timeLineType, hashName?) {\n    if (store.state.statusMap[newStatus.id]) return\n\n    // update status map\n    store.commit('updateStatusMap', { [newStatus.id]: newStatus })\n    store.dispatch('updateCardMap', newStatus.id)\n    if (timeLineType === TimeLineTypes.HOME) {\n      prepareRootStatus(newStatus)\n    }\n\n    // update target timeline list\n    const targetMutationName = store.state.appStatus.settings.realTimeLoadStatusMode ? 'unShiftTimeLineStatuses' : 'unShiftStreamStatusesPool'\n    store.commit(targetMutationName, {\n      newStatusIdList: [newStatus.id],\n      timeLineType, hashName\n    })\n\n  }\n\n  private deleteStatus (statusId: string) {\n    if (!store.state.statusMap[statusId]) return\n\n    // remove from time line\n    store.commit('deleteStatusFromTimeLine', statusId)\n\n    // remove from status map\n    store.commit('removeStatusFromStatusMapById', statusId)\n  }\n\n  private emitNotification (newNotification: mastodonentities.Notification) {\n    // update notification list\n    store.commit('unShiftNotification', [newNotification])\n\n    // set notification icon unread\n    store.commit('updateUnreadNotificationCount', store.state.appStatus.unreadNotificationCount + 1)\n\n    // send browser notification\n    // @ts-ignore\n    if (window.Notification) {\n      notificationHandler.emit(newNotification)\n    }\n  }\n\n}\n\nexport default new Streaming()\n"
  },
  {
    "path": "src/api/timelines.ts",
    "content": "import Vue from 'vue'\nimport { patchApiUri, isBaseTimeLine } from '@/util'\nimport { TimeLineTypes } from '@/constant'\nimport { mastodonentities } from '@/interface'\n\nconst allTimeLineTypeList = [\n  TimeLineTypes.HOME, TimeLineTypes.PUBLIC, TimeLineTypes.DIRECT, TimeLineTypes.LOCAL,\n  TimeLineTypes.TAG, TimeLineTypes.LIST\n]\n\nasync function getTimeLineStatuses ({ timeLineType = '', maxId = '', sinceId = '', hashName = '', limit = 40, local = false} = {}): Promise<{ data: Array<mastodonentities.Status> }> {\n  if (allTimeLineTypeList.indexOf(timeLineType) === -1) throw new Error('unknown timeline type!')\n\n  let urlFragmentString = ''\n  if (isBaseTimeLine(timeLineType)) {\n    urlFragmentString = timeLineType\n\n  } else {\n    if (!hashName) throw new Error('need a hash name!')\n    urlFragmentString = `${timeLineType}/${hashName}`\n  }\n  const params: any = { limit: limit }\n  if (maxId) params.max_id = maxId\n  if (sinceId) params.since_id = sinceId\n  if (local) params.local = true\n  if (timeLineType === TimeLineTypes.LOCAL) {\n    urlFragmentString = TimeLineTypes.PUBLIC\n    params.local = true\n  }\n  return Vue.http.get(patchApiUri(`/api/v1/timelines/${urlFragmentString}`), {\n    params\n  }) as any\n}\n\nexport {\n  getTimeLineStatuses\n}\n"
  },
  {
    "path": "src/components/Drawer/PeopleResultCard.vue",
    "content": "<template>\n  <mu-list-item class=\"people-result-card\" avatar :ripple=\"false\" v-loading=\"isLoading\" data-mu-loading-size=\"36\">\n    <mu-list-item-action>\n      <mu-avatar class=\"people-result-card-avatar\" @click=\"onCheckUserAccountPage(account)\">\n        <img :src=\"account.avatar\" />\n      </mu-avatar>\n    </mu-list-item-action>\n    <mu-list-item-content class=\"people-result-card-content ellipsis-text\" @click=\"onCheckUserAccountPage(account)\">\n      <mu-list-item-title class=\"user-display-name primary-read-text-color\"\n                          v-html=\"getAccountDisplayName(account)\" />\n      <mu-list-item-sub-title class=\"user-at-name secondary-read-text-color\"\n                              v-html=\"`@${getAccountAtName(account)}`\" />\n    </mu-list-item-content>\n    <mu-list-item-action v-if=\"currentUserAccount.id !== account.id && relationships[account.id] && !relationships[account.id].following\">\n      <mu-icon class=\"operate-btn\" value=\"person_add\" @click=\"onFollowingAccount\"/>\n    </mu-list-item-action>\n    <mu-list-item-action v-if=\"currentUserAccount.id !== account.id && relationships[account.id] && relationships[account.id].following\">\n      <mu-icon class=\"operate-btn secondary-theme-text-color\" value=\"person_add_disabled\" @click=\"onUnFollowingAccount\"/>\n    </mu-list-item-action>\n  </mu-list-item>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { State, Action, Getter } from 'vuex-class'\n  import { mastodonentities } from '@/interface'\n\n  @Component({})\n  class PeopleResultCard extends Vue {\n\n    isLoading: boolean = false\n\n    @Prop() account: mastodonentities.Account\n\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n\n    @State('relationships') relationships: {\n      [id: string]: mastodonentities.Relationship\n    }\n\n    @Getter('getAccountDisplayName') getAccountDisplayName\n    @Getter('getAccountAtName') getAccountAtName\n\n    @Action('followAccountById') followAccountById\n    @Action('unFollowAccountById') unFollowAccountById\n\n    onCheckUserAccountPage (account: mastodonentities.Account) {\n      window.open(account.url, \"_blank\")\n    }\n\n    async onFollowingAccount () {\n      this.isLoading = true\n      await this.followAccountById(this.account.id)\n      this.isLoading = false\n    }\n\n    async onUnFollowingAccount () {\n      this.isLoading = true\n      await this.unFollowAccountById(this.account.id)\n      this.isLoading = false\n    }\n  }\n\n  export default PeopleResultCard\n</script>\n\n<style lang=\"less\" scoped>\n  .ellipsis-text {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .operate-btn {\n    cursor: pointer;\n  }\n\n  .people-result-card {\n    position: relative;\n\n    .people-result-card-avatar {\n      cursor: pointer;\n    }\n\n    .people-result-card-content {\n      cursor: pointer;\n\n      .user-display-name {\n        display: inline;\n      }\n\n      &:hover {\n        .user-display-name, .user-at-name {\n          text-decoration: underline;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Drawer/Search.vue",
    "content": "<template>\n  <div class=\"search-area-container\">\n    <div class=\"search-bar\">\n      <mu-icon value=\"search\" style=\"margin-right: 10px\"/>\n      <mu-text-field class=\"search-input\" v-model=\"searchKey\"\n                     @keydown.stop=\"onKeyDown\"\n                     @keydown.enter.stop=\"onSearch\" :placeholder=\"$t($i18nTags.drawer.search_input_placeholder)\"\n                     :action-icon=\"shouldShowSearchActionIcon ? 'search' : 'cancel'\"\n                     :action-click=\"onSearchInputActionClick\"/>\n    </div>\n    <div class=\"search-results default-theme-bg-color\" :style=\"resultPanelStyle\">\n      <mu-list>\n        <mu-sub-header>{{$t($i18nTags.drawer.search_result_people_label)}}</mu-sub-header>\n        <people-result-card v-for=\"(account, index) in searchResults.accounts\" :account=\"account\" :key=\"index\"/>\n      </mu-list>\n\n      <mu-divider></mu-divider>\n\n      <mu-list>\n\n        <mu-sub-header>{{$t($i18nTags.drawer.search_result_hashtag_label)}}</mu-sub-header>\n\n        <mu-list-item v-for=\"(hashTag, index) in searchResults.hashtags\" :key=\"index\"\n                      class=\"hashtag-result-card\" :ripple=\"false\">\n          <mu-list-item-title class=\"hash-tag ellipsis-text primary-read-text-color\"\n                              v-html=\"hashTag\" @click=\"onCheckHashTagTimeLine(hashTag)\"/>\n          <mu-list-item-action v-if=\"!appStatus.settings.tags.includes(hashTag)\">\n            <mu-icon class=\"operate-btn\" value=\"playlist_add\" @click=\"onSaveHashTag(hashTag)\"/>\n          </mu-list-item-action>\n        </mu-list-item>\n\n      </mu-list>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component } from 'vue-property-decorator'\n  import { Getter, State, Action, Mutation } from 'vuex-class'\n  import * as Api from '@/api'\n  import { mastodonentities } from '@/interface'\n  import { UiWidthCheckConstants } from '@/constant'\n  import PeopleResultCard from './PeopleResultCard'\n\n  @Component({\n    components: {\n      'people-result-card': PeopleResultCard\n    }\n  })\n  class Search extends Vue {\n\n    $progress\n\n    $t\n\n    $i18nTags\n\n    $router\n\n    $routersInfo\n\n    @State('relationships') relationships: {\n      [id: string]: mastodonentities.Relationship\n    }\n    @State('appStatus') appStatus\n\n    @Getter('isMobileMode') isMobileMode\n\n    @Action('updateRelationships') updateRelationships\n\n    @Mutation('updateDrawerOpenStatus') updateDrawerOpenStatus\n    @Mutation('updateTags') updateTags\n\n    searchKey: string = ''\n\n    currentSearchKey: string = ''\n\n    shouldShowResultPanel: boolean = false\n\n    searchResults: mastodonentities.SearchResults = {\n      accounts: [],\n      hashtags: [],\n      statuses: []\n    }\n\n    get shouldShowSearchActionIcon () {\n      return !this.searchKey.length && !this.shouldShowResultPanel\n    }\n\n    get resultPanelStyle () {\n      return {\n        height: `calc(100vh - 68px${this.isMobileMode ? '' : ' - 64px'})`,\n        left: this.shouldShowResultPanel ? '0' : `-${this.isMobileMode ? UiWidthCheckConstants.DRAWER_MOBILE_WIDTH : UiWidthCheckConstants.DRAWER_DESKTOP_WIDTH}px`\n      }\n    }\n\n    async onSearchInputActionClick () {\n      if (this.shouldShowSearchActionIcon) return\n\n      this.searchKey = ''\n      this.currentSearchKey = ''\n      this.shouldShowResultPanel = false\n    }\n\n    async onSearch () {\n      if (this.searchKey === this.currentSearchKey) return\n\n      this.currentSearchKey = this.searchKey\n\n      this.$progress.start()\n\n      try {\n        const result = await Api.search.getSearchResults(this.searchKey)\n\n        this.searchResults = result.data\n\n        this.updateRelationship()\n\n        this.shouldShowResultPanel = true\n\n        this.$progress.done()\n\n      } catch (e) {\n        this.$progress.done()\n      }\n    }\n\n    updateRelationship () {\n      const newAccountResultList = this.searchResults.accounts.filter(account => !this.relationships[account.id])\n      this.updateRelationships({ idList: newAccountResultList.map(account => account.id) })\n    }\n\n    onCheckHashTagTimeLine (hashTagName: string) {\n      this.$router.push({\n        name: this.$routersInfo.tagtimelines.name,\n        params: {\n          tagName: hashTagName\n        }\n      })\n\n      if (this.isMobileMode) this.updateDrawerOpenStatus(false)\n    }\n\n    onSaveHashTag (hashTagName: string) {\n      this.updateTags([...this.appStatus.settings.tags, hashTagName])\n    }\n\n    onKeyDown () {}\n  }\n\n  export default Search\n</script>\n\n<style lang=\"less\" scoped>\n\n  .ellipsis-text {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n  .operate-btn {\n    cursor: pointer;\n  }\n\n  .search-area-container {\n    position: relative;\n\n    .search-bar {\n      display: flex;\n      align-items: center;\n      height: 68px;\n      padding: 0 16px;\n\n      .search-input {\n        min-height: unset;\n        margin: 0;\n        padding: 0;\n      }\n    }\n\n    .search-results {\n      position: absolute;\n      // todo 在iPhone X上失效\n      overflow: auto;\n      -webkit-overflow-scrolling: touch;\n      top: 68px;\n      width: 100%;\n      z-index: 1;\n      -webkit-transition: left .45s cubic-bezier(.23,1,.32,1);\n      -moz-transition: left .45s cubic-bezier(.23,1,.32,1);\n      -ms-transition: left .45s cubic-bezier(.23,1,.32,1);\n      -o-transition: left .45s cubic-bezier(.23,1,.32,1);\n      transition: left .45s cubic-bezier(.23,1,.32,1);\n\n      .hashtag-result-card {\n        .hash-tag {\n          cursor: pointer;\n\n          &:hover {\n            text-decoration: underline;\n          }\n        }\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .search-area-container {\n    .search-input {\n      .mu-input-line, .mu-input-focus-line {\n        display: none;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Drawer/index.vue",
    "content": "<template>\n  <mu-drawer class=\"cuckoo-drawer default-theme-bg-color primary-read-text-color\" :open.sync=\"appStatus.isDrawerOpened\" :style=\"drawerStyle\"\n             :docked=\"shouldDrawerDocked\" :z-depth=\"shouldDrawerDocked ? 0 : 16\">\n\n    <search />\n\n    <mu-divider />\n\n    <mu-list :value=\"currentListValue\" toggle-nested>\n\n      <mu-list-item button v-for=\"(info, index) in baseRouterInfoList\"\n                    :value=\"info.value\"\n                    :nested=\"!!info.hashList\" :ripple=\"!info.hashList\"\n                    :key=\"index\" @click=\"!info.hashList && onBaseRouteItemClick(info.value)\">\n        <mu-list-item-action>\n          <mu-icon :value=\"info.icon\"/>\n        </mu-list-item-action>\n        <mu-list-item-title>{{$t(info.title)}}</mu-list-item-title>\n\n        <mu-list-item-action v-if=\"!!info.hashList\">\n          <mu-icon class=\"toggle-icon\" size=\"24\" value=\"keyboard_arrow_down\" />\n        </mu-list-item-action>\n\n        <mu-list-item class=\"hash-list-item\" v-if=\"info.hashList\" slot=\"nested\" button\n                      v-for=\"(hashName, index) in info.hashList\"\n                      :value=\"info.to + '/' + hashName\"\n                      :key=\"index\" @click=\"onHashRouteItemClick(info.value, hashName)\">\n          <mu-list-item-title># {{hashName}}</mu-list-item-title>\n          <mu-list-item-action>\n            <mu-button class=\"delete-hash-btn\" icon @click.stop=\"onDeleteHash(hashName)\">\n              <mu-icon value=\"delete\" />\n            </mu-button>\n          </mu-list-item-action>\n        </mu-list-item>\n\n      </mu-list-item>\n\n    </mu-list>\n\n    <mu-divider />\n\n    <mu-list class=\"secondary-list\">\n      <mu-list-item button :to=\"$routersInfo.settings.path\" @click=\"onSecondaryItemClick\">\n        <mu-list-item-title class=\"secondary-read-text-color\">{{$t($i18nTags.drawer.settings)}}</mu-list-item-title>\n      </mu-list-item>\n    </mu-list>\n\n    <div class=\"bottom-info-area secondary-read-text-color\">\n      <div style=\"margin-bottom: 6px\">\n        <a class=\"secondary-read-text-color\">\n          ©{{(new Date().getFullYear()).toString().split('').reverse().join('') }} Cuckoo</a>\n        •\n        <a class=\"secondary-read-text-color link-text\" href=\"https://github.com/NanaMorse/Cuckoo.Plus\" target=\"_blank\">Github</a>\n      </div>\n      <a class=\"secondary-read-text-color link-text\" :href=\"mastodonServerUri\" target=\"_blank\">{{$t($i18nTags.drawer.toHostInstance)}}</a>\n      <div style=\"margin-top: 6px\">\n        <a class=\"secondary-read-text-color link-text\" @click=\"onTryLogout\">{{$t($i18nTags.drawer.logout)}}</a>\n      </div>\n    </div>\n\n  </mu-drawer>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Mutation, Action } from 'vuex-class'\n  import { isBaseTimeLine } from '@/util'\n  import { TimeLineTypes, UiWidthCheckConstants, RoutersInfo, I18nTags } from '@/constant'\n  import Search from './Search'\n  import store from '@/store'\n\n  const baseRouterInfoList = [\n    {\n      value: TimeLineTypes.HOME,\n      title: I18nTags.drawer.home,\n      icon: 'home',\n      to: '/timelines/home'\n    },\n    {\n      value: TimeLineTypes.PUBLIC,\n      title: I18nTags.drawer.public,\n      icon: 'public',\n      to: '/timelines/public'\n    },\n    {\n      value: TimeLineTypes.LOCAL,\n      title: I18nTags.drawer.local,\n      icon: 'people',\n      to: '/timelines/local'\n    },\n    {\n      value: TimeLineTypes.TAG,\n      title: I18nTags.drawer.tag,\n      icon: 'loyalty',\n      to: '/timelines/tag',\n      hashList: []\n    },\n    {\n      value: 'profile',\n      title: I18nTags.drawer.profile,\n      icon: 'person'\n    }\n  ]\n\n  @Component({\n    components: {\n      'search': Search\n    }\n  })\n  class Drawer extends Vue {\n\n    $route\n\n    $router\n\n    $routersInfo\n\n    $progress\n\n    $toast\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    @State('currentUserAccount') currentUserAccount\n\n    @State('appStatus') appStatus\n\n    @State('mastodonServerUri') mastodonServerUri\n\n    @Mutation('updateDrawerOpenStatus') updateDrawerOpenStatus\n\n    @Mutation('updateTags') updateTags\n\n    @Action('updateTimeLineStatuses') updateTimeLineStatuses\n\n    @Watch('shouldDrawerDocked')\n    onShouldDrawerDockedChanged () {\n      if (!this.shouldDrawerDocked && this.appStatus.isDrawerOpened) {\n        this.updateDrawerOpenStatus(false)\n      }\n    }\n\n    get shouldDrawerDocked () {\n      return this.appStatus.documentWidth > UiWidthCheckConstants.DRAWER_DOCKING_BOUNDARY\n    }\n\n    get baseRouterInfoList () {\n      // @ts-ignore\n      baseRouterInfoList.find(info => info.value === TimeLineTypes.TAG).hashList = this.appStatus.settings.tags\n\n      return baseRouterInfoList\n    }\n\n    get drawerStyle () {\n      if (this.shouldDrawerDocked) {\n        return {\n          top: '64px',\n          width: `${UiWidthCheckConstants.DRAWER_DESKTOP_WIDTH}px`\n        }\n      } else {\n        return {\n          width: `${UiWidthCheckConstants.DRAWER_MOBILE_WIDTH}px`\n        }\n      }\n    }\n\n    get currentListValue () {\n      if (this.$route.name === RoutersInfo.tagtimelines.name) {\n        return this.$route.path\n      } else {\n        const currentRouterInfo = baseRouterInfoList.find(routerInfo => routerInfo.to === this.$route.path)\n\n        if (currentRouterInfo) return currentRouterInfo.value\n      }\n    }\n\n    async onBaseRouteItemClick (clickedRouterValue: string) {\n      if (clickedRouterValue === 'profile') {\n        // todo\n        // this.$router.push({\n        //   name: this.$routersInfo.accounts.name,\n        //   params: {\n        //     accountId: this.currentUserAccount.id\n        //   }\n        // })\n\n        return window.open(this.currentUserAccount.url, '_blank')\n      } else {\n\n        const targetPath = baseRouterInfoList.find(routerInfo => routerInfo.value === clickedRouterValue).to\n\n        if (isBaseTimeLine(clickedRouterValue) && (targetPath === this.$route.path)) {\n          this.fetchTimeLineStatuses(clickedRouterValue)\n        }\n\n        if (!this.shouldDrawerDocked) this.updateDrawerOpenStatus(false)\n\n        this.$router.push(targetPath)\n\n        window.scrollTo(0, 0)\n      }\n    }\n\n    async onHashRouteItemClick (clickedRouterValue: string, hashName: string) {\n      const targetPath = baseRouterInfoList.find(routerInfo => routerInfo.value === clickedRouterValue).to + '/' + hashName\n\n      if (targetPath === this.$route.path) {\n        this.fetchTimeLineStatuses(clickedRouterValue, hashName)\n      }\n\n      if (!this.shouldDrawerDocked) this.updateDrawerOpenStatus(false)\n\n      this.$router.push(targetPath)\n\n      window.scrollTo(0, 0)\n    }\n\n    onSecondaryItemClick () {\n      if (!this.shouldDrawerDocked) this.updateDrawerOpenStatus(false)\n      window.scrollTo(0, 0)\n    }\n\n    async onTryLogout () {\n      const doLogout = (await this.$confirm(this.$t(this.$i18nTags.drawer.do_logout_message_confirm), {\n        okLabel: this.$t(this.$i18nTags.drawer.do_logout_message_yes),\n        cancelLabel: this.$t(this.$i18nTags.drawer.do_logout_message_no),\n      })).result\n      if (doLogout) {\n        localStorage.clear()\n        location.href = '/'\n      }\n    }\n\n    onDeleteHash (hashName: string) {\n      // todo only tag has hash now\n      const newTags = [...this.appStatus.settings.tags]\n      newTags.splice(newTags.indexOf(hashName as any), 1)\n\n      this.updateTags(newTags)\n    }\n\n    /**\n     * @desc if clicked timeline item is just current timeline\n     * */\n    async fetchTimeLineStatuses (timeLineType: string, hashName: string = '') {\n      this.$progress.start()\n      await this.updateTimeLineStatuses({\n        isFetchMore: true,\n        timeLineType, hashName\n      })\n\n      this.$progress.done()\n    }\n\n    onOpenHostInstance () {\n      window.open(this.mastodonServerUri, '_blank');\n    }\n  }\n\n  export default Drawer\n</script>\n\n<style lang=\"less\" scoped>\n  .cuckoo-drawer {\n    .hash-list-item {\n\n      .delete-hash-btn {\n        display: none;\n      }\n\n      &:hover {\n\n        .delete-hash-btn {\n          display: unset;\n        }\n\n      }\n    }\n\n    .bottom-info-area {\n      position: absolute;\n      bottom: 0;\n      margin: 0 0 24px 24px;\n      font-size: 13px;\n\n      .link-text {\n        cursor: pointer;\n        &:hover {\n          text-decoration: underline;\n        }\n      }\n    }\n  }\n\n</style>\n\n<style lang=\"less\">\n  .cuckoo-drawer {\n    // todo current size is not fit to every screen\n    //background: url(\"https://i.imgur.com/vKv5bn5.png\") no-repeat left bottom;\n    //background-size: 42%;\n\n    .mu-item-wrapper {\n      -webkit-transition: background-color .3s cubic-bezier(0,0,0.2,1);\n      -moz-transition: background-color .3s cubic-bezier(0,0,0.2,1);\n      -ms-transition: background-color .3s cubic-bezier(0,0,0.2,1);\n      -o-transition: background-color .3s cubic-bezier(0,0,0.2,1);\n      transition: background-color .3s cubic-bezier(0,0,0.2,1);\n    }\n\n    .toggle-icon {\n      transform: rotate(0);\n      transition: transform .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1)\n    }\n\n    .mu-item__open .toggle-icon {\n      transform: rotate(180deg);\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Header.vue",
    "content": "<template>\n  <div class=\"cuckoo-header-container\">\n    <mu-appbar class=\"header\" :class=\"shouldUseSecondaryThemeHeader && 'dialog-theme-bg-color'\" color=\"primary\" @click.native=\"onHeaderBarClick\">\n      <mu-button v-if=\"isOAuthUser\" icon @click.stop=\"onMenuBtnClick\" slot=\"left\">\n        <mu-icon value=\"menu\"></mu-icon>\n      </mu-button>\n      <div class=\"host-mastodon-url cuckoo-hub-logo\" v-if=\"isCuckooHubTheme\">\n        <span>Cuck</span><span>Hub</span>\n      </div>\n      <span v-if=\"!isCuckooHubTheme\" class=\"host-mastodon-url\" @click=\"onHostMastodonUrlClick\">{{parsedMastodonServerUri}}</span>\n      <mu-button v-if=\"isOAuthUser\" ref=\"notificationBtn\" icon @click.stop=\"onOpenNotificationPanel\" slot=\"right\">\n        <mu-icon v-if=\"appStatus.unreadNotificationCount === 0\" value=\"notifications\"></mu-icon>\n        <mu-badge class=\"notification-badge\" v-if=\"appStatus.unreadNotificationCount > 0\" :content=\"String(appStatus.unreadNotificationCount)\" circle color=\"primary\" />\n      </mu-button>\n\n      <mu-popover v-if=\"isOAuthUser\" v-show=\"showNotificationAsPopOver\"\n                  cover lazy placement=\"left-start\" style=\"width: 420px\"\n                  :open=\"appStatus.isNotificationsPanelOpened && showNotificationAsPopOver\"\n                  @close=\"updateNotificationsPanelStatus(false)\" :trigger=\"notificationBtnTrigger\">\n        <notifications />\n      </mu-popover>\n\n      <mu-dialog v-if=\"isOAuthUser\" v-show=\"!showNotificationAsPopOver\" :overlay=\"false\"\n                 :open=\"appStatus.isNotificationsPanelOpened && !showNotificationAsPopOver\"\n                 :fullscreen=\"true\" transition=\"slide-bottom\">\n        <mu-appbar color=\"primary\" title=\"Notifications\" v-show=\"shouldShowNotificationDialogHeader\">\n          <mu-button slot=\"left\" icon @click=\"updateNotificationsPanelStatus(false)\">\n            <mu-icon value=\"close\" />\n          </mu-button>\n          <mu-button slot=\"right\" icon @click=\"onFetchMoreNotifications\">\n            <mu-icon value=\"refresh\" />\n          </mu-button>\n        </mu-appbar>\n        <notifications :style=\"notificationContainerStyle\" :hideHeader=\"true\" @shouldShowTargetStatusChanged=\"onDialogNotificationShowStatusChanged\"/>\n      </mu-dialog>\n\n      <span class=\"route-info\" v-if=\"shouldShowRouteInfo\">{{pathToRouteInfo[$route.path].name}}</span>\n    </mu-appbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Mutation, Action, Getter } from 'vuex-class'\n  import { TimeLineTypes, RoutersInfo, UiWidthCheckConstants, ThemeNames } from '@/constant'\n  import { cuckoostore } from '@/interface'\n  import { animatedScrollTo } from '@/util'\n  import Notifications from '@/components/Notifications/index'\n\n  // todo 统一位置管理\n  const pathToRouteInfo = {\n    '/timelines/home': {\n      name: 'Home'\n    },\n    '/timelines/public': {\n      name: 'Public'\n    },\n    '/timelines/local': {\n      name: 'Local'\n    }\n  }\n\n  @Component({\n    components: {\n      'notifications': Notifications\n    }\n  })\n  class Header extends Vue {\n\n    $refs: {\n      notificationBtn: any,\n    }\n\n    $router\n\n    $route\n\n    $progress\n\n    notificationBtnTrigger: HTMLButtonElement = null\n\n    @State('appStatus') appStatus\n\n    @State('mastodonServerUri') mastodonServerUri\n\n    @Action('updateNotifications') updateNotifications\n\n    @Getter('isOAuthUser') isOAuthUser\n\n    @Mutation('updateDrawerOpenStatus') updateDrawerOpenStatus\n\n    @Mutation('updateNotificationsPanelStatus') updateNotificationsPanelStatus\n\n    @Mutation('updateUnreadNotificationCount') updateUnreadNotificationCount\n\n    pathToRouteInfo = pathToRouteInfo\n\n    shouldShowNotificationDialogHeader: boolean = true\n\n    @Watch('$route')\n    onRouteChanged () {\n      if (!this.isOAuthUser) return\n\n      this.updateNotificationsPanelStatus(false)\n    }\n\n    get shouldShowRouteInfo () {\n      return this.isOAuthUser && (this.appStatus.documentWidth > 600) && this.pathToRouteInfo[this.$route.path]\n    }\n\n    get parsedMastodonServerUri () {\n      if (!this.isOAuthUser) {\n        return 'Cuckoo.Plus'\n      }\n\n      const url = new URL(this.mastodonServerUri)\n      return url.host.replace(url.host[0], (c) => c.toUpperCase())\n    }\n\n    get showNotificationAsPopOver (): boolean {\n      return this.appStatus.documentWidth > UiWidthCheckConstants.NOTIFICATION_DIALOG_TOGGLE_WIDTH\n    }\n\n    get notificationContainerStyle () {\n      return {\n        height: this.shouldShowNotificationDialogHeader ? 'auto' : '100%'\n      }\n    }\n\n    get shouldUseSecondaryThemeHeader () {\n      return this.isCuckooHubTheme\n    }\n\n    get isCuckooHubTheme () {\n      return this.appStatus.settings.theme === ThemeNames.CUCKOO_HUB\n    }\n\n    mounted () {\n      if (this.isOAuthUser) {\n        this.notificationBtnTrigger = this.$refs.notificationBtn.$el\n      }\n    }\n\n    onMenuBtnClick () {\n      this.updateDrawerOpenStatus(!this.appStatus.isDrawerOpened)\n    }\n\n    onHostMastodonUrlClick () {\n      this.$router.push({ path: '/timelines/home' })\n    }\n\n    onHeaderBarClick () {\n      animatedScrollTo(document.querySelector('html'), 0, 400)\n    }\n\n    onOpenNotificationPanel () {\n      this.onFetchMoreNotifications()\n      this.updateUnreadNotificationCount(0)\n      this.updateNotificationsPanelStatus(!this.appStatus.isNotificationsPanelOpened)\n    }\n\n    onDialogNotificationShowStatusChanged (val) {\n      this.shouldShowNotificationDialogHeader = !val\n    }\n\n    async onFetchMoreNotifications() {\n      this.$progress.start()\n      await this.updateNotifications({\n        isFetchMore: true\n      })\n      this.$progress.done()\n    }\n  }\n\n  export default Header\n</script>\n\n<style lang=\"less\" scoped>\n  .header {\n    padding-left: 8px;\n    position: fixed;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    right: 0;\n    z-index: 20141223;\n\n    .host-mastodon-url {\n      cursor: pointer;\n    }\n\n    .cuckoo-hub-logo {\n\n      span:first-child {\n        padding: 5px 5px;\n        font-weight: 600;\n      }\n\n      span:last-child {\n        padding: 5px 10px;\n        background-color: #FF9900;\n        border-radius: 7px;\n        font-weight: 700;\n      }\n    }\n\n    .route-info {\n      height: 32px;\n      line-height: 32px;\n      padding-left: 10px;\n      margin-left: 20px;\n    }\n\n    .search-input-area {\n      width: 720px;\n      display: flex;\n      margin-left: 28px;\n      align-items: center;\n\n      .pre-fix-icon {\n        margin-left: 10px;\n      }\n\n      .search-input {\n        margin: 0;\n        padding: 0;\n        padding-left: 10px;\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .cuckoo-header-container {\n    .mu-appbar-title {\n      display: flex;\n      align-items: center;\n      padding-left: 0;\n\n      .search-input-area {\n        .mu-text-field-input {\n          height: 48px;\n        }\n\n        .mu-input-line, .mu-input-focus-line {\n          display: none;\n        }\n      }\n    }\n\n    .notification-badge {\n      .mu-badge {\n        border: 2px solid;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Input.vue",
    "content": "<template>\n  <div class=\"cuckoo-input-container\">\n\n    <textarea v-show=\"shouldShowSpoilerTextInputArea\" ref=\"spoilerTextArea\"\n              class=\"auto-size-text-area spoiler-text-area base-theme-bg-color\"\n              v-model=\"spoilerTextValue\" :placeholder=\"$t($i18nTags.common.write_your_warning_here)\"/>\n\n    <textarea ref=\"textArea\" class=\"auto-size-text-area\" v-model=\"textValue\"\n              @keydown.stop=\"onKeyDown\"\n              @keydown.ctrl.enter=\"onQuickSubmit\" @input=\"onInput\"\n              @keydown.38=\"onMinisSelectedResultIndex\" @keydown.40=\"onPlusSelectedResultIndex\"\n              @keydown.enter=\"onSelectedSearchResult\" @click=\"onTextAreaClick\"\n              :placeholder=\"placeholder\"/>\n\n    <div v-if=\"uploadProcesses.length\" class=\"media-area\" :class=\"{ 'single-media-area': uploadProcesses.length === 1 }\">\n      <div class=\"media-item\" :key=\"index\"\n           v-for=\"(processInfo, index) in uploadProcesses\">\n        <div class=\"media-loading-wrapper\" v-loading=\"!processInfo.uploadResult\">\n          <img v-if=\"uploadFileDataUrlList[index]\" :src=\"uploadFileDataUrlList[index]\"/>\n        </div>\n\n        <div class=\"remove-icon-wrapper\" @click=\"onRemoveMediaFileByIndex(index)\">\n          <svg height=\"24px\" width=\"24px\" viewBox=\"0 0 48 48\">\n            <circle fill=\"#fefefe\" cx=\"24\" cy=\"24\" r=\"24\"></circle>\n            <path fill=\"#000\" d=\"M24,4C12.9,4,4,12.9,4,24s8.9,20,20,20s20-9,20-20S35,4,24,4z M34,31.2L31.2,34L24,26.8L16.8,34L14,31.2l7.2-7.2L14,16.8l2.8-2.8l7.2,7.2l7.2-7.2l2.8,2.8L26.8,24L34,31.2z\"></path>\n          </svg>\n        </div>\n      </div>\n    </div>\n\n    <mu-list v-if=\"shouldShowAccountSearchResultList\" v-loading=\"isLoadingSearchResult\"\n             class=\"at-account-search-result-list dialog-theme-bg-color\"\n             :style=\"accountSearchResultListStyle\">\n      <mu-list-item avatar button :ripple=\"false\" :key=\"index\"\n                    @hover=\"currentSelectedResultIndex = index\"\n                    @click.stop=\"onSelectedSearchResult\"\n                    :class=\"{ 'active': currentSelectedResultIndex === index }\"\n                    v-for=\"(account, index) in atAccountSearchResultList\">\n        <mu-list-item-action>\n          <mu-avatar>\n            <img :src=\"account.avatar\">\n          </mu-avatar>\n        </mu-list-item-action>\n        <mu-list-item-title v-html=\"getSearchUserFullName(account)\" />\n      </mu-list-item>\n    </mu-list>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { mastodonentities } from '@/interface'\n  import * as Api from '@/api'\n  import { formatAccountDisplayName, resetImageFileSizeForUpload } from '@/util'\n\n  const autosize = require('autosize')\n  const getCaretCoordinates = require('textarea-caret');\n\n  const maxImageSize = 7.8 * 1024 * 1024\n\n  const searchResultListMaxHeight = 240\n  const listItemHeight = 48\n  const listVerticalPadding = 0\n  const listMargin = 4\n  const atCheckRegex = /\\s@\\S*|^@\\S*/\n\n  @Component({})\n  class Input extends Vue {\n\n    $refs: {\n      textArea: HTMLTextAreaElement\n      spoilerTextArea: HTMLTextAreaElement\n    }\n\n    $toast\n\n    @Prop() text: string\n\n    @Prop() uploadProcesses: Array<{\n      file: File,\n      hasStartedUpload: boolean,\n      uploadResult: mastodonentities.Attachment\n    }>\n\n    @Prop() shouldShowSpoilerTextInputArea: boolean\n\n    @Prop() spoilerText: string\n\n    @Prop() placeholder: string\n\n    @Prop({ default: () => [] }) presetAtAccounts: Array<mastodonentities.Account>\n\n    uploadFileDataUrlList: Array<string> = []\n\n    atAccountSearchResultList: Array<mastodonentities.Account> = []\n\n    currentSelectedResultIndex: number = 0\n\n    currentSearchTextPosition: [number, number] = null\n\n    insertedAcctList: Array<string> = []\n\n    isLoadingSearchResult = false\n\n    get textValue () {\n      return this.text\n    }\n\n    set textValue (val) {\n      this.$emit('update:text', val)\n    }\n\n    get spoilerTextValue () {\n      return this.spoilerText\n    }\n\n    set spoilerTextValue (val) {\n      this.$emit('update:spoilerText', val)\n    }\n\n    get shouldShowAccountSearchResultList () {\n      return this.atAccountSearchResultList.length !== 0\n    }\n\n    accountSearchResultListStyle = null\n\n    @Watch('uploadProcesses')\n    startUploadProcess () {\n      const uploadProcessesCopy = [...this.uploadProcesses]\n\n      uploadProcessesCopy.forEach(async (processInfo, index) => {\n        // update data url list\n        if (!this.uploadFileDataUrlList[index] && !processInfo.hasStartedUpload) {\n          const resolvedFile = await resetImageFileSizeForUpload(processInfo.file)\n          if (resolvedFile !== processInfo.file) {\n            uploadProcessesCopy[index].file = resolvedFile as any\n          }\n\n          const fileReader = new FileReader()\n          fileReader.readAsDataURL(processInfo.file)\n          // @ts-ignore\n          fileReader.onload = () => Vue.set(this.uploadFileDataUrlList, index, fileReader.result)\n        }\n\n        // start upload process\n        if (!processInfo.hasStartedUpload) {\n          uploadProcessesCopy[index].hasStartedUpload = true\n\n          this.$emit('update:uploadProcesses', uploadProcessesCopy)\n\n          const formData = new FormData()\n          formData.append('file', processInfo.file)\n\n          try {\n            const result = await Api.media.postMediaFile(formData)\n\n            uploadProcessesCopy[index].uploadResult = result.data\n\n            this.$emit('update:uploadProcesses', uploadProcessesCopy)\n\n          } catch (e) {\n            this.onRemoveMediaFileByIndex(index)\n            this.$toast.error(e.data.error)\n          }\n        }\n      })\n    }\n\n    public focus () {\n      this.$nextTick(() => {\n        this.$refs.textArea.focus()\n      })\n    }\n\n    public updateSize () {\n      this.$nextTick(() => {\n        this.$refs.textArea.dispatchEvent(new Event('autosize:update'))\n        this.$refs.spoilerTextArea.dispatchEvent(new Event('autosize:update'))\n      })\n    }\n\n    mounted () {\n      this.startUploadProcess()\n      this.insertedAcctList = this.presetAtAccounts.map(accounts => accounts.acct)\n      autosize(this.$refs.textArea)\n      autosize(this.$refs.spoilerTextArea)\n    }\n\n    onKeyDown (e: KeyboardEvent) {\n      if (e.key === 'Escape') {\n        this.$emit('esc')\n      }\n    }\n\n    onQuickSubmit () {\n      this.$emit('submit')\n    }\n\n    onRemoveMediaFileByIndex (index: number) {\n      const uploadProcessesCopy = [...this.uploadProcesses]\n      uploadProcessesCopy.splice(index, 1)\n      // todo update upload processes\n      this.$emit('update:uploadProcesses', uploadProcessesCopy)\n\n      // update uploadFileDataUrlList\n      this.uploadFileDataUrlList.splice(index, 1)\n    }\n\n    onInput () {\n      this.searchAtUsers()\n    }\n\n    onTextAreaClick () {\n      if (this.shouldShowAccountSearchResultList) {\n        this.closeSearchAtUsersList()\n      }\n    }\n\n    onPlusSelectedResultIndex (e: KeyboardEvent) {\n      if (this.shouldShowAccountSearchResultList) {\n        e.preventDefault()\n        if (this.currentSelectedResultIndex === (this.atAccountSearchResultList.length - 1)) {\n          this.currentSelectedResultIndex = 0\n        } else {\n          this.currentSelectedResultIndex = this.currentSelectedResultIndex + 1\n        }\n      }\n    }\n\n    onMinisSelectedResultIndex (e: KeyboardEvent) {\n      if (this.shouldShowAccountSearchResultList) {\n        e.preventDefault()\n        if (this.currentSelectedResultIndex === 0) {\n          this.currentSelectedResultIndex = this.atAccountSearchResultList.length - 1\n        } else {\n          this.currentSelectedResultIndex = this.currentSelectedResultIndex - 1\n        }\n      }\n    }\n\n    onSelectedSearchResult (e: KeyboardEvent) {\n      if (this.shouldShowAccountSearchResultList) {\n        e.preventDefault()\n        const preText = this.textValue.substring(0, this.currentSearchTextPosition[0])\n        const insertText = `@${this.atAccountSearchResultList[this.currentSelectedResultIndex].acct}`\n        const endText = this.textValue.substring(this.currentSearchTextPosition[1])\n\n        this.textValue = `${preText}${insertText}${endText} `\n\n        this.insertedAcctList.push(this.atAccountSearchResultList[this.currentSelectedResultIndex].acct)\n        this.closeSearchAtUsersList()\n\n        this.focus()\n        this.updateSize()\n      }\n    }\n\n    getSearchUserFullName (account: mastodonentities.Account) {\n      return `${formatAccountDisplayName(account)} <span class=\"at-name secondary-read-text-color\">@${account.acct}</span>`\n    }\n\n    getAccountListTopPosition () {\n      const { top, height } = getCaretCoordinates(this.$refs.textArea, this.$refs.textArea.selectionEnd)\n\n      const { height: offsetHeight, top: offsetTop } = this.$refs.textArea.getBoundingClientRect()\n\n      let topPosition = top + height + listMargin\n\n      if (innerHeight - offsetTop - offsetHeight - top < searchResultListMaxHeight) {\n        const listHeight = Math.min(listItemHeight * this.atAccountSearchResultList.length + listVerticalPadding * 2, searchResultListMaxHeight)\n        topPosition = -listHeight - listMargin\n      }\n\n      return `${topPosition}px`\n    }\n\n    getSearchAtUsersKeyWords (): string {\n\n      let selectionEnd = this.$refs.textArea.selectionEnd\n\n      if (this.textValue[selectionEnd - 1] === ' ') return\n\n      const len = this.textValue.length\n      for (; selectionEnd < len; selectionEnd ++) {\n        if (this.textValue[selectionEnd] === ' ') {\n          break\n        }\n      }\n\n      const textBeforeSelection = this.textValue.slice(0, selectionEnd).split(' ').pop().split('\\n').pop()\n\n      if (textBeforeSelection.match(atCheckRegex)) {\n        this.currentSearchTextPosition = [selectionEnd - textBeforeSelection.length, selectionEnd]\n\n        return textBeforeSelection.slice(1)\n      }\n    }\n\n    searchAtUsers () {\n      this.$nextTick(async () => {\n        const searchUsersKeyWords = this.getSearchAtUsersKeyWords()\n\n        if (searchUsersKeyWords === undefined || this.insertedAcctList.indexOf(searchUsersKeyWords) !== -1) {\n          this.isLoadingSearchResult = false\n          return this.closeSearchAtUsersList()\n        }\n\n        if (searchUsersKeyWords === '') {\n          Api.search.abortSearch()\n          this.isLoadingSearchResult = false\n          this.atAccountSearchResultList = [...this.presetAtAccounts]\n        } else {\n          // search for accounts\n          try {\n            this.isLoadingSearchResult = true\n            const result = await Api.search.getSearchResults(searchUsersKeyWords)\n            this.isLoadingSearchResult = false\n            this.atAccountSearchResultList = [...result.data.accounts]\n          } catch (e) {\n\n          }\n        }\n\n        if (this.atAccountSearchResultList.length) {\n          this.accountSearchResultListStyle = { top: this.getAccountListTopPosition() }\n        }\n\n      })\n    }\n\n    closeSearchAtUsersList () {\n      this.atAccountSearchResultList = []\n      this.currentSelectedResultIndex = 0\n      this.currentSearchTextPosition = [0, 0]\n    }\n\n    beforeDestroy () {\n      this.$refs.textArea.dispatchEvent(new Event('autosize:destroy'))\n      this.$refs.spoilerTextArea.dispatchEvent(new Event('autosize:destroy'))\n    }\n  }\n\n  export default Input\n</script>\n\n<style lang=\"less\" scoped>\n  .cuckoo-input-container {\n    width: 100%;\n    position: relative;\n\n    .spoiler-text-area {\n      height: 38px;\n      padding: 10px;\n      border-radius: 4px;\n    }\n\n    .media-area {\n      padding-left: 16px;\n\n      .media-loading-wrapper {\n        height: 100%;\n        position: relative;\n\n        img {\n          width: auto;\n        }\n      }\n\n      .remove-icon-wrapper {\n        cursor: pointer;\n        position: absolute;\n        right: 12px;\n        top: 12px;\n        z-index: 20141223;\n      }\n    }\n\n    .at-account-search-result-list {\n      width: 100%;\n      max-height: 240px;\n      position: absolute;\n      box-shadow: 0 2px 5px 0 rgba(0,0,0,0.26);\n      z-index: 1;\n      padding: 0;\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .at-account-search-result-list {\n\n\n    .active > .mu-item-wrapper {\n      background-color: rgba(0, 0, 0, .1) !important;\n    }\n\n    .mu-item-wrapper {\n      &.hover {\n        background-color: unset;\n      }\n\n      .mu-item.has-avatar {\n        height: 48px;\n        padding: 0 10px;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Notifications/Card.vue",
    "content": "<template>\n  <mu-list-item :style=\"notificationCardStyle\" v-loading=\"isLoading\"\n                @click=\"onNotificationCardClick(notification)\"\n                class=\"notification-card dialog-theme-bg-color\" avatar button :ripple=\"false\">\n    <mu-list-item-action class=\"user-avatar-area\">\n      <mu-avatar class=\"user-avatar\" @click.stop=\"onCheckUserAccountPage(notification.account)\">\n        <img :src=\"notification.account.avatar\" />\n      </mu-avatar>\n    </mu-list-item-action>\n    <mu-list-item-content>\n      <mu-list-item-title class=\"user-display-name primary-read-text-color\"\n                          v-html=\"getAccountDisplayName(notification.account)\"\n                          @click.stop=\"onCheckUserAccountPage(notification.account)\" />\n      <mu-list-item-sub-title class=\"notification-content primary-read-text-color\"\n                              v-html=\"getNotificationSubTitle(notification)\" @click.prevent=\"onNotificationContentClick\"/>\n    </mu-list-item-content>\n    <mu-list-item-action v-if=\"shouldShowFollowOperateBtn(notification, followOperateBtnTypes.FOLLOW)\">\n      <mu-icon @click.stop=\"onFollowingAccount(notification.account.id)\" class=\"follow-action\" value=\"person_add\" />\n    </mu-list-item-action>\n    <mu-list-item-action v-if=\"shouldShowFollowOperateBtn(notification, followOperateBtnTypes.UN_FOLLOW)\">\n      <mu-icon @click.stop=\"onUnFollowingAccount(notification.account.id)\" class=\"follow-action secondary-theme-text-color\" value=\"person_add_disabled\" />\n    </mu-list-item-action>\n  </mu-list-item>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { State, Getter, Action } from 'vuex-class'\n  import { NotificationTypes, ThemeNames, I18nTags } from '@/constant'\n  import { mastodonentities } from '@/interface'\n  import { prepareRootStatus, formatHtml } from \"@/util\"\n\n  const notificationTypeToI18nTagsMap = {\n    [NotificationTypes.FAVOURITE]: I18nTags.notifications.favourited_your_status,\n    [NotificationTypes.REBLOG]: I18nTags.notifications.boosted_your_status\n  }\n\n  @Component({})\n  class Card extends Vue {\n\n    $t\n\n    $i18nTags\n\n    isLoadingSingleCard: boolean = false\n\n    isLoading: boolean = false\n\n    followOperateBtnTypes = {\n      FOLLOW: 'FOLLOW',\n      UN_FOLLOW: 'UN_FOLLOW'\n    }\n\n    @Prop() notification: mastodonentities.Notification\n\n    @Action('followAccountById') followAccountById\n    @Action('unFollowAccountById') unFollowAccountById\n\n    @State('appStatus') appStatus\n\n    @State('relationships') relationships: {\n      [id: string]: mastodonentities.Relationship\n    }\n\n    @Getter('getAccountDisplayName') getAccountDisplayName\n\n    onNotificationContentClick () {}\n\n    get notificationCardStyle () {\n      const themeToStyle: any = {\n        [ThemeNames.GOOGLE_PLUS]: {\n          backgroundColor: '#fff'\n        },\n        [ThemeNames.GREEN_LIGHT]: {\n          backgroundColor: '#fff'\n        }\n      }\n\n      themeToStyle[this.appStatus.settings.theme] = themeToStyle[this.appStatus.settings.theme] || {}\n\n      themeToStyle[this.appStatus.settings.theme].position = this.isLoadingSingleCard ? 'relative' : ''\n\n      return themeToStyle[this.appStatus.settings.theme]\n    }\n\n    onCheckUserAccountPage (account: mastodonentities.Account) {\n      window.open(account.url, \"_blank\")\n    }\n\n    getNotificationSubTitle (notification) {\n      switch (notification.type) {\n        case NotificationTypes.FOLLOW: {\n          return `${this.getAccountDisplayName(notification.account)} ${this.$t(this.$i18nTags.notifications.someone_followed_you)}`\n        }\n\n        case NotificationTypes.FAVOURITE:\n        case NotificationTypes.REBLOG: {\n          return this.$t(notificationTypeToI18nTagsMap[notification.type]) + \": \" + formatHtml(notification.status.content)\n        }\n\n        case NotificationTypes.MENTION: {\n          return formatHtml(notification.status.content)\n        }\n      }\n    }\n\n\n    async onNotificationCardClick (notification: mastodonentities.Notification) {\n      if (!notification.status) {\n        if (notification.type === NotificationTypes.FOLLOW) {\n          window.open(notification.account.url, \"_blank\")\n        }\n      } else {\n        this.isLoadingSingleCard = false\n\n        this.isLoading = true\n        const targetStatus = await prepareRootStatus(notification.status)\n        this.isLoading = false\n\n        this.$emit('updateCurrentCheckStatus', targetStatus)\n      }\n    }\n\n    shouldShowFollowOperateBtn (notification: mastodonentities.Notification, operateType: string) {\n      const isFollowingNotification = notification.type === NotificationTypes.FOLLOW\n      let typeCheckOK = false\n      if (operateType === this.followOperateBtnTypes.FOLLOW) {\n        typeCheckOK = this.relationships[notification.account.id] && !this.relationships[notification.account.id].following\n      } else if (operateType === this.followOperateBtnTypes.UN_FOLLOW) {\n        typeCheckOK = this.relationships[notification.account.id] && this.relationships[notification.account.id].following\n      }\n      return isFollowingNotification && typeCheckOK\n    }\n\n    async onFollowingAccount (id: string) {\n      this.isLoadingSingleCard = true\n\n      this.isLoading = true\n      await this.followAccountById(id)\n      this.isLoading = false\n    }\n\n    async onUnFollowingAccount (id: string) {\n      this.isLoadingSingleCard = true\n\n      this.isLoading = true\n      await this.unFollowAccountById(id)\n      this.isLoading = false\n    }\n  }\n\n  export default Card\n</script>\n\n<style lang=\"less\" scoped>\n  .notification-card {\n    margin: 2px 0;\n    box-shadow: 0 1px 2px rgba(0,0,0,.2);\n    border-radius: 3px;\n    cursor: pointer;\n\n    .user-avatar {\n      cursor: pointer;\n    }\n\n    .user-display-name {\n      display: inline;\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n\n    .notification-content {\n      cursor: pointer;\n    }\n\n    .follow-action {\n      cursor: pointer;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/Notifications/index.vue",
    "content": "<template>\n  <div class=\"notification-panel-container base-theme-bg-color\" :style=\"isLoadingTargetStatus ? { overflow: 'hidden' } : null\">\n\n    <keep-alive>\n      <div class=\"notification-list\" v-show=\"!shouldShowTargetStatus\">\n        <mu-load-more loading-text=\"\" @load=\"loadNotifications(true)\" :loading=\"isLoadingNotifications\">\n          <mu-flex v-if=\"!hideHeader\" class=\"panel-header\" calign-items=\"center\">\n            <mu-flex justify-content=\"start\" fill>\n              <mu-sub-header class=\"secondary-read-text-color\">Notifications</mu-sub-header>\n            </mu-flex>\n            <mu-flex justify-content=\"end\" fill>\n              <mu-button icon @click=\"loadNotifications(false, true)\">\n                <mu-icon class=\"primary-read-text-color\" value=\"refresh\" />\n              </mu-button>\n            </mu-flex>\n          </mu-flex>\n\n          <mu-list textline=\"three-line\">\n            <notification-card :notification=\"notification\" @updateCurrentCheckStatus=\"onUpdateCurrentCheckStatus\"\n                               v-for=\"(notification, index) in notificationsToShow\" :key=\"index\"/>\n          </mu-list>\n\n        </mu-load-more>\n      </div>\n    </keep-alive>\n\n    <div v-if=\"shouldShowTargetStatus\" class=\"notification-status-check-area\">\n      <mu-appbar color=\"secondary\">\n        <mu-button slot=\"left\" icon @click.stop=\"shouldShowTargetStatus = false\">\n          <mu-icon value=\"arrow_back\" />\n        </mu-button>\n      </mu-appbar>\n      <div class=\"notification-status-card-container\">\n        <status-card class=\"status-card-container no-limit-reply-area-height\" v-if=\"currentCheckStatus\" :status=\"currentCheckStatus\"/>\n      </div>\n    </div>\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { State, Action, Mutation } from 'vuex-class'\n  import { NotificationTypes, UiWidthCheckConstants } from '@/constant'\n  import StatusCard from '@/components/StatusCard'\n  import NotificationCard from './Card'\n  import { mastodonentities } from '@/interface'\n  import { prepareRootStatus, formatHtml } from \"@/util\"\n\n  @Component({\n    components: {\n      'status-card': StatusCard,\n      'notification-card': NotificationCard\n    }\n  })\n  class Notifications extends Vue {\n\n    $progress\n\n    $router\n\n    $routersInfo\n\n    @Prop() hideHeader: boolean\n\n    @Action('updateNotifications') updateNotifications\n\n    @Mutation('updateNotificationsPanelStatus') updateNotificationsPanelStatus\n\n    @State('notifications') notifications: Array<mastodonentities.Notification>\n    @State('contextMap') contextMap: {\n      [statusId: string]: {\n        ancestors: Array<string>\n        descendants: Array<string>\n      }\n    }\n    @State('appStatus') appStatus\n\n    isLoadingNotifications: boolean = false\n\n    // todo\n    isLoadingTargetStatus: boolean = false\n\n    shouldShowTargetStatus: boolean = false\n\n    currentCheckStatus: mastodonentities.Status = null\n\n    @Watch('isLoadingNotifications')\n    onLoadingNotificationStatusChanged (toValue) {\n      toValue ? this.$progress.start() : this.$progress.done()\n    }\n\n    @Watch('shouldShowTargetStatus')\n    onShouldShowTargetStatusChanged (val) {\n      this.$emit('shouldShowTargetStatusChanged', val)\n    }\n\n    get notificationsToShow () {\n      const allDescendantsToMute = []\n\n      this.appStatus.settings.muteMap.statusList.forEach(statusId => {\n        if (this.contextMap[statusId]) {\n          allDescendantsToMute.push(...this.contextMap[statusId].descendants, statusId)\n        }\n      })\n\n      return this.notifications.filter(notification => {\n        let toMuteByStatus, toMuteByUser\n\n        if (notification.status) toMuteByStatus = allDescendantsToMute.indexOf(notification.status.id) !== -1\n        if (notification.account) toMuteByUser = this.appStatus.settings.muteMap.userList.indexOf(notification.account.id) !== -1\n\n        return !toMuteByStatus && !toMuteByUser\n      })\n    }\n\n    async loadNotifications (isLoadMore, isFetchMore) {\n      if (this.shouldShowTargetStatus) return\n\n      this.isLoadingNotifications = true\n      await this.updateNotifications({\n        isLoadMore,\n        isFetchMore\n      })\n      this.isLoadingNotifications = false\n    }\n\n    onUpdateCurrentCheckStatus (targetStatus: mastodonentities.Status) {\n\n      if (this.appStatus.documentWidth < UiWidthCheckConstants.NOTIFICATION_DIALOG_TOGGLE_WIDTH) {\n        this.updateNotificationsPanelStatus(false)\n        return this.$router.push({\n          name: this.$routersInfo.statuses.name,\n          params: { statusId: targetStatus.id }\n        })\n      }\n\n      this.currentCheckStatus = targetStatus\n      this.shouldShowTargetStatus = true\n    }\n  }\n\n  export default Notifications\n</script>\n\n<style lang=\"less\" scoped>\n  .notification-panel-container {\n    width: 100%;\n    height: calc(100vh - 56px) !important;\n    max-height: 1200px;\n    position: relative;\n\n    .notification-list {\n      padding: 8px;\n      height: 100%;\n      overflow: auto;\n      overflow-x: hidden;\n      -webkit-overflow-scrolling: touch;\n    }\n\n    .notification-status-check-area {\n      height: 100%;\n\n      .notification-status-card-container {\n        padding-top: 8px;\n\n        .status-card-container {\n          height: 100%;\n          margin-bottom: 40px;\n        }\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .notification-panel-container {\n    .notification-list {\n\n      .mu-item-wrapper.hover {\n        background-color: inherit !important;\n        cursor: pointer;\n      }\n\n      .notification-content {\n        > p { display: inline }\n      }\n\n      .mu-item-sub-title {\n        p { margin: 0 }\n      }\n\n      .mu-avatar {\n        margin: 0;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/PostStatusDialog.vue",
    "content": "<template>\n  <mu-dialog dialog-class=\"post-status-dialog-container\"\n             :open.sync=\"isDialogOpening\" overlay-color=\"rgba(0,0,0,0.12)\"\n             :overlay-opacity=\"1\" @close=\"onTryCloseDialog\" :transition=\"transition\"\n             :width=\"dialogWidth\" :fullscreen=\"shouldDialogFullScreen\" v-loading=\"isPostLoading\">\n\n    <mu-appbar v-if=\"shouldDialogFullScreen\" class=\"dialog-fullscreen-bar\" color=\"primary\">\n      <mu-button slot=\"left\" icon @click=\"onTryCloseDialog\">\n        <mu-icon value=\"close\"></mu-icon>\n      </mu-button>\n      <mu-button slot=\"right\" flat :disabled=\"!shouldEnableSubmitButton\"\n                 @click=\"onSubmitNewStatus\">\n        {{$t($i18nTags.statusCard.submit_post)}}\n      </mu-button>\n    </mu-appbar>\n\n    <div class=\"dialog-header\">\n      <div class=\"left-area\">\n        <mu-avatar class=\"current-user-avatar\" slot=\"avatar\" size=\"40\">\n          <img :src=\"currentUserAccount.avatar\">\n        </mu-avatar>\n        <div class=\"user-and-status-info\">\n          <a class=\"user-name primary-read-text-color\" v-html=\"currentUserAccount.display_name\"></a>\n          <div class=\"visibility-row\">\n            <div class=\"arrow-container\">\n              <svg viewBox=\"0 0 48 48\" height=\"100%\" width=\"100%\">\n                <path class=\"header-svg-fill\" d=\"M20 14l10 10-10 10z\" />\n              </svg>\n            </div>\n            <div class=\"visibility-info secondary-theme-text-color\" ref=\"visibilitySelectBtn\"\n                 @click=\"setVisibilitySelectPopOverDisplay(true)\">\n              {{$t(visibility)}}\n              <mu-icon size=\"18\" class=\"visibility-icon secondary-read-text-color\" :value=\"getVisibilityDescInfo(visibility).icon\"></mu-icon>\n            </div>\n            <visibility-select-pop-over :visibility.sync=\"visibility\"\n                                        :open.sync=\"shouldOpenVisibilitySelectPopOver\"\n                                        :trigger=\"visibilityTriggerBtn\"/>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"right-area\">\n        <div class=\"card-header-action\">\n          <mu-icon class=\"header-icon\" value=\"more_vert\" />\n        </div>\n      </div>\n    </div>\n\n    <section>\n\n      <cuckoo-input ref=\"cuckooInput\" @submit=\"onSubmitNewStatus\"\n                    @esc=\"onTryCloseDialog\"\n                    :shouldShowSpoilerTextInputArea=\"shouldShowSpoilerTextInputArea\"\n                    :spoilerText.sync=\"spoilerTextValue\"\n                    :text.sync=\"textContentValue\" :uploadProcesses.sync=\"uploadProcesses\"\n                    :placeholder=\"$t($i18nTags.statusCard.post_new_status_placeholder)\"/>\n\n      <div class=\"bottom-area\">\n\n        <div class=\"attachment-select-btn-group\">\n          <mu-button icon @click=\"onSelectMediaFiles\" :disabled=\"uploadProcesses.length === 4\">\n            <mu-icon class=\"secondary-read-text-color\" value=\"camera_alt\" />\n            <input ref=\"fileInput\" type=\"file\" @change=\"onUploadMediaFiles\"\n                   accept=\".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg,image/png,image/gif,video/webm,video/mp4,video/quicktime\"\n                   style=\"display: none\" multiple/>\n          </mu-button>\n          <!--<mu-button icon>-->\n            <!--<mu-icon class=\"secondary-read-text-color\" value=\"link\" />-->\n          <!--</mu-button>-->\n          <mu-button v-if=\"uploadProcesses.length\" @click=\"markMediaAsSensitive = !markMediaAsSensitive\"\n                     class=\"secondary-read-text-color\" icon>\n            <mu-icon :value=\"markMediaAsSensitive ? 'visibility_off' : 'visibility'\" />\n          </mu-button>\n\n          <mu-button @click=\"shouldShowSpoilerTextInputArea = !shouldShowSpoilerTextInputArea\"\n                     class=\"operate-btn\" icon\n                     :class=\"shouldShowSpoilerTextInputArea ? 'secondary-theme-text-color' : 'secondary-read-text-color'\">\n            <mu-icon class=\"reply-action-icon\" value=\"add_alert\" />\n          </mu-button>\n        </div>\n\n        <div class=\"content-length-indicator secondary-read-text-color\">\n          {{textContentValue.length}}/500\n        </div>\n      </div>\n    </section>\n\n    <footer v-if=\"!shouldDialogFullScreen\">\n      <mu-button class=\"dialog-button secondary-theme-text-color\" flat :disabled=\"!shouldEnableSubmitButton\"\n                 @click=\"onSubmitNewStatus\">\n        {{$t($i18nTags.statusCard.submit_post)}}\n      </mu-button>\n      <mu-button class=\"dialog-button\" color=\"secondary\" flat @click=\"onTryCloseDialog\">\n        {{$t($i18nTags.statusCard.cancel_post)}}\n      </mu-button>\n    </footer>\n\n  </mu-dialog>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { State, Getter, Action } from 'vuex-class'\n  import { UiWidthCheckConstants, VisibilityTypes } from '@/constant'\n  import { getVisibilityDescInfo } from '@/util'\n  import VisibilitySelectPopOver from '@/components/VisibilitySelectPopOver'\n  import Input from '@/components/Input'\n  import { mastodonentities } from \"../interface\";\n\n  const maxUploadLength = 4\n\n  class InjectDragAndDropEvents {\n\n    private dialogComponent\n\n    private beingDragOver: boolean = false\n\n    constructor (dialogComponent: PostStatusDialog) {\n      this.dialogComponent = dialogComponent\n\n      this.dialogComponent.$el.addEventListener('dragenter', this.onDragOver.bind(this))\n    }\n\n    private insertDragOverLayer () {\n      const layer = document.createElement('div')\n      layer.className = 'mu-loading-wrap drag-over-layer'\n\n      layer.innerText = this.dialogComponent.$t(this.dialogComponent.$i18nTags.common.drag_and_drop_to_upload)\n\n      this.dialogComponent.$el.appendChild(layer)\n\n      layer.addEventListener('dragover', e => e.preventDefault())\n      layer.addEventListener('dragleave', this.onDragLeave.bind(this))\n      layer.addEventListener('drop', this.onDrop.bind(this))\n    }\n\n    private removeDragOverLayer () {\n      this.dialogComponent.$el.removeChild(this.dialogComponent.$el.querySelector('.drag-over-layer'))\n    }\n\n    private onDragOver (e: DragEvent) {\n      e.preventDefault()\n\n      if (!this.beingDragOver) {\n        this.beingDragOver = true\n        this.insertDragOverLayer()\n      }\n    }\n\n    private onDragLeave (e: DragEvent) {\n      e.preventDefault()\n\n      if (this.beingDragOver) {\n        this.beingDragOver = false\n        this.removeDragOverLayer()\n      }\n    }\n\n    private onDrop (e: DragEvent) {\n      e.preventDefault()\n\n      this.beingDragOver = false\n      this.removeDragOverLayer()\n\n      const filesToUpload = Array.from(e.dataTransfer.files)\n        .splice(0, maxUploadLength - this.dialogComponent.uploadProcesses.length)\n\n      if (filesToUpload.length === 0) return\n\n      filesToUpload.forEach(file => {\n        this.dialogComponent.uploadProcesses.push({\n          file, hasStartedUpload: false, uploadResult: null\n        })\n      })\n    }\n  }\n\n  @Component({\n    components: {\n      'cuckoo-input': Input,\n      'visibility-select-pop-over': VisibilitySelectPopOver\n    }\n  })\n  class PostStatusDialog extends Vue {\n\n    $refs: {\n      cuckooInput: Input\n      textArea: HTMLTextAreaElement\n      fileInput: HTMLInputElement\n      visibilitySelectBtn: HTMLDivElement\n    }\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    $toast\n\n    getVisibilityDescInfo = getVisibilityDescInfo\n\n    isConfirmDialogShowing: boolean = false\n\n    postPrivacy = null\n\n    get visibility () {\n      return this.postPrivacy || this.appStatus.settings.postPrivacy\n    }\n\n    set visibility (val) {\n      this.postPrivacy = val\n    }\n\n    postMediaAsSensitiveMode: boolean = null\n\n    get markMediaAsSensitive () {\n      return typeof this.postMediaAsSensitiveMode === 'boolean' ? this.postMediaAsSensitiveMode :\n        this.appStatus.settings.postMediaAsSensitiveMode\n    }\n\n    set markMediaAsSensitive (val) {\n      this.postMediaAsSensitiveMode = val\n    }\n\n    visibilityTriggerBtn: HTMLDivElement = null\n\n    shouldOpenVisibilitySelectPopOver = false\n\n    isPostLoading: boolean = false\n\n    uploadProcesses: Array<{\n      file: File,\n      hasStartedUpload: boolean,\n      uploadResult: mastodonentities.Attachment\n    }> = []\n\n    textContentValue: string = ''\n\n    shouldShowSpoilerTextInputArea: boolean = null\n\n    spoilerTextValue: string = ''\n\n    @Prop() open: boolean\n\n    @Prop() close: Function\n\n    @State('appStatus') appStatus\n\n    @State('currentUserAccount') currentUserAccount\n\n    @Action('postStatus') postStatus\n\n    @Getter('shouldDialogFullScreen') shouldDialogFullScreen\n\n    get isDialogOpening () {\n      return this.open\n    }\n\n    set isDialogOpening (val) {}\n\n    get shouldEnableSubmitButton () {\n      const isInUploadProcess = this.uploadProcesses.every(i => !i.uploadResult)\n\n      return this.uploadProcesses.length ? !isInUploadProcess : this.textContentValue\n    }\n\n    @Watch('isDialogOpening')\n    onDialogOpenChanged (isOpening) {\n      if (isOpening) {\n        this.$nextTick(() => {\n          this.visibilityTriggerBtn = this.$refs.visibilitySelectBtn\n          this.$refs.cuckooInput.focus()\n\n          new InjectDragAndDropEvents(this)\n        })\n      } else {\n        this.setVisibilitySelectPopOverDisplay(false)\n      }\n    }\n\n    get dialogWidth () {\n      return this.shouldDialogFullScreen ? null : UiWidthCheckConstants.POST_STATUS_DIALOG_TOGGLE_WIDTH\n    }\n\n    get transition () {\n      return this.shouldDialogFullScreen ? 'slide-bottom' : 'slide-top'\n    }\n\n    async onTryCloseDialog () {\n      if (this.isConfirmDialogShowing) return\n\n      if (this.textContentValue || this.spoilerTextValue || this.uploadProcesses.length) {\n        this.isConfirmDialogShowing = true\n        const doCloseDialog = (await this.$confirm(this.$t(this.$i18nTags.postStatusDialog.do_discard_message_confirm), {\n          okLabel: this.$t(this.$i18nTags.postStatusDialog.do_discard_message),\n          cancelLabel: this.$t(this.$i18nTags.postStatusDialog.do_keep_message),\n        })).result\n        if (doCloseDialog) {\n          this.closeDialog()\n        } else {\n          this.$refs.cuckooInput.focus()\n        }\n\n        this.isConfirmDialogShowing = false\n      } else {\n        this.closeDialog()\n      }\n    }\n\n    async onSubmitNewStatus () {\n      if (!this.shouldEnableSubmitButton) return\n\n      if (this.textContentValue.length > 500) {\n        return this.$toast.error(this.$t(this.$i18nTags.postStatusDialog.text_character_limit_exceed))\n      }\n\n      const formData = {\n        status: this.textContentValue,\n        visibility: this.visibility,\n        spoilerText: this.shouldShowSpoilerTextInputArea ? this.spoilerTextValue : '',\n        sensitive: this.postMediaAsSensitiveMode,\n        mediaIds: this.uploadProcesses.map(info => info.uploadResult.id)\n      }\n\n      this.isPostLoading = true\n\n      await this.postStatus({ formData })\n\n      this.isPostLoading = false\n\n      // clear data and close dialog\n      this.closeDialog()\n    }\n\n    onSelectMediaFiles () {\n      this.$refs.fileInput.click()\n    }\n\n    onUploadMediaFiles () {\n      Array.from(this.$refs.fileInput.files)\n        .splice(0, maxUploadLength - this.uploadProcesses.length)\n        .forEach(file => {\n          this.uploadProcesses.push({\n            file, hasStartedUpload: false, uploadResult: null\n          })\n        })\n\n      this.$refs.fileInput.value = ''\n    }\n\n    setVisibilitySelectPopOverDisplay (open: boolean) {\n      this.shouldOpenVisibilitySelectPopOver = open\n    }\n\n    closeDialog () {\n      this.textContentValue = ''\n      this.$refs.fileInput.value = ''\n      this.spoilerTextValue = ''\n      this.shouldShowSpoilerTextInputArea = false\n      this.uploadProcesses = []\n      this.$emit('update:open', false)\n    }\n  }\n\n  export default PostStatusDialog\n</script>\n\n<style lang=\"less\" scoped>\n  .post-status-dialog-container {\n\n    .dialog-header {\n      line-height: 1;\n      display: flex;\n      justify-content: space-between;\n      padding: 16px 4px 8px 16px;\n\n      .left-area {\n        display: flex;\n        align-items: center;\n\n        .current-user-avatar {\n          margin-right: 8px;\n          cursor: pointer;\n        }\n\n        .user-and-status-info {\n          display: flex;\n          align-items: center;\n\n          .user-name {\n            cursor: pointer;\n            font-size: 15px;\n          }\n\n          .visibility-row {\n            display: flex;\n            align-items: center;\n\n            .arrow-container {\n              width: 18px;\n              height: 18px;\n            }\n\n            .visibility-info {\n              cursor: pointer;\n              display: flex;\n              align-items: center;\n\n              .visibility-icon {\n                margin-left: 4px;\n              }\n            }\n          }\n\n        }\n\n      }\n\n      .right-area {\n        display: flex;\n        align-items: center;\n        width: 48px;\n        height: 48px;\n\n        .card-header-action {\n          .header-icon {\n            cursor: pointer;\n            font-size: 18px;\n            margin-left: 10px;\n          }\n        }\n      }\n    }\n\n    section {\n      @media (max-width: 530px) {\n        height: calc(100% - 56px - 72px);\n        display: flex;\n        flex-direction: column;\n        justify-content: space-between;\n\n        .auto-size-text-area {\n          max-height: unset !important;\n          flex-grow: 1;\n        }\n      }\n\n      .auto-size-text-area {\n        height: 187px;\n        padding: 0 16px;\n        max-height: 373px;\n      }\n\n      .bottom-area {\n        display: flex;\n        justify-content: space-between;\n\n        .content-length-indicator {\n          line-height: 48px;\n          font-size: 16px;\n          margin-right: 20px;\n        }\n      }\n\n    }\n\n    footer {\n      display: flex;\n      align-items: center;\n      flex-direction: row-reverse;\n      height: 57px;\n\n      .dialog-button {\n        margin-right: 16px;\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .post-status-dialog-container {\n    border-radius: 4px;\n\n    .mu-dialog-body {\n      padding: 0;\n    }\n\n    section {\n\n      .cuckoo-input-container {\n        margin: 0 16px;\n        width: auto;\n      }\n\n      @media (max-width: 530px) {\n        .auto-size-text-area {\n          max-height: unset !important;\n          flex-grow: 1;\n        }\n      }\n\n      .auto-size-text-area {\n        height: 187px;\n        max-height: 373px;\n      }\n    }\n  }\n\n  .mu-item-wrapper {\n    &:hover {\n\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/CardHeader.vue",
    "content": "<template>\n  <mu-card-header class=\"mu-card-header\" ref=\"cardHeader\"\n                  @mouseover=\"shouldShowHeaderActionButtonGroup = true\"\n                  @mouseout=\"shouldShowHeaderActionButtonGroup = false\">\n    <div class=\"left-area\" :style=\"leftAreaStyle\">\n      <mu-avatar @click=\"onCheckUserAccountPage\" class=\"status-account-avatar\" slot=\"avatar\" size=\"34\">\n        <img :src=\"status.account.avatar\">\n      </mu-avatar>\n      <div class=\"user-and-status-info\">\n        <a @click=\"onCheckUserAccountPage\" class=\"user-name primary-read-text-color\">\n          <span class=\"display-name\" v-html=\"getAccountDisplayName(status.account)\"></span>\n          <span class=\"at-name secondary-read-text-color\">@{{getAccountAtName(status.account)}}</span>\n        </a>\n        <div ref=\"visibilityInfo\" class=\"visibility-row secondary-read-text-color\">\n          <div class=\"arrow-container\">\n            <svg viewBox=\"0 0 48 48\" height=\"100%\" width=\"100%\">\n              <path class=\"header-svg-fill\" d=\"M20 14l10 10-10 10z\" />\n            </svg>\n          </div>\n          <div class=\"visibility-info secondary-read-text-color\">{{$t(status.visibility)}}</div>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"right-area\" ref=\"rightArea\" v-if=\"isOAuthUser\">\n      <span v-show=\"!shouldOpenMoreOperationPopOver && !shouldShowHeaderActionButtonGroup\" class=\"status-from-now secondary-read-text-color\">{{getFromNowTime(status.created_at)}}</span>\n\n      <div v-show=\"shouldOpenMoreOperationPopOver || shouldShowHeaderActionButtonGroup\" class=\"card-header-action\">\n        <mu-icon class=\"header-icon secondary-read-text-color\" value=\"open_in_new\" @click=\"onCheckStatusInSinglePage\"/>\n        <mu-icon class=\"header-icon secondary-read-text-color\" value=\"more_vert\" ref=\"moreOperationTriggerBtn\"\n                 @click=\"shouldOpenMoreOperationPopOver = true\"/>\n      </div>\n    </div>\n\n    <mu-popover v-if=\"isOAuthUser\" cover placement=\"left-start\"\n                :open.sync=\"shouldOpenMoreOperationPopOver\"\n                :trigger=\"moreOperationTriggerBtn\">\n      <mu-list>\n        <mu-list-item button @click.stop=\"onMuteStatusByOperateList\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.mute_status)}}</mu-list-item-title>\n        </mu-list-item>\n        <mu-list-item button v-if=\"currentUserAccount.id !== status.account.id\"\n                      @click.stop=\"onMuteUserByOperateList\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.mute_user)}}</mu-list-item-title>\n        </mu-list-item>\n        <mu-list-item button v-if=\"currentUserAccount.id === status.account.id\"\n                      @click.stop=\"onDeleteStatusByOperateList\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.delete_status)}}</mu-list-item-title>\n        </mu-list-item>\n      </mu-list>\n    </mu-popover>\n\n  </mu-card-header>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { Getter, State, Action } from 'vuex-class'\n  import * as moment from 'moment'\n  import { mastodonentities } from '@/interface'\n\n  @Component({})\n  class CardHeader extends Vue {\n\n    @Prop() status: mastodonentities.Status\n\n    $router\n\n    $routersInfo\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    $refs: {\n      cardHeader: HTMLDivElement\n      visibilityInfo: any\n      rightArea: HTMLDivElement\n      moreOperationTriggerBtn: any\n    }\n\n    @Getter('getAccountDisplayName') getAccountDisplayName\n    @Getter('getAccountAtName') getAccountAtName\n    @Getter('isOAuthUser') isOAuthUser\n\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n\n    @Action('deleteStatus') deleteStatus\n\n    shouldShowHeaderActionButtonGroup = false\n\n    shouldOpenMoreOperationPopOver = false\n\n    moreOperationTriggerBtn: any = null\n\n    leftAreaStyle = null\n\n    mounted () {\n      if (this.isOAuthUser) {\n        this.moreOperationTriggerBtn = this.$refs.moreOperationTriggerBtn\n      }\n\n      this.setLeftAreaStyle()\n    }\n\n    onOpenMoreOperationPopOver () {\n      this.shouldOpenMoreOperationPopOver = true\n    }\n\n    onCheckUserAccountPage () {\n      window.open(this.status.account.url, \"_blank\")\n    }\n\n    onCheckStatusInSinglePage () {\n      this.$router.push({\n        name: this.$routersInfo.statuses.name,\n        params: {\n          statusId: this.status.id\n        }\n      })\n    }\n\n    async onDeleteStatusByOperateList () {\n      const targetStatusId = this.status.id\n\n      const doDeleteStatus = (await this.$confirm(this.$t(this.$i18nTags.statusCard.delete_status_confirm), {\n        okLabel: this.$t(this.$i18nTags.statusCard.do_delete_status_btn),\n        cancelLabel: this.$t(this.$i18nTags.statusCard.cancel_delete_status_btn),\n      })).result\n      if (doDeleteStatus) {\n        this.$emit('deleteStatus')\n        await this.deleteStatus({ statusId: targetStatusId })\n      }\n    }\n\n    onMuteStatusByOperateList () {\n      this.$emit('muteStatus', this.status.id)\n    }\n\n    onMuteUserByOperateList () {\n      this.$emit('muteUser', this.status.account.id)\n    }\n\n    getFromNowTime (createdAt: string) {\n      return moment(createdAt).fromNow(true)\n    }\n\n    setLeftAreaStyle () {\n      const headerPadding = 16 * 2\n      const rightAreaWidth = Math.max(60, this.$refs.rightArea.clientWidth)\n\n      this.leftAreaStyle = {\n        maxWidth: `${this.$refs.cardHeader.clientWidth - headerPadding - rightAreaWidth}px`\n      }\n    }\n  }\n\n  export default CardHeader\n</script>\n\n<style lang=\"less\" scoped>\n  .mu-card-header {\n    line-height: 1;\n    display: flex;\n    justify-content: space-between;\n\n    .left-area {\n      display: flex;\n      align-items: center;\n\n      .status-account-avatar {\n        margin-right: 8px;\n        cursor: pointer;\n        flex-shrink: 0;\n      }\n\n      .user-and-status-info {\n        display: flex;\n        flex-wrap: wrap;\n        align-items: center;\n        max-width: calc(100% - 34px - 8px);\n\n        .user-name {\n          cursor: pointer;\n          font-size: 15px;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          white-space: nowrap;\n          font-weight: 700;\n        }\n\n        .visibility-row {\n          display: flex;\n          align-items: center;\n\n          .arrow-container {\n            width: 18px;\n            height: 18px;\n          }\n\n          .visibility-info {\n            cursor: pointer;\n          }\n        }\n\n      }\n\n    }\n\n    .right-area {\n      display: flex;\n      align-items: center;\n\n      .status-from-now {\n        font-size: 13px;\n        font-weight: 400;\n      }\n\n      .card-header-action {\n        .header-icon {\n          cursor: pointer;\n          font-size: 18px;\n          margin-left: 10px;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/FullActionBar.vue",
    "content": "<template>\n  <div class=\"full-action-bar\">\n    <div class=\"reply-input-area\">\n      <mu-avatar class=\"current-user-avatar\" slot=\"avatar\" size=\"24\">\n        <img :src=\"currentUserAccount.avatar\">\n      </mu-avatar>\n\n      <div class=\"input-container\">\n        <cuckoo-input ref=\"cuckooInput\" @submit=\"onSubmitReplyContent\"\n                      @esc=\"onTryHideFullReplyActionArea\"\n                      :shouldShowSpoilerTextInputArea=\"shouldShowSpoilerTextInputArea\"\n                      :spoilerText.sync=\"replySpoilerTextValue\"\n                      :text.sync=\"inputValue\" :uploadProcesses.sync=\"uploadProcesses\"\n                      :presetAtAccounts=\"presetAtAccounts\"\n                      :placeholder=\"$t($i18nTags.statusCard.reply_to_main_status)\"/>\n      </div>\n\n    </div>\n\n    <div class=\"reply-action-area\">\n      <div class=\"left-area\">\n        <mu-button @click.stop=\"onSelectMediaFiles\" :disabled=\"uploadProcesses.length === 4\"\n                   class=\"operate-btn add-image secondary-read-text-color\" icon :title=\"$t($i18nTags.statusCard.add_photos)\">\n          <mu-icon class=\"reply-action-icon\" value=\"camera_alt\" />\n          <input ref=\"fileInput\" type=\"file\" @change=\"onUploadMediaFiles\"\n                 accept=\".jpg,.jpeg,.png,.gif,.webm,.mp4,.m4v,.mov,image/jpeg,image/png,image/gif,video/webm,video/mp4,video/quicktime\"\n                 style=\"display: none\" multiple/>\n        </mu-button>\n\n        <mu-button ref=\"visibilityTriggerBtn\" @click.stop=\"shouldOpenVisibilitySelectPopOver = true\" class=\"operate-btn change-visibility secondary-read-text-color\" icon :title=\"$t($i18nTags.statusCard.change_visibility)\">\n          <mu-icon class=\"reply-action-icon\" :value=\"getVisibilityDescInfo(visibility).icon\" />\n        </mu-button>\n\n        <mu-button v-if=\"uploadProcesses.length\" @click.stop=\"markMediaAsSensitive = !markMediaAsSensitive\"\n                   class=\"operate-btn secondary-read-text-color\" icon>\n          <mu-icon class=\"reply-action-icon\" :value=\"markMediaAsSensitive ? 'visibility_off' : 'visibility'\" />\n        </mu-button>\n\n        <mu-button @click.stop=\"shouldShowSpoilerTextInputArea = !shouldShowSpoilerTextInputArea\"\n                   class=\"operate-btn\" icon\n                   :class=\"shouldShowSpoilerTextInputArea ? 'secondary-theme-text-color' : 'secondary-read-text-color'\">\n          <mu-icon class=\"reply-action-icon\" value=\"add_alert\" />\n        </mu-button>\n\n      </div>\n      <div class=\"right-area\">\n        <mu-button flat class=\"operate-btn cancel\"\n                   color=\"secondary\" @click.stop=\"hideFullReplyActionArea\">{{$t($i18nTags.statusCard.cancel_post)}}</mu-button>\n        <mu-button flat class=\"operate-btn submit secondary-theme-text-color\" @click.stop=\"onSubmitReplyContent\"\n                   :disabled=\"!shouldEnableSubmitButton\">{{$t($i18nTags.statusCard.submit_post)}}</mu-button>\n      </div>\n    </div>\n\n    <visibility-select-pop-over :visibility.sync=\"visibility\"\n                                :open.sync=\"shouldOpenVisibilitySelectPopOver\"\n                                :trigger=\"visibilityTriggerBtn\"/>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { State, Action } from 'vuex-class'\n  import { VisibilityTypes } from '@/constant'\n  import { getVisibilityDescInfo } from '@/util'\n  import VisibilitySelectPopOver from '@/components/VisibilitySelectPopOver'\n  import Input from '@/components/Input'\n  import { mastodonentities } from '@/interface'\n\n  const maxUploadLength = 4\n\n  @Component({\n    components: {\n      'cuckoo-input': Input,\n      'visibility-select-pop-over': VisibilitySelectPopOver\n    }\n  })\n  class FullActionBar extends Vue {\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    $refs: {\n      cuckooInput: Input\n      replayTextInput: HTMLTextAreaElement\n      fileInput: HTMLInputElement\n      visibilityTriggerBtn: any\n    }\n\n    @Prop() status\n\n    @Prop() value\n\n    @Prop() replySpoilerText\n\n    @Prop() currentReplyToStatus\n\n    @Prop() descendantStatusList: Array<mastodonentities.Status>\n\n    @Prop() droppedFiles: Array<File>\n\n    @State('currentUserAccount') currentUserAccount\n\n    @State('appStatus') appStatus\n\n    @Action('postStatus') postStatus\n\n    isConfirmDialogShowing: boolean = false\n\n    postPrivacy_ = null\n\n    get postPrivacy () {\n      if (this.currentReplyToStatus.visibility === VisibilityTypes.DIRECT && !this.postPrivacy_) {\n        return VisibilityTypes.DIRECT\n      }\n\n      return this.postPrivacy_\n    }\n\n    set postPrivacy (val) {\n      this.postPrivacy_ = val\n    }\n\n    postMediaAsSensitiveMode: boolean = null\n\n    shouldShowSpoilerTextInputArea: boolean = null\n\n    get visibility () {\n      return this.postPrivacy || this.appStatus.settings.postPrivacy\n    }\n\n    set visibility (val) {\n      this.postPrivacy = val\n    }\n\n    get markMediaAsSensitive () {\n      return typeof this.postMediaAsSensitiveMode === 'boolean' ? this.postMediaAsSensitiveMode :\n        this.appStatus.settings.postMediaAsSensitiveMode\n    }\n\n    set markMediaAsSensitive (val) {\n      this.postMediaAsSensitiveMode = val\n    }\n\n    shouldOpenVisibilitySelectPopOver = false\n\n    uploadProcesses: Array<{\n      file: File,\n      hasStartedUpload: boolean,\n      uploadResult: mastodonentities.Attachment\n    }> = []\n\n    visibilityTriggerBtn: any = null\n\n    getVisibilityDescInfo = getVisibilityDescInfo\n\n    get shouldEnableSubmitButton () {\n      const isInUploadProcess = this.uploadProcesses.every(i => !i.uploadResult)\n\n      return this.uploadProcesses.length ? !isInUploadProcess : this.inputValue\n    }\n\n    get inputValue () {\n      return this.value\n    }\n\n    set inputValue (val) {\n      this.$emit('update:value', val)\n    }\n\n    get replySpoilerTextValue () {\n      return this.replySpoilerText\n    }\n\n    set replySpoilerTextValue (val) {\n      this.$emit('update:replySpoilerText', val)\n    }\n\n    get presetAtAccounts (): Array<mastodonentities.Account> {\n      const result: Array<mastodonentities.Account> = [];\n      [this.status, ...this.descendantStatusList].forEach(status => {\n        if (!result.find(acc => acc.id === status.account.id)) {\n          result.push(status.account)\n        }\n      })\n      return result\n    }\n\n    mounted () {\n      this.visibilityTriggerBtn = this.$refs.visibilityTriggerBtn.$el\n      this.$refs.cuckooInput.focus()\n      this.$refs.cuckooInput.updateSize()\n      this.prepareDroppedFiles()\n    }\n\n    @Watch('droppedFiles')\n    onDropFiles () {\n      this.prepareDroppedFiles()\n    }\n\n    async onSubmitReplyContent () {\n      if (!this.shouldEnableSubmitButton) return\n\n      const currentReplyToStatus = this.currentReplyToStatus\n\n      this.$emit('loadingStart')\n\n      await this.postStatus({\n        mainStatusId: this.status.id,\n        formData: {\n          status: this.value,\n          spoilerText: this.shouldShowSpoilerTextInputArea ? this.replySpoilerText : '',\n          inReplyToId: currentReplyToStatus.id,\n          visibility: this.visibility,\n          sensitive: this.postMediaAsSensitiveMode,\n          mediaIds: this.uploadProcesses.map(info => info.uploadResult.id)\n        }\n      })\n\n      this.$emit('loadingEnd')\n\n      this.$emit('replySuccess')\n\n      this.hideFullReplyActionArea()\n    }\n\n    onSelectMediaFiles () {\n      this.$refs.fileInput.click()\n    }\n\n    onUploadMediaFiles () {\n      Array.from(this.$refs.fileInput.files)\n        .splice(0, maxUploadLength - this.uploadProcesses.length)\n        .forEach(file => {\n          this.uploadProcesses.push({\n            file, hasStartedUpload: false, uploadResult: null\n          })\n        })\n\n      this.$refs.fileInput.value = ''\n    }\n\n    prepareDroppedFiles () {\n      if (!this.droppedFiles || !this.droppedFiles.length) return\n\n      const filesToUpload = [...this.droppedFiles].splice(0, maxUploadLength - this.uploadProcesses.length)\n\n      // todo show notification toast\n      if (!filesToUpload.length) return\n\n      filesToUpload.forEach(file => {\n        this.uploadProcesses.push({\n          file, hasStartedUpload: false, uploadResult: null\n        })\n      })\n    }\n\n    hideFullReplyActionArea () {\n      this.uploadProcesses = []\n      this.shouldShowSpoilerTextInputArea = false\n      this.$emit('hide')\n    }\n\n    async onTryHideFullReplyActionArea () {\n      if (this.isConfirmDialogShowing) return\n\n      if (this.inputValue || this.replySpoilerTextValue || this.uploadProcesses.length) {\n        this.isConfirmDialogShowing = true\n\n        const doHideFullReplyActionArea = (await this.$confirm(this.$t(this.$i18nTags.postStatusDialog.do_discard_message_confirm), {\n          okLabel: this.$t(this.$i18nTags.postStatusDialog.do_discard_message),\n          cancelLabel: this.$t(this.$i18nTags.postStatusDialog.do_keep_message),\n        })).result\n        if (doHideFullReplyActionArea) {\n          this.hideFullReplyActionArea()\n        } else {\n          this.$refs.cuckooInput.focus()\n        }\n\n        this.isConfirmDialogShowing = false\n      } else {\n        this.hideFullReplyActionArea()\n      }\n    }\n  }\n\n  export default FullActionBar\n</script>\n\n<style lang=\"less\" scoped>\n  .full-action-bar {\n    padding: 12px 16px 0 16px;\n\n    .reply-input-area {\n      display: flex;\n\n      .current-user-avatar {\n        margin-top: 6px;\n      }\n\n      .input-container {\n        width: 100%;\n        display: flex;\n        align-items: center;\n        flex-grow: 1;\n        margin-left: 16px;\n        padding: 9px 12px 8px 0;\n      }\n    }\n\n    .reply-action-area {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      height: 48px;\n      margin: 0 -12px;\n\n      .left-area {\n        display: flex;\n\n        .operate-btn {\n          width: 48px;\n          height: 48px;\n          display: flex;\n          align-items: center;\n          justify-content: center;\n\n          .reply-action-icon {\n            font-size: 18px;\n          }\n        }\n      }\n\n      .right-area {\n        display: flex;\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .full-action-bar {\n    .auto-size-text-area {\n      height: 18px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/FullReplyListItem.vue",
    "content": "<template>\n  <div class=\"full-reply-list-item\" v-loading=\"isListItemLoading\" @mouseover=\"onItemMouseOver\" @mouseout=\"onItemMouseOut\">\n    <div class=\"left-area\">\n      <mu-avatar @click=\"onCheckUserAccountPage\" class=\"status-replier-avatar\" slot=\"avatar\" size=\"34\">\n        <img :src=\"status.account.avatar\">\n      </mu-avatar>\n    </div>\n\n    <div class=\"content-area\" ref=\"contentArea\">\n\n      <div class=\"content-header\">\n        <div class=\"reply-user-display-name\">\n          <a @click=\"onCheckUserAccountPage\" class=\"primary-read-text-color\">\n            <span class=\"display-name\" v-html=\"status.account.display_name\"></span>\n            <span class=\"at-name secondary-read-text-color\">@{{getAccountAtName(status.account)}}</span>\n          </a>\n          <span v-if=\"status.favourites_count > 0\"\n                class=\"reply-favorites-count\"\n                :class=\"[ status.favourited ? 'primary-theme-text-color' : 'secondary-read-text-color' ]\">\n                  +{{status.favourites_count}}\n                </span>\n        </div>\n\n        <div class=\"operation-area\" ref=\"operationArea\" :style=\"operationAreaStyle\">\n          <span v-show=\"!shouldOpenMoreOperationPopOver && !shouldShowMoreOperationTriggerBtn\" class=\"reply-from-now secondary-read-text-color\">{{getFromNowTime()}}</span>\n          <mu-button v-show=\"shouldOpenMoreOperationPopOver || shouldShowMoreOperationTriggerBtn\" icon style=\"width: 16px; height: 16px\" @click=\"onOpenMoreOperationPopOver\">\n            <mu-icon class=\"header-icon secondary-read-text-color\" value=\"more_vert\"/>\n          </mu-button>\n        </div>\n      </div>\n\n      <div class=\"spoiler-text-area primary-read-text-color\" v-if=\"status.spoiler_text\">\n        <span v-html=\"status.spoiler_text\"/>\n        <mu-button flat small class=\"secondary-theme-text-color\" :style=\"{ minWidth: 'unset' }\"\n                   @click=\"shouldShowContentWhileSpoilerExists = !shouldShowContentWhileSpoilerExists\">\n          {{ $t(shouldShowContentWhileSpoilerExists ? $i18nTags.statusCard.hide_content : $i18nTags.statusCard.show_content) }}\n        </mu-button>\n      </div>\n\n      <mu-card-text class=\"status-content full-reply-status-content\"\n                    v-show=\"(status.spoiler_text ? shouldShowContentWhileSpoilerExists : true)\"\n                    v-html=\"status.content\"></mu-card-text>\n\n      <div v-if=\"neteaseMusicLink\" class=\"netease-music-panel\">\n        <iframe class=\"netease-music-iframe\" frameborder=\"no\" border=\"0\"\n                marginwidth=\"0\" marginheight=\"0\" height=86\n                :src=\"neteaseMusicLink\"></iframe>\n      </div>\n\n      <div v-if=\"youtubeVideoLink\" class=\"youtube-video-panel\">\n        <iframe class=\"youtube-video-iframe\"\n                :height=\"youtubeVideoIFrameHeight\"\n                :src=\"youtubeVideoLink\"\n                frameborder=\"0\"\n                allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n                allowfullscreen></iframe>\n      </div>\n\n      <div v-if=\"!status.reblog && hasLinkCardInfo\" class=\"full-reply-link-preview-area\">\n        <link-preview-panel :cardInfo=\"cardMap[status.id]\"/>\n      </div>\n\n      <div class=\"full-reply-attachment-area\">\n        <media-panel :mediaList=\"status.media_attachments\" :pixivCards=\"status.pixiv_cards\" :sensitive=\"status.sensitive\"/>\n      </div>\n\n      <div class=\"reply-action-list\">\n\n        <a class=\"reply-button secondary-theme-text-color\"\n           @click=\"onReplyToStatus\">{{$t($i18nTags.statusCard.reply_to_replier)}}</a>\n\n        <div class=\"plus-one-button secondary-theme-text-color\"\n             @click=\"onFavoriteButtonClick\"\n             :class=\"{ 'primary-theme-bg-color': status.favourited }\">\n          <a>+1</a>\n        </div>\n\n        <div class=\"reshare-button secondary-theme-text-color\"\n             @click=\"onReBlogButtonClick\"\n             :class=\"{ 'primary-theme-bg-color': status.reblogged }\">\n          <mu-icon class=\"share-icon\" value=\"share\" />\n        </div>\n\n      </div>\n    </div>\n\n    <mu-popover cover placement=\"left-start\"\n                :open.sync=\"shouldOpenMoreOperationPopOver\"\n                :trigger=\"moreOperationTriggerBtn\">\n      <mu-list>\n        <mu-list-item button @click.stop=\"onMuteStatus\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.mute_status)}}</mu-list-item-title>\n        </mu-list-item>\n        <mu-list-item button v-if=\"currentUserAccount.id !== status.account.id\"\n                      @click.stop=\"onMuteUser\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.mute_user)}}</mu-list-item-title>\n        </mu-list-item>\n        <mu-list-item button v-if=\"currentUserAccount.id === status.account.id\"\n                      @click.stop=\"onDeleteStatus\">\n          <mu-list-item-title>{{$t($i18nTags.statusCard.delete_status)}}</mu-list-item-title>\n        </mu-list-item>\n      </mu-list>\n    </mu-popover>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { Getter, Action, State } from 'vuex-class'\n  import * as moment from 'moment'\n  import { mastodonentities } from \"@/interface\"\n  import MediaPanel from './MediaPanel'\n  import LinkPreviewPanel from './LinkPreviewPanel'\n  import {getNetEaseMusicFrameLinkFromContentLink, getYoutubeVideoFrameLinkFromContentLink } from '@/util'\n  import * as $ from \"jquery\"\n\n  @Component({\n    components: {\n      'media-panel': MediaPanel,\n      'link-preview-panel': LinkPreviewPanel,\n    }\n  })\n  class FullReplyListItem extends Vue {\n\n    $refs: {\n      operationArea: HTMLDivElement\n      contentArea: HTMLDivElement\n    }\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    @Prop() status: mastodonentities.Status\n\n    @State('cardMap') cardMap\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n\n    @Action('updateFavouriteStatusById') updateFavouriteStatusById\n    @Action('updateReblogStatusById') updateReblogStatusById\n    @Action('deleteStatus') deleteStatus\n\n    @Getter('getAccountAtName') getAccountAtName\n\n    isListItemLoading: boolean = false\n\n    shouldShowMoreOperationTriggerBtn: boolean = false\n    shouldOpenMoreOperationPopOver: boolean = false\n    moreOperationTriggerBtn = null\n\n    shouldShowContentWhileSpoilerExists: boolean = false\n\n    operationAreaStyle = null\n\n    youtubeVideoIFrameHeight = 0\n\n    get hasLinkCardInfo () {\n      return this.cardMap[this.status.id]\n        && (Object.keys(this.cardMap[this.status.id]).length !== 0)\n        && this.cardMap[this.status.id].type === 'link'\n    }\n\n    get contentLinkList () {\n      return [...$(this.status.content).find('a')].map(a => {\n        return a.getAttribute('href')\n      })\n    }\n\n    get neteaseMusicLink () {\n      return this.contentLinkList.map(link => {\n        return getNetEaseMusicFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    get youtubeVideoLink () {\n      return this.contentLinkList.map(link => {\n        return getYoutubeVideoFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    mounted () {\n      this.operationAreaStyle = {\n        // todo 2 is magic number\n        width: `${this.$refs.operationArea.clientWidth + 2}px`\n      }\n\n      this.youtubeVideoIFrameHeight = this.$refs.contentArea.clientWidth * 315 / 560\n    }\n\n    getFromNowTime () {\n      return moment(this.status.created_at).fromNow(true)\n    }\n\n    onFavoriteButtonClick () {\n      this.updateFavouriteStatusById({\n        favourited: !this.status.favourited,\n        mainStatusId: this.status.id,\n        targetStatusId: this.status.id\n      })\n    }\n\n    onReBlogButtonClick () {\n      this.updateReblogStatusById({\n        reblogged: !this.status.reblogged,\n        mainStatusId: this.status.id,\n        targetStatusId: this.status.id\n      })\n    }\n\n    onReplyToStatus () {\n      this.$emit('reply', this.status)\n    }\n\n    onOpenMoreOperationPopOver (e) {\n      this.moreOperationTriggerBtn = e.target\n      this.shouldOpenMoreOperationPopOver = true\n    }\n\n    onCheckUserAccountPage () {\n      window.open(this.status.account.url, \"_blank\")\n    }\n\n    async onDeleteStatus () {\n      const doDeleteStatus = (await this.$confirm(this.$t(this.$i18nTags.statusCard.delete_status_confirm), {\n        okLabel: this.$t(this.$i18nTags.statusCard.do_delete_status_btn),\n        cancelLabel: this.$t(this.$i18nTags.statusCard.cancel_delete_status_btn),\n      })).result\n      if (doDeleteStatus) {\n        this.isListItemLoading = true\n        await this.deleteStatus({ statusId: this.status.id })\n      }\n    }\n\n    onMuteStatus () {\n      this.$emit('muteStatus', this.status.id)\n    }\n\n    async onMuteUser () {\n      this.$emit('muteUser', this.status.account.id)\n    }\n\n    onItemMouseOver () {\n      this.shouldShowMoreOperationTriggerBtn = true\n    }\n\n    onItemMouseOut () {\n      this.shouldShowMoreOperationTriggerBtn = false\n    }\n\n  }\n\n  export default FullReplyListItem\n</script>\n\n<style lang=\"less\" scoped>\n  .full-reply-list-item {\n    position: relative;\n    display: flex;\n    padding: 12px 16px;\n\n    .status-replier-avatar {\n      cursor: pointer;\n    }\n\n    .content-area {\n      flex-grow: 1;\n      margin: 0 10px 0 16px;\n      display: flex;\n      flex-direction: column;\n      width: 0;\n\n      .content-header {\n        display: flex;\n        justify-content: space-between;\n        min-height: 26px;\n        line-height: 26px;\n\n        .reply-user-display-name {\n          display: flex;\n          align-items: center;\n\n          > a {\n            margin: 0;\n            font-size: 15px;\n            font-weight: 500;\n            text-overflow: ellipsis;\n            overflow: hidden;\n            cursor: pointer;\n          }\n\n          .reply-favorites-count {\n            font-size: 13px;\n            font-weight: 500;\n            margin-left: 8px;\n          }\n        }\n\n        .operation-area {\n          display: flex;\n          flex-direction: row-reverse;\n\n          .reply-from-now {\n            font-size: 13px;\n            white-space: nowrap;\n          }\n        }\n      }\n\n      .netease-music-panel {\n        margin-left: -10px;\n        margin-right: -20px;\n      }\n\n      .full-reply-status-content {\n        padding: 0;\n      }\n\n      .full-reply-link-preview-area {\n        margin: 12px 0 0 4px;\n      }\n\n      .reply-action-list {\n        display: flex;\n        align-items: center;\n        margin-top: 6px;\n\n        .common-style-mixin() {\n          cursor: pointer;\n          font-size: 13px;\n          margin: 0 8px;\n        }\n\n        .reply-button {\n          .common-style-mixin();\n          margin-left: 0;\n        }\n\n        .plus-one-button {\n          .common-style-mixin();\n          width: 24px;\n          height: 24px;\n          line-height: 24px;\n          text-align: center;\n          border-radius: 50%;\n        }\n\n        .reshare-button {\n          .common-style-mixin();\n          line-height: 1;\n          width: 24px;\n          height: 24px;\n          text-align: center;\n          border-radius: 50%;\n\n          .share-icon {\n            font-size: 16px;\n            line-height: 24px;\n          }\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/LinkPreviewPanel.vue",
    "content": "<template>\n  <div class=\"link-preview-panel-container\" @click=\"onLinkPreviewPanelClick\">\n    <div class=\"content-area\">\n      <img v-if=\"cardInfo.image\" class=\"preview-image\" :src=\"cardInfo.image\" :alt=\"cardInfo.title\"/>\n      <div class=\"text-area\">\n        <div class=\"link-preview-text primary-read-text-color\">\n          {{cardInfo.title}}\n        </div>\n        <div class=\"link-preview-text link-preview-description primary-read-text-color\">\n          {{cardInfo.description}}\n        </div>\n        <div class=\"link-preview-text link-preview-url secondary-read-text-color\">\n          {{cardInfo.url}}\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import {} from 'vuex-class'\n  import { mastodonentities } from '@/interface'\n\n  @Component({})\n  class LinkPreviewPanel extends Vue {\n\n    @Prop() cardInfo: mastodonentities.Card\n\n    onLinkPreviewPanelClick () {\n      window.open(this.cardInfo.url, \"_blank\")\n    }\n  }\n\n  export default LinkPreviewPanel\n</script>\n\n<style lang=\"less\" scoped>\n  .link-preview-panel-container {\n    cursor: pointer;\n\n    .content-area {\n      display: flex;\n      align-items: center;\n      overflow: hidden;\n\n      .preview-image {\n        width: 110px;\n        height: 110px;\n        margin-right: 16px;\n        flex-shrink: 0;\n        object-fit: cover;\n      }\n\n      .text-area {\n        .link-preview-text {\n          font-size: 20px;\n          font-weight: 300;\n          margin-bottom: 8px;\n          line-height: 24px;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          -webkit-box-orient: vertical;\n          -webkit-line-clamp: 2;\n          display: -webkit-box;\n          max-height: 48px;\n        }\n\n        .link-preview-description {\n          font-size: 16px;\n        }\n\n        .link-preview-url {\n          font-size: 12px;\n        }\n      }\n    }\n  }\n</style>\n\n<style lang=\"less\">\n  .full-reply-link-preview-area {\n    .preview-image {\n      width: 72px !important;\n      height: 72px !important;\n      margin-right: 12px !important;\n    }\n\n    .link-preview-text {\n      font-size: 16px !important;\n      margin-top: 8px !important;\n      margin-bottom: 8px !important;\n      line-height: 18px !important;\n      max-height: 36px !important;\n    }\n\n    .link-preview-description {\n      font-size: 12px !important;\n    }\n\n    .link-preview-url {\n      font-size: 8px !important;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/MediaPanel.vue",
    "content": "<template>\n  <div class=\"media-panel-container\" v-if=\"combinedMediaList.length > 0\">\n    <div class=\"media-area\" ref=\"mediaArea\" :style=\"mediaAreaScrollStyle\"\n         :class=\"{ 'single-media-area': combinedMediaList.length === 1 }\">\n      <place-holder-media-item class=\"media-item\" v-for=\"(media, index) in combinedMediaList\" :key=\"index\"\n                               @click.native=\"onMediaItemClick(index)\" :holderStyle=\"getMediaSizeStyle(index)\"\n                               :url=\"media.url\" :mediaType=\"media.type\"\n                               :shouldShowSensitiveCover.sync=\"shouldShowSensitiveCover\"/>\n\n      <div class=\"sensitive-alert-cover\" v-show=\"shouldShowSensitiveCover\" @click=\"shouldShowSensitiveCover = false\">\n        <p v-html=\"$t($i18nTags.statusCard.sensitive_media_alert)\"></p>\n      </div>\n    </div>\n\n    <mu-dialog class=\"light-box\" transition=\"fade\" ref=\"lightBox\"\n               @click.native.stop=\"onLightBoxClick\"\n               :open.sync=\"shouldShowLightBox\" :overlay-opacity=\"0.7\">\n      <mu-icon class=\"close-icon\" value=\"close\" @click=\"shouldShowLightBox = false\"/>\n      <mu-carousel :cycle=\"false\" :active=\"lightBoxActiveIndex\" transition=\"fade\"\n                   @click.native.stop=\"onCarouselBackgroundClick\"\n                   :hide-indicators=\"(combinedMediaList.length === 1) || !shouldShowLightBoxControlBtn\"\n                   :hide-controls=\"(combinedMediaList.length === 1) || !shouldShowLightBoxControlBtn\">\n        <mu-carousel-item v-for=\"(mediaInfo, index) in combinedMediaList\" :key=\"index\">\n          <div class=\"light-box-item\" @click.stop=\"onLightBoxMediaItemClick\">\n            <img v-if=\"mediaInfo.type === mediaTypes.IMAGE\" :src=\"mediaInfo.url\"/>\n            <video v-if=\"mediaInfo.type === mediaTypes.GIFV || mediaInfo.type === mediaTypes.VIDEO\"\n                   controls :loop=\"mediaInfo.type === mediaTypes.GIFV\" :src=\"mediaInfo.url\"/>\n          </div>\n        </mu-carousel-item>\n      </mu-carousel>\n    </mu-dialog>\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { State } from 'vuex-class'\n  import { AttachmentTypes, StatusCardTypes } from '@/constant'\n  import { documentGlobalEventBus } from '@/util'\n  import { mastodonentities } from '@/interface'\n  import PlaceHolderMediaItem from './PlaceHolderMediaItem'\n  import ImageMeta = mastodonentities.ImageMeta\n  import GifvMeta = mastodonentities.GifvMeta\n\n  let mediaPanelKeyDownEventListener\n\n  @Component({\n    components: {\n      'place-holder-media-item': PlaceHolderMediaItem\n    }\n  })\n  class MediaPanel extends Vue {\n\n    $refs: {\n      mediaArea: HTMLDivElement\n      mediaPanelContainer: HTMLDivElement\n      lightBox: {\n        $el: HTMLDivElement\n      }\n    }\n\n    @Prop() mediaList?: Array<mastodonentities.Attachment>\n\n    @Prop({ default: () => [] }) pixivCards?: Array<{ url: string, image_url: string }>\n\n    @Prop({ default: () => {} }) cardInfo: mastodonentities.Card\n\n    @Prop() sensitive?: boolean\n\n    @State('appStatus') appStatus\n\n    manuallyShowSensitiveCover: boolean = null\n\n    isMounted: boolean = false\n\n    onLightBoxClick () { }\n\n    get fixedPixivCards () {\n      if (this.mediaList.length) {\n        return []\n      }\n\n      return this.pixivCards\n    }\n\n    get fixedCardInfo () {\n      if (this.mediaList.length ||\n        this.fixedPixivCards.length) {\n        return null\n      }\n\n      if (!this.cardInfo || (this.cardInfo.type !== StatusCardTypes.PHOTO)) {\n        return null\n      }\n\n      return this.cardInfo\n    }\n\n    mounted () {\n      this.isMounted = true\n    }\n\n    get mediaAreaWidth () {\n      if (!this.isMounted) return null\n\n      return this.$refs.mediaArea.clientWidth\n    }\n\n    get mediaAreaScrollStyle () {\n      if (this.shouldShowSensitiveCover) {\n        return {\n          overflow: 'hidden'\n        }\n      } else {\n        return null\n      }\n    }\n\n    //\n    getMediaSizeStyle (mediaIndex: number) {\n      if (!this.isMounted) return {}\n\n      if (this.combinedMediaList.length === 1) {\n\n        let aspect: number = 1\n\n        // for normal media list\n        if (this.mediaList.length === 1) {\n\n          if (!this.mediaList[0].meta) return {}\n\n          const mediaType = this.mediaList[0].type\n\n          if (mediaType === AttachmentTypes.IMAGE) {\n            aspect = (this.mediaList[0].meta as ImageMeta).original.aspect\n          }\n\n          else if (mediaType === AttachmentTypes.VIDEO || mediaType === AttachmentTypes.GIFV) {\n            const originInfo = (this.mediaList[0].meta as GifvMeta).original\n            aspect = originInfo.width / originInfo.height\n          }\n        }\n\n        // for pixiv cards and photo type card info\n        if (this.fixedPixivCards.length === 1) {\n          // pixiv cards media size is 1050 * 550 by now\n          aspect = 1050 / 550\n        }\n\n        if (this.fixedCardInfo) {\n          aspect = this.fixedCardInfo.width / this.fixedCardInfo.height\n        }\n\n        return { height: `${this.mediaAreaWidth / aspect}px` }\n      } else if (this.combinedMediaList.length > 1) {\n\n        if (!this.mediaList[mediaIndex].meta) return {}\n\n        // multi media's height was static by now\n        const height = 212\n\n        return { width: `${height * (this.mediaList[mediaIndex].meta as ImageMeta).original.aspect}px` }\n      }\n\n      return {}\n    }\n\n    get shouldShowSensitiveCover () {\n      if (typeof this.manuallyShowSensitiveCover === 'boolean') {\n        return this.manuallyShowSensitiveCover\n      }\n\n      if (this.appStatus.settings.showSensitiveContentMode) return false\n\n      return this.sensitive\n    }\n\n    set shouldShowSensitiveCover (val) {\n      this.manuallyShowSensitiveCover = val\n      this.$refs.mediaArea.scrollTo(0, 0)\n    }\n\n    get combinedMediaList () {\n      const mediaListPart = this.mediaList.map(item => {\n        const url = item.url || item.remote_url\n\n        let type: string = item.type\n\n        if (type === AttachmentTypes.UNKNOWN) {\n          type = url.endsWith('.mp4') ? AttachmentTypes.GIFV : AttachmentTypes.IMAGE\n        }\n\n        return { url, type, previewUrl: item.preview_url }\n      })\n\n      const pixivCardsPart = this.fixedPixivCards.map(item => {\n        return { url: item.image_url, type: this.mediaTypes.IMAGE }\n      })\n\n      const photoCardPart = []\n      if (this.fixedCardInfo) {\n        photoCardPart.push({\n          url: this.fixedCardInfo.image,\n          type: this.mediaTypes.IMAGE\n        })\n      }\n\n      return [...mediaListPart, ...pixivCardsPart, ...photoCardPart]\n    }\n\n    get mediaAreaClass () {\n      const mediaAreaClassList = [\n        'one-media', 'two-medias', 'three-medias', 'four-medias'\n      ]\n\n      return mediaAreaClassList[this.combinedMediaList.length - 1]\n    }\n\n    mediaTypes = AttachmentTypes\n\n    shouldShowLightBox: boolean = false\n\n    shouldShowLightBoxControlBtn: boolean = true\n\n    lightBoxActiveIndex: number = 0\n\n    @Watch('$route')\n    onRouteChanged () {\n      this.shouldShowLightBox = false\n    }\n\n    @Watch('shouldShowLightBox')\n    onLightBoxStatusChanged () {\n      if (this.shouldShowLightBox) {\n        mediaPanelKeyDownEventListener = e => this.onMediaPanelKeyDown(e)\n        documentGlobalEventBus.on('keydown', mediaPanelKeyDownEventListener)\n      } else {\n        documentGlobalEventBus.off('keydown', mediaPanelKeyDownEventListener)\n      }\n    }\n\n    onMediaItemClick (mediaItemIndex: number) {\n      this.shouldShowLightBox = true\n      this.lightBoxActiveIndex = mediaItemIndex\n    }\n\n    onMediaPanelKeyDown (e) {\n      e.stopPropagation()\n      e.preventDefault()\n\n      switch (e.key) {\n        case 'h': {\n          if (this.lightBoxActiveIndex === 0) {\n            this.lightBoxActiveIndex = this.combinedMediaList.length - 1\n          } else {\n            this.lightBoxActiveIndex --\n          }\n          break\n        }\n\n        case 'l': {\n          if (this.lightBoxActiveIndex === this.combinedMediaList.length - 1) {\n            this.lightBoxActiveIndex = 0\n          } else {\n            this.lightBoxActiveIndex ++\n          }\n          break\n        }\n      }\n    }\n\n    onCarouselBackgroundClick (e) {\n      if (e.target.className === 'mu-carousel-item') {\n        this.shouldShowLightBox = false\n      }\n    }\n\n    onLightBoxMediaItemClick () {\n      this.shouldShowLightBoxControlBtn = !this.shouldShowLightBoxControlBtn\n    }\n  }\n\n  export default MediaPanel\n</script>\n\n<style lang=\"less\" scoped>\n  .media-panel-container {\n\n    .media-area {\n      position: relative;\n\n      &.single-media-area {\n        width: 100%;\n        height: auto;\n        padding-left: 0;\n\n      }\n\n      .sensitive-alert-cover {\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        display: flex;\n        background: rgba(0,0,0,0.2);\n        color: #fff;\n        font-weight: 700;\n        align-items: center;\n        justify-content: center;\n        cursor: pointer;\n      }\n    }\n\n  }\n</style>\n\n<style lang=\"less\">\n  .media-panel-container {\n    img, video {\n      display: block;\n      cursor: zoom-in;\n      filter: blur(0);\n\n      &.sensitive-hide {\n        filter: blur(20px);\n      }\n\n      -webkit-transition: filter 200ms;\n      -moz-transition: filter 200ms;\n      -ms-transition: filter 200ms;\n      -o-transition: filter 200ms;\n      transition: filter 200ms;\n    }\n  }\n\n  .hide-sensitive-btn {\n    position: absolute;\n    left: 6px;\n    top: 6px;\n    background-color: rgba(0,0,0,.6);\n    min-width: unset;\n    height: auto;\n    color: hsla(0,0%,100%,.7);\n\n    &:hover {\n      background-color: rgba(0,0,0,.9);\n    }\n\n    .mu-button-wrapper {\n      padding: 0;\n    }\n  }\n\n  .light-box {\n    .mu-dialog {\n      background-color: transparent;\n      max-width: unset;\n\n      .close-icon {\n        font-size: 46px;\n        position: fixed;\n        z-index: 1;\n        right: 20px;\n        top: 20px;\n        cursor: pointer;\n        color: #fff;\n      }\n\n      .mu-dialog-body {\n        padding: 0;\n        width: 100vw;\n        height: 100vh;\n\n        .mu-carousel {\n          height: 100%;\n\n          .mu-carousel-item {\n            display: flex;\n            align-items: center;\n            justify-content: center;\n\n            .light-box-item {\n              img {\n                max-width: 100vw;\n                max-height: 80vh;\n                width: auto;\n                height: auto;\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/PlaceHolderMediaItem.vue",
    "content": "<template>\n  <div class=\"placeholder-media-item-container\" :style=\"placeHolderItemStyle\">\n\n    <div class=\"placeholder-area\" v-if=\"!showSensitiveCover && !isMediaLoaded\">\n      <mu-icon class=\"status-icon\" :value=\"placeHolderStatusIconValue\"/>\n    </div>\n\n    <img v-show=\"isMediaLoaded\" v-if=\"(mediaType === mediaTypes.IMAGE)\"\n         :class=\"[showSensitiveCover && 'sensitive-hide']\"\n         :src=\"url\" @load=\"onMediaContentLoaded\" @error=\"onMediaContentLoadFailed\"/>\n\n    <div class=\"gifv-container\" v-show=\"isMediaLoaded\" v-if=\"mediaType === mediaTypes.GIFV || mediaType === mediaTypes.VIDEO\">\n      <video width=\"100%\" controls :loop=\"mediaType === mediaTypes.GIFV\"\n             :class=\"[showSensitiveCover && 'sensitive-hide']\"\n             :src=\"url\" @loadstart=\"onMediaContentLoaded\" @error=\"onMediaContentLoadFailed\"/>\n    </div>\n\n    <mu-button class=\"hide-sensitive-btn\" v-show=\"!showSensitiveCover && isMediaLoaded\" @click.stop=\"onShowSensitiveCover\">\n      <mu-icon value=\"visibility_off\"/>\n    </mu-button>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { AttachmentTypes } from '@/constant'\n\n  @Component({})\n  class PlaceHolderMediaItem extends Vue {\n\n    @Prop() url: string\n\n    @Prop() mediaType: string\n\n    @Prop() shouldShowSensitiveCover: boolean\n\n    @Prop() holderStyle\n\n    get showSensitiveCover () {\n      return this.shouldShowSensitiveCover\n    }\n\n    get placeHolderItemStyle () {\n      return this.isMediaLoaded ? null : this.holderStyle\n    }\n\n    get placeHolderStatusIconValue () {\n      return this.isMediaLoadFailed ? 'broken_image' : this.mediaType === AttachmentTypes.IMAGE ? 'photo' : 'videocam'\n    }\n\n    get isVideo () {\n      return null\n    }\n\n    mediaTypes = AttachmentTypes\n\n    isMediaLoaded = false\n\n    isMediaLoadFailed = false\n\n    onShowSensitiveCover () {\n      this.$emit('update:shouldShowSensitiveCover', true)\n    }\n\n    onMediaContentLoaded () {\n      this.isMediaLoaded = true\n    }\n\n    onMediaContentLoadFailed () {\n      this.isMediaLoadFailed = true\n    }\n  }\n\n  export default PlaceHolderMediaItem\n\n</script>\n\n<style lang=\"less\" scoped>\n  .placeholder-media-item-container {\n    width: auto;\n    max-width: 100%;\n\n    .placeholder-area {\n      width: 100%;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      background-color: #ccc;\n      color: #b0b0b0;\n\n      .status-icon {\n        font-size: 30px;\n      }\n    }\n\n    .gifv-container {\n      width: 100%;\n    }\n\n    .sensitive-hide {\n      filter: blur(20px);\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/SimpleActionBar.vue",
    "content": "<template>\n  <div class=\"simple-action-bar\">\n\n    <div class=\"left-area\">\n      <mu-avatar v-if=\"isOAuthUser\" class=\"current-user-avatar\" slot=\"avatar\" size=\"24\">\n        <img :src=\"currentUserAccount.avatar\">\n      </mu-avatar>\n\n      <div v-if=\"isOAuthUser\" :style=\"activeReplyEntryStyle\"\n           class=\"active-reply-entry placeholder-read-text-color\"\n           @click=\"onReplyToStatus\">\n        {{$t($i18nTags.statusCard.reply_to_main_status)}}\n      </div>\n    </div>\n\n    <div class=\"right-area\">\n      <div class=\"plus-one operate-btn-group\">\n        <mu-button :disabled=\"!isOAuthUser\" class=\"circle-btn\" icon @click=\"onFavoriteButtonClick\"\n                   :class=\"{ 'primary-theme-bg-color': operateCheckTargetStatus.favourited }\">\n          +1\n        </mu-button>\n        <span v-if=\"operateCheckTargetStatus.favourites_count > 0\" class=\"count\">{{operateCheckTargetStatus.favourites_count}}</span>\n      </div>\n      <div class=\"share operate-btn-group\" v-if=\"shouldShowReblogButton\">\n        <mu-button :disabled=\"!isOAuthUser\" class=\"circle-btn unset-display\" @click=\"onReBlogButtonClick\"\n                   :class=\"{ 'primary-theme-bg-color': operateCheckTargetStatus.reblogged }\" icon>\n          <mu-icon class=\"share-icon\" value=\"share\" />\n        </mu-button>\n        <span v-if=\"operateCheckTargetStatus.reblogs_count > 0\" class=\"count\">{{operateCheckTargetStatus.reblogs_count}}</span>\n      </div>\n    </div>\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { State, Getter, Action } from 'vuex-class'\n  import { I18nLocales, VisibilityTypes } from '@/constant'\n  import { mastodonentities, cuckoostore } from '@/interface'\n\n  @Component({})\n  class SimpleActionBar extends Vue {\n\n    @Prop() status: mastodonentities.Status\n\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n\n    @State('appStatus') appStatus\n\n    @Getter('isOAuthUser') isOAuthUser\n\n    @Action('updateFavouriteStatusById') updateFavouriteStatusById\n    @Action('updateReblogStatusById') updateReblogStatusById\n\n    get shouldShowReblogButton () {\n      return this.status.visibility !== VisibilityTypes.DIRECT\n    }\n\n    get operateCheckTargetStatus () {\n      return this.status.reblog || this.status\n    }\n\n    get activeReplyEntryStyle () {\n      if (this.appStatus.settings.locale === I18nLocales.JA) {\n        return {\n          fontSize: '12px'\n        }\n      }\n    }\n\n    onFavoriteButtonClick () {\n      const mainStatusId = this.status.id\n      const targetStatusId = this.operateCheckTargetStatus.id\n\n      this.updateFavouriteStatusById({\n        favourited: !this.operateCheckTargetStatus.favourited,\n        mainStatusId, targetStatusId\n      })\n    }\n\n    onReBlogButtonClick () {\n      const mainStatusId = this.status.id\n      const targetStatusId = this.operateCheckTargetStatus.id\n\n      this.updateReblogStatusById({\n        reblogged: !this.operateCheckTargetStatus.reblogged,\n        mainStatusId, targetStatusId\n      })\n    }\n\n    onReplyToStatus () {\n      this.$emit('reply', this.status)\n    }\n\n    get operateBtnStyle () {\n      if (!this.isOAuthUser) {\n        return {\n          cursor: ''\n        }\n      }\n    }\n\n  }\n\n  export default SimpleActionBar\n</script>\n\n<style lang=\"less\" scoped>\n  .simple-action-bar {\n    min-height: 60px;\n    display: flex;\n    justify-content: space-between;\n\n    .left-area {\n      padding: 12px 16px;\n      display: flex;\n      align-items: center;\n      flex-grow: 1;\n\n      .active-reply-entry {\n        margin-left: 16px;\n        height: 36px;\n        font-size: 14px;\n        line-height: 36px;\n        font-weight: 300;\n        flex-grow: 1;\n      }\n    }\n\n    .right-area {\n      margin: 12px 8px;\n      display: flex;\n      align-items: center;\n      flex-shrink: 0;\n\n      .operate-btn-group {\n        display: flex;\n\n        &.plus-one {\n          font-size: 12px;\n        }\n\n        .share-icon {\n          font-size: 18px;\n        }\n\n        .count {\n          line-height: 36px;\n          font-size: 13px;\n          margin-right: 6px;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/StatusCard/index.vue",
    "content": "<template>\n  <div class=\"status-card-container\" @dragenter=\"onDragFileOver\" ref=\"statusCardContainer\">\n    <mu-card class=\"status-card status-card-bg-color\" v-loading=\"isCardLoading\"\n             v-drag-over=\"isFileDragOver\"\n             @cuckooDragOver=\"onDragFileOver\"\n             @cuckooDragleave=\"isFileDragOver = false\"\n             @cuckooDrop=\"onDropFile\">\n\n      <card-header :status=\"status\" @deleteStatus=\"isCardLoading = true\"\n                   @muteStatus=\"onMuteStatus\" @muteUser=\"onMuteUser\"/>\n\n      <div class=\"spoiler-text-area primary-read-text-color\" v-if=\"status.spoiler_text\">\n        <span v-html=\"status.spoiler_text\"/>\n        <mu-button flat small class=\"secondary-theme-text-color\" :style=\"{ minWidth: 'unset' }\"\n                   @click=\"shouldShowContentWhileSpoilerExists = !shouldShowContentWhileSpoilerExists\">\n          {{ $t(shouldShowContentWhileSpoilerExists ? $i18nTags.statusCard.hide_content : $i18nTags.statusCard.show_content) }}\n        </mu-button>\n      </div>\n\n      <mu-card-text v-if=\"!status.reblog && status.content\" v-show=\"(status.spoiler_text ? shouldShowContentWhileSpoilerExists : true)\"\n                    class=\"status-content main-status-content\"\n                    v-html=\"status.content\" :style=\"mainStatusContentStyle\"/>\n\n      <div v-if=\"neteaseMusicLink\" class=\"netease-music-panel\">\n        <iframe class=\"netease-music-iframe\" frameborder=\"no\" border=\"0\"\n                marginwidth=\"0\" marginheight=\"0\" height=86\n                :src=\"neteaseMusicLink\"></iframe>\n      </div>\n\n      <div v-if=\"youtubeVideoLink\" class=\"youtube-video-panel\">\n        <iframe class=\"youtube-video-iframe\"\n                :height=\"youtubeVideoIFrameHeight\"\n                :src=\"youtubeVideoLink\"\n                frameborder=\"0\"\n                allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n                allowfullscreen></iframe>\n      </div>\n\n      <div v-if=\"!status.reblog && hasLinkCardInfo\" class=\"main-link-preview-area\">\n        <mu-divider class=\"link-preview-divider\"/>\n        <link-preview-panel :cardInfo=\"cardMap[status.id]\"/>\n      </div>\n\n      <mu-divider v-if=\"!status.media_attachments.length && !(status.pixiv_cards || []).length\"/>\n\n      <div v-if=\"!status.reblog\" class=\"main-attachment-area\">\n        <media-panel :mediaList=\"status.media_attachments\"\n                     :pixivCards=\"status.pixiv_cards\"\n                     :cardInfo=\"mainStatusCardInfo\" :sensitive=\"status.sensitive\"/>\n      </div>\n\n      <div v-if=\"status.reblog\" class=\"reblog-area\">\n        <div class=\"reblog-plain-info-area\">\n          <a @click=\"onCheckSharedOriginalPost\" class=\"reblog-source-link\" v-html=\"$t($i18nTags.statusCard.originally_shared_by, {\n              displayName: status.reblog.account.display_name,\n              atName: getAccountAtName(status.reblog.account)\n            })\">\n          </a>\n          <mu-card-text v-if=\"status.reblog.content\" class=\"status-content reblog-status-content\" v-html=\"status.reblog.content\" />\n        </div>\n\n        <div v-if=\"reblogNeteaseMusicLink\" class=\"netease-music-panel\">\n          <iframe class=\"netease-music-iframe\" frameborder=\"no\" border=\"0\"\n                  marginwidth=\"0\" marginheight=\"0\" height=86\n                  :src=\"reblogNeteaseMusicLink\"></iframe>\n        </div>\n\n        <div v-if=\"reblogYoutubeVideoLink\" class=\"youtube-video-panel\">\n          <iframe class=\"youtube-video-iframe\"\n                  :height=\"youtubeVideoIFrameHeight\"\n                  :src=\"reblogYoutubeVideoLink\"\n                  frameborder=\"0\"\n                  allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\"\n                  allowfullscreen></iframe>\n        </div>\n\n        <div v-if=\"reblogHasLinkCardInfo\" class=\"main-link-preview-area\">\n          <mu-divider class=\"link-preview-divider\"/>\n          <link-preview-panel :cardInfo=\"cardMap[status.reblog.id]\"/>\n        </div>\n\n        <div class=\"reblog-attachment-area\">\n          <media-panel\n            :mediaList=\"status.reblog.media_attachments\"\n            :pixivCards=\"status.reblog.pixiv_cards\"\n            :cardInfo=\"cardMap[status.reblog.id]\" :sensitive=\"status.reblog.sensitive\"/>\n        </div>\n      </div>\n\n      <div class=\"reply-area-full\">\n        <div class=\"full-reply-list\" ref=\"replyListContainer\">\n          <full-reply-list-item v-for=\"replierStatus in descendantStatusList\" @muteStatus=\"onMuteStatus\" @muteUser=\"onMuteUser\"\n                                :key=\"replierStatus.id\" :status=\"replierStatus\" @reply=\"onReplyToStatus(replierStatus)\"/>\n        </div>\n      </div>\n\n      <div class=\"current-reply-to-info-area\" v-if=\"currentReplyToStatus\">\n        <mu-chip class=\"reply-to-account-info\" color=\"primary\" @delete=\"hideFullReplyActionArea\" delete>\n          <mu-avatar :size=\"32\">\n            <img :src=\"currentReplyToStatus.account.avatar\">\n          </mu-avatar>\n          <span v-html=\"currentReplyToStatus.account.display_name\"/>\n          <span>&nbsp;@{{currentReplyToStatus.account.username}}</span>\n        </mu-chip>\n      </div>\n\n      <mu-card-actions class=\"card-action-area\">\n        <simple-action-bar v-show=\"!shouldShowFullReplyActionArea\" :status=\"status\"\n                           @reply=\"onReplyToStatus(status)\"/>\n\n        <full-action-bar v-if=\"isOAuthUser && shouldShowFullReplyActionArea\"\n                         :currentReplyToStatus=\"currentReplyToStatus\"\n                         :descendantStatusList=\"descendantStatusList\"\n                         :droppedFiles=\"droppedFiles\" :replySpoilerText.sync=\"replySpoilerText\"\n                         :status=\"status\" :value.sync=\"replyInputValue\" @hide=\"hideFullReplyActionArea\"\n                         @loadingStart=\"isCardLoading = true\" @loadingEnd=\"isCardLoading = false\" @replySuccess=\"onReplySuccess\"/>\n      </mu-card-actions>\n    </mu-card>\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop, Watch } from 'vue-property-decorator'\n  import { State, Getter, Mutation } from 'vuex-class'\n  import { mastodonentities } from '@/interface'\n  import { StatusCardTypes } from '@/constant'\n  import * as $ from 'jquery'\n\n  import CardHeader from './CardHeader'\n  import MediaPanel from './MediaPanel'\n  import LinkPreviewPanel from './LinkPreviewPanel'\n  import FullReplyListItem from './FullReplyListItem'\n  import SimpleActionBar from './SimpleActionBar'\n  import FullActionBar from './FullActionBar'\n\n  import VisibilitySelectPopOver from '@/components/VisibilitySelectPopOver'\n  import { getNetEaseMusicFrameLinkFromContentLink, getYoutubeVideoFrameLinkFromContentLink } from '@/util'\n\n  @Component({\n    components: {\n      'card-header': CardHeader,\n      'media-panel': MediaPanel,\n      'link-preview-panel': LinkPreviewPanel,\n      'full-reply-list-item': FullReplyListItem,\n      'simple-action-bar': SimpleActionBar,\n      'full-action-bar': FullActionBar,\n      'visibility-select-pop-over': VisibilitySelectPopOver\n    }\n  })\n  class StatusCard extends Vue {\n\n    $router\n\n    $routersInfo\n\n    $refs: {\n      replyListContainer: HTMLDivElement\n    }\n\n    $confirm\n\n    $t\n\n    $i18nTags\n\n    @State('contextMap') contextMap\n    @State('statusMap') statusMap\n    @State('cardMap') cardMap\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n    @State('appStatus') appStatus\n\n    @Getter('getAccountAtName') getAccountAtName\n    @Getter('isOAuthUser') isOAuthUser\n\n    @Mutation('updateMuteStatusList') updateMuteStatusList\n    @Mutation('updateMuteUserList') updateMuteUserList\n\n    currentReplyToStatus: mastodonentities.Status = null\n\n    shouldShowContentWhileSpoilerExists_ = null\n\n    shouldShowFullReplyActionArea: boolean = false\n\n    replyInputValue: string = ''\n\n    replySpoilerText: string = ''\n\n    isCardLoading = false\n\n    isFileDragOver = false\n\n    youtubeVideoIFrameHeight = 0\n\n    droppedFiles: Array<File> = null\n\n    @Prop() status: mastodonentities.Status\n\n    @Prop() shouldCollapseContent: boolean\n\n    mounted () {\n      this.youtubeVideoIFrameHeight = this.$refs.replyListContainer.clientWidth * 315 / 560\n    }\n\n    get mainStatusCardInfo (): mastodonentities.Card {\n      return this.cardMap[this.status.id]\n    }\n\n    get contentLinkList () {\n      return [...$(this.status.content).find('a')].map(a => {\n        return a.getAttribute('href')\n      })\n    }\n\n    get neteaseMusicLink () {\n      return this.contentLinkList.map(link => {\n        return getNetEaseMusicFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    get youtubeVideoLink () {\n      return this.contentLinkList.map(link => {\n        return getYoutubeVideoFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    get reblogContentLinkList () {\n      return this.status.reblog ? [...$(this.status.reblog.content).find('a')].map(a => {\n        return a.getAttribute('href')\n      }) : []\n    }\n\n    get reblogNeteaseMusicLink () {\n      return this.reblogContentLinkList.map(link => {\n        return getNetEaseMusicFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    get reblogYoutubeVideoLink () {\n      return this.reblogContentLinkList.map(link => {\n        return getYoutubeVideoFrameLinkFromContentLink(link)\n      }).filter(l => l)[0]\n    }\n\n    get hasLinkCardInfo () {\n      return this.mainStatusCardInfo &&\n        (Object.keys(this.mainStatusCardInfo).length !== 0)\n        && this.mainStatusCardInfo.type === StatusCardTypes.LINK\n    }\n\n    get reblogHasLinkCardInfo () {\n      return this.status.reblog &&\n        this.cardMap[this.status.reblog.id] &&\n        (Object.keys(this.cardMap[this.status.reblog.id]).length !== 0) &&\n        this.cardMap[this.status.reblog.id].type === StatusCardTypes.LINK\n    }\n\n    get shouldShowContentWhileSpoilerExists () {\n      if (typeof this.shouldShowContentWhileSpoilerExists_ === 'boolean') {\n        return this.shouldShowContentWhileSpoilerExists_\n      }\n\n      return this.appStatus.settings.autoExpandSpoilerTextMode\n    }\n\n    set shouldShowContentWhileSpoilerExists (val) {\n      this.shouldShowContentWhileSpoilerExists_ = val\n    }\n\n    get descendantStatusList (): Array<mastodonentities.Status> {\n      if (!this.contextMap[this.status.id] || !this.contextMap[this.status.id].descendants) return []\n\n      return this.contextMap[this.status.id].descendants.map(descendantStatusId => {\n        return this.statusMap[descendantStatusId]\n      }).filter(s => s).sort((a, b) => {\n        return new Date(a.created_at) >= new Date(b.created_at) ? 1 : -1\n      }).filter(status => {\n        const muteByStatus = this.appStatus.settings.muteMap.statusList.indexOf(status.id) !== -1\n        const muteByUser = this.appStatus.settings.muteMap.userList.indexOf(status.account.id) !== -1\n        return !muteByStatus && !muteByUser\n      })\n    }\n\n    get mainStatusContentStyle () {\n      return this.shouldCollapseContent ? {\n        'max-height': '500px',\n        'overflow': 'auto'\n      } : null\n    }\n\n    @Watch('shouldShowFullReplyActionArea')\n    onFullReplyActionAreaDisplayToggled (val) {\n      if (val) this.$emit('statusCardFocus')\n    }\n\n    hideFullReplyActionArea () {\n      this.shouldShowFullReplyActionArea = false\n      this.currentReplyToStatus = null\n      this.replyInputValue = ''\n      this.replySpoilerText = ''\n      this.droppedFiles = []\n    }\n\n    onCheckSharedOriginalPost () {\n      this.$router.push({\n        name: this.$routersInfo.statuses.name,\n        params: {\n          statusId: this.status.reblog.id\n        }\n      })\n    }\n\n    onReplyToStatus (status: mastodonentities.Status) {\n      this.currentReplyToStatus = status\n\n      let preSetMentions\n\n      if (this.appStatus.settings.onlyMentionTargetUserMode) {\n        preSetMentions = [{ acct: status.account.acct }]\n      } else {\n        preSetMentions = status.mentions.filter(mention => {\n          return (mention.id !== this.currentUserAccount.id) && (mention.id !== status.account.id)\n        })\n\n        if (status.account.id !== this.currentUserAccount.id || preSetMentions.length === 0) {\n          preSetMentions.unshift({\n            acct: status.account.acct,\n            id: status.account.id\n          } as mastodonentities.Mention)\n        }\n      }\n\n      this.replyInputValue = preSetMentions.reduce((pre, cur) => pre + `@${cur.acct} `, '')\n\n      this.shouldShowFullReplyActionArea = true\n    }\n\n    onReplySuccess () {\n      this.$nextTick(() => {\n        this.$refs.replyListContainer.scrollTo({ top: this.$refs.replyListContainer.scrollHeight, left: 0, behavior: 'smooth' })\n      })\n    }\n\n    onDragFileOver (e: DragEvent) {\n      e.preventDefault()\n\n      this.isFileDragOver = true\n    }\n\n    onDropFile (e: DragEvent) {\n      e.preventDefault()\n\n      this.isFileDragOver = false\n\n      if (!this.shouldShowFullReplyActionArea) {\n        this.onReplyToStatus(this.status)\n      }\n\n      this.droppedFiles = Array.from(e.dataTransfer.files)\n    }\n\n    async onMuteStatus (statusId: string) {\n      const doMuteStatus = (await this.$confirm(this.$t(this.$i18nTags.statusCard.mute_status_confirm), {\n        okLabel: this.$t(this.$i18nTags.statusCard.do_mute_status_btn),\n        cancelLabel: this.$t(this.$i18nTags.statusCard.cancel_mute_status_btn),\n      })).result\n      if (doMuteStatus) {\n        this.updateMuteStatusList(statusId)\n      }\n    }\n\n    async onMuteUser (userId: string) {\n      const doMuteUser = (await this.$confirm(this.$t(this.$i18nTags.statusCard.mute_user_confirm), {\n        okLabel: this.$t(this.$i18nTags.statusCard.do_mute_user_btn),\n        cancelLabel: this.$t(this.$i18nTags.statusCard.cancel_mute_user_btn),\n      })).result\n      if (doMuteUser) {\n        this.updateMuteUserList(userId)\n      }\n    }\n  }\n\n  export default StatusCard\n</script>\n\n<style lang=\"less\" scoped>\n  .status-card-container {\n    width: 100%;\n\n    .status-card {\n      height: 100%;\n      display: flex;\n      flex-direction: column;\n      transition: box-shadow 0.3s ease-in-out;\n    }\n  }\n\n  .at-name {\n    font-weight: 400;\n    font-size: 13px;\n  }\n\n  .spoiler-text-area {\n    padding: 0 16px 16px;\n  }\n\n  .main-status-content {\n    padding: 0 16px 16px;\n  }\n\n  .main-link-preview-area {\n    padding: 0 16px 16px 16px;\n    .link-preview-divider {\n      margin-bottom: 16px;\n    }\n  }\n\n  .main-attachment-area {\n    .attachment-list {\n      > img {\n        width: 100%;\n        height: auto;\n      }\n    }\n  }\n\n  .reblog-area {\n    .reblog-plain-info-area {\n      margin: 16px;\n\n      .reblog-source-link {\n        cursor: pointer;\n        font-weight: 500;\n\n        .at-name {\n          color: unset;\n        }\n      }\n\n      .reblog-status-content {\n        padding: 0;\n        margin-top: 8px;\n      }\n    }\n  }\n\n  .reply-area-full {\n\n    .full-reply-list {\n      max-height: 400px;\n      overflow-y: auto;\n      -webkit-overflow-scrolling: touch;\n    }\n\n    .full-reply-status-content {\n      padding: 0;\n    }\n  }\n\n  .current-reply-to-info-area {\n    height: 44px;\n    line-height: 44px;\n    padding-left: 16px;\n\n    .reply-to-account-info {\n      margin-top: 6px;\n    }\n  }\n\n  .card-action-area {\n    padding: 0;\n  }\n</style>\n\n<style lang=\"less\">\n\n  .status-content {\n    // https://stackoverflow.com/questions/5241369/word-wrap-a-link-so-it-doesnt-overflow-its-parent-div-width\n    word-wrap: break-word;\n    white-space: pre-wrap;\n\n    > p {\n      margin: 0 0 10px 0;\n      padding: 0;\n    }\n\n    > P:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  .simple-reply-status-content {\n    > p { display: inline }\n  }\n\n  .reply-text-input {\n    .el-textarea__inner {\n      width: 100%;\n      outline: none;\n      border: none;\n      padding: 0;\n      resize: none;\n    }\n  }\n\n  .no-limit-reply-area-height.status-card-container {\n    .full-reply-list {\n      max-height: unset;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/ThemeEditPanel.vue",
    "content": "<template>\n  <div class=\"theme-edit-panel-container\">\n    <mu-dialog :open.sync=\"isDialogOpening\" overlay-color=\"rgba(0,0,0,0.12)\"\n               dialog-class=\"theme-edit-dialog default-theme-bg-color\" :width=\"dialogWidth\"\n               :overlay-close=\"false\"\n               :overlay-opacity=\"1\" :transition=\"transition\" :fullscreen=\"shouldDialogFullScreen\">\n\n      <mu-appbar color=\"secondary\">\n        <mu-button slot=\"right\" icon @click=\"onMinimizePanel\">\n          <mu-icon value=\"exit_to_app\" />\n        </mu-button>\n      </mu-appbar>\n\n      <div class=\"color-select-area\">\n        <mu-card class=\"color-select-card\" v-for=\"(colorInfo, index) in colorPickPanelOrder\" :key=\"index\">\n          <div class=\"color-info\">\n            <span class=\"color-name primary-read-text-color\">{{colorInfo.label}}</span>\n            <span class=\"color-value secondary-read-text-color\">{{colorInfo.value}}</span>\n          </div>\n          <div class=\"color-cake\" ref=\"colorCakes\" :style=\"{ backgroundColor: colorInfo.value }\"\n               @click=\"onColorCakeClick(colorInfo, index)\">\n            <mu-ripple />\n          </div>\n        </mu-card>\n      </div>\n\n      <mu-button slot=\"actions\" flat color=\"secondary\" @click=\"onTryToCancelEdit\">Cancel</mu-button>\n      <mu-button slot=\"actions\" flat class=\"secondary-theme-text-color\" @click=\"onTryToSaveTheme\" :disabled=\"!hasColorChanged\">Save</mu-button>\n\n    </mu-dialog>\n\n    <mu-button fab class=\"minimize-theme-edit-button\" color=\"secondary\" v-show=\"!isDialogOpening\"\n               @click=\"onMaximisePanel\">\n      <mu-icon value=\"color_lens\" />\n    </mu-button>\n\n    <mu-popover cover :trigger=\"triggerPopOverElem\"\n                :open.sync=\"shouldOpenColorPickerPopOver\">\n      <mu-tabs :value.sync=\"activeTabIndex\">\n        <mu-tab>Simple</mu-tab>\n        <mu-tab>Advanced</mu-tab>\n      </mu-tabs>\n      <div class=\"color-pickers-container\">\n        <swatches-picker v-show=\"activeTabIndex === 0\" :value=\"currentEditColorInfo.value\" @input=\"onColorPickerInput\"/>\n        <chrome-picker v-show=\"activeTabIndex === 1\" :value=\"currentEditColorInfo.value\" @input=\"onColorPickerInput\"/>\n      </div>\n    </mu-popover>\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Getter, Mutation } from 'vuex-class'\n  import { UiWidthCheckConstants } from '@/constant'\n  import ThemeManager from '@/themes'\n  import { Chrome, Compact, Swatches } from 'vue-color'\n  import * as _ from 'underscore'\n\n  const themeColorNameToDataNameMap = {\n    primaryColor: 'primaryColor',\n    secondaryColor: 'secondaryColor',\n    trackColor: 'placeholderTextColor',\n    textColor: 'primaryTextColor',\n    secondaryTextColor: 'secondaryTextColor',\n    disabledColor: 'disabledColor',\n    backgroundColor: 'primaryBGColor',\n    dialogBackgroundColor: 'secondaryBGColor'\n  }\n\n  const colorPickPanelNameOrder = [\n    'primaryColor', 'secondaryColor',\n    'primaryTextColor', 'secondaryTextColor',\n    'placeholderTextColor', 'disabledColor',\n    'primaryBGColor', 'secondaryBGColor'\n  ]\n\n  @Component({\n    components: {\n      'swatches-picker': Swatches,\n      'chrome-picker': Chrome\n    }\n  })\n  class ThemeEditPanel extends Vue {\n\n    $refs: {\n      colorCakes: any\n    }\n\n    $confirm\n\n    $prompt\n\n    @State('appStatus') appStatus\n\n    @Getter('shouldDialogFullScreen') shouldDialogFullScreen\n\n    @Mutation('updateIsEditingThemeMode') updateIsEditingThemeMode\n    @Mutation('updateShouldShowThemeEditPanel') updateShouldShowThemeEditPanel\n    @Mutation('updateTheme') updateTheme\n\n    currentEditColorInfo = {\n      label: '',\n      value: ''\n    }\n\n    activeTabIndex: number = 0\n\n    shouldOpenColorPickerPopOver: boolean = false\n\n    primaryColor: string = ''\n\n    secondaryColor: string = ''\n\n    primaryTextColor: string = ''\n\n    secondaryTextColor: string = ''\n\n    placeholderTextColor: string = ''\n\n    disabledColor: string = ''\n\n    primaryBGColor: string = ''\n\n    secondaryBGColor: string = ''\n\n    triggerPopOverElem: any = null\n\n    onSomeColorChangedListener = () => {}\n\n    hasColorChanged: boolean = false\n\n    mounted () {\n      const currentThemeInfo = ThemeManager.getThemeInfoByThemeName(this.appStatus.settings.theme)\n\n      this.initColorList(currentThemeInfo.theme.colorSet)\n\n      this.onSomeColorChangedListener = _.throttle(() => {\n        const currentColorSet = this.getCurrentColorSet()\n\n        ThemeManager.setTempThemeByColorSet(currentColorSet)\n      }, 20)\n    }\n\n    get colorPickPanelOrder () {\n      return colorPickPanelNameOrder.map(colorName => {\n        return { label: colorName, value: this[colorName] }\n      })\n    }\n\n    get isDialogOpening () {\n      return this.appStatus.shouldShowThemeEditPanel\n    }\n\n    set isDialogOpening (show) {\n      this.updateShouldShowThemeEditPanel(show)\n    }\n\n    get transition () {\n      return this.shouldDialogFullScreen ? 'slide-bottom' : 'slide-top'\n    }\n\n    get dialogWidth () {\n      return this.shouldDialogFullScreen ? null : UiWidthCheckConstants.POST_STATUS_DIALOG_TOGGLE_WIDTH\n    }\n\n    @Watch('colorPickPanelOrder')\n    onSomeColorChanged () {\n      this.onSomeColorChangedListener()\n    }\n\n    getCurrentColorSet () {\n      const colorSet = {}\n\n      Object.keys(themeColorNameToDataNameMap).forEach(colorName => {\n        colorSet[`@${colorName}`] = this[themeColorNameToDataNameMap[colorName]]\n      })\n\n      return colorSet\n    }\n\n    initColorList (colorSet) {\n      Object.keys(themeColorNameToDataNameMap).forEach(colorName => {\n        this[themeColorNameToDataNameMap[colorName]] = colorSet[`@${colorName}`]\n      })\n    }\n\n    async onTryToCancelEdit () {\n      if (this.hasColorChanged) {\n        const doClosePanel = (await this.$confirm('Exit without save your editing?', {\n          okLabel: 'Yes',\n          cancelLabel: 'No',\n        })).result\n\n        if (doClosePanel) {\n          ThemeManager.setTheme(this.appStatus.settings.theme)\n          this.updateIsEditingThemeMode(false)\n        }\n      } else {\n        this.updateIsEditingThemeMode(false)\n      }\n    }\n\n    async onTryToSaveTheme () {\n      this.$prompt('Please naming your new theme', 'Theme Name', {\n        validator (value) {\n          return {\n            valid: !ThemeManager.themeInfo[value],\n            message: 'theme name conflict'\n          }\n        }\n      }).then(({ result, value }) => {\n        if (result) {\n          ThemeManager.importTheme(this.getCurrentColorSet(), value)\n          this.updateTheme(value)\n          ThemeManager.setTheme(value)\n          this.updateIsEditingThemeMode(false)\n        }\n      })\n    }\n\n    onMinimizePanel () {\n      this.isDialogOpening = false\n    }\n\n    onMaximisePanel () {\n      this.isDialogOpening = true\n    }\n\n    onColorCakeClick (colorInfo, index) {\n      this.triggerPopOverElem = this.$refs.colorCakes[index]\n      this.currentEditColorInfo = colorInfo\n      this.shouldOpenColorPickerPopOver = true\n    }\n\n    onColorPickerInput (val) {\n      this.hasColorChanged = true\n\n      const newColorRGBA = `rgba(${val.rgba.r}, ${val.rgba.g}, ${val.rgba.b}, ${val.rgba.a})`\n\n      this.currentEditColorInfo.value = newColorRGBA\n      this[this.currentEditColorInfo.label] = newColorRGBA\n    }\n\n  }\n\n  export default ThemeEditPanel\n</script>\n\n<style lang=\"less\" scoped>\n  .theme-edit-panel-container {\n\n    .minimize-theme-edit-button {\n      position: fixed;\n      right: 16px;\n      bottom: 16px;\n\n      @media (min-width: 768px) {\n        right: 32px;\n        bottom: 32px;\n      }\n    }\n  }\n\n  .theme-edit-dialog {\n    .color-select-area {\n      padding: 16px;\n      display: flex;\n      flex-wrap: wrap;\n      justify-content: space-between;\n\n      .color-select-card {\n        user-select: none;\n        min-width: 240px;\n        display: flex;\n        height: 60px;\n        padding: 14px;\n        align-items: center;\n        justify-content: space-between;\n        margin-bottom: 10px;\n\n        .color-info {\n          height: 36px;\n          display: flex;\n          flex-direction: column;\n          justify-content: center;\n\n          .color-name {\n            font-size: 16px;\n            height: 16px;\n            line-height: 16px;\n          }\n\n          .color-value {\n            font-size: 10px;\n            height: 20px;\n            line-height: 14px;\n            padding-top: 6px;\n\n            text-transform: uppercase;\n            letter-spacing: 1.5px;\n          }\n        }\n\n        .color-cake {\n          height: 36px;\n          width: 36px;\n          box-sizing: border-box;\n          border-radius: 100%;\n          cursor: pointer;\n          border: 1px solid #dadada;\n          box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.14);\n          position: relative;\n        }\n      }\n    }\n  }\n\n  .color-pickers-container {\n    display: flex;\n    justify-content: center;\n  }\n</style>\n\n<style lang=\"less\">\n  .theme-edit-dialog {\n    border-radius: 4px;\n\n    .mu-dialog-body {\n      padding: 0;\n      height: auto !important;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/components/VisibilitySelectPopOver.vue",
    "content": "<template>\n  <mu-popover cover :open.sync=\"shouldOpen\"\n              :trigger=\"trigger\">\n    <mu-list textline=\"two-line\">\n      <mu-list-item button v-for=\"(visibilityType, index) in VisibilityTypeList\"\n                    :class=\"{ 'selected-item': visibilityType === visibility }\"\n                    :key=\"index\" @click.stop=\"onChangeVisibility(visibilityType)\">\n        <mu-list-item-action>\n          <mu-icon :value=\"getVisibilityDescInfo(visibilityType).icon\"></mu-icon>\n        </mu-list-item-action>\n        <mu-list-item-content>\n          <mu-list-item-title class=\"primary-read-text-color\">{{$t(visibilityType)}}</mu-list-item-title>\n          <mu-list-item-sub-title class=\"secondary-read-text-color\">{{$t(getVisibilityDescInfo(visibilityType).descTag)}}</mu-list-item-sub-title>\n        </mu-list-item-content>\n      </mu-list-item>\n    </mu-list>\n  </mu-popover>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Prop } from 'vue-property-decorator'\n  import { VisibilityTypes } from '@/constant'\n  import { getVisibilityDescInfo } from '@/util'\n\n  @Component({})\n  class VisibilitySelectPopOver extends Vue {\n\n    @Prop() visibility: string\n\n    @Prop() open: boolean\n\n    @Prop() trigger: HTMLElement\n\n    getVisibilityDescInfo = getVisibilityDescInfo\n\n    VisibilityTypeList = [\n      VisibilityTypes.PUBLIC, VisibilityTypes.PRIVATE,\n      VisibilityTypes.UNLISTED, VisibilityTypes.DIRECT\n    ]\n\n    get shouldOpen () {\n      return this.open\n    }\n\n    set shouldOpen (open) {\n      this.$emit('update:open', open)\n    }\n\n    onChangeVisibility (newVisibility: string) {\n      this.shouldOpen = false\n      this.$emit('update:visibility', newVisibility)\n    }\n  }\n\n  export default VisibilitySelectPopOver\n</script>\n\n<style lang=\"less\" scoped>\n\n</style>\n"
  },
  {
    "path": "src/constant/i18n.ts",
    "content": "export const I18nLocales = {\n  EN: 'en',\n  JA: 'ja',\n  DE: 'de',\n  ZH_CN: 'zh-cn',\n  ZH_HK: 'zh-hk',\n  ZH_TW: 'zh-tw'\n}\n\nexport const I18nTags = {\n  common: {\n    status_visibility_public: 'public',\n    status_visibility_private: 'private',\n    status_visibility_unlisted: 'unlisted',\n    status_visibility_direct: 'direct',\n    status_visibility_public_desc: 'public_desc',\n    status_visibility_private_desc: 'private_desc',\n    status_visibility_unlisted_desc: 'unlisted_desc',\n    status_visibility_direct_desc: 'direct_desc',\n    drag_and_drop_to_upload: 'drag_and_drop_to_upload',\n    write_your_warning_here: 'write_your_warning_here'\n  },\n\n  statusCard: {\n    post_new_status_placeholder: 'post_new_status_placeholder',\n    reply_to_replier: 'status_card_reply_to_replier',\n    reply_to_main_status: 'status_card_reply_to_main_status',\n    cancel_post: 'status_card_cancel_post',\n    submit_post: 'status_card_submit_post',\n    show_content: 'status_card_show_content',\n    hide_content: 'status_card_hide_content',\n    mute_status: 'status_card_mute_status',\n    mute_user: 'status_card_mute_user',\n    delete_status: 'status_card_delete_status',\n    delete_status_confirm: 'status_card_delete_status_confirm',\n    do_delete_status_btn: 'status_card_do_delete_status_btn',\n    cancel_delete_status_btn: 'status_card_cancel_delete_status_btn',\n    originally_shared_by: 'status_card_originally_shared_by',\n    sensitive_media_alert: 'status_card_sensitive_media_alert',\n    change_visibility: 'status_card_change_visibility',\n    add_photos: 'status_card_add_photos',\n    mute_status_confirm: 'status_card_mute_status_confirm',\n    do_mute_status_btn: 'status_card_do_mute_status_btn',\n    cancel_mute_status_btn: 'status_card_cancel_mute_status_btn',\n    mute_user_confirm: 'status_card_mute_user_confirm',\n    do_mute_user_btn: 'status_card_do_mute_user_btn',\n    cancel_mute_user_btn: 'status_card_cancel_mute_user_btn',\n  },\n\n  timeLines: {\n    no_load_more_status_notice: 'timeLines_no_load_more_status_notice',\n    new_message_notice: 'timeLines_new_message_notice',\n    whats_new_with_you: 'timeLines_whats_new_with_you'\n  },\n\n  drawer: {\n    home: 'drawer_home',\n    public: 'drawer_public',\n    tag: 'drawer_tag',\n    local: 'drawer_local',\n    profile: 'drawer_profile',\n    settings: 'drawer_settings',\n    logout: 'drawer_logout',\n    do_logout_message_confirm: 'drawer_do_logout_message_confirm',\n    do_logout_message_yes: 'drawer_do_logout_message_yes',\n    do_logout_message_no: 'drawer_do_logout_message_no',\n    toHostInstance: 'drawer_to_host_instance',\n    search_input_placeholder: 'drawer_search_input_placeholder',\n    search_result_people_label: 'drawer_search_result_people_label',\n    search_result_hashtag_label: 'drawer_search_result_hashtag_label'\n  },\n\n  settings: {\n    general_label: 'settings_general_label',\n    choose_theme: 'settings_choose_theme',\n    export_theme_color_set: 'settings_export_theme_color_set',\n    import_theme_color_set: 'settings_import_theme_color_set',\n    edit_theme_color_set: 'settings_edit_theme_color_set',\n    delete_theme_color_set: 'settings_delete_theme_color_set',\n\n    choose_language: 'settings_choose_language',\n    use_multi_line_mode: 'settings_use_multi_line_mode',\n    maximum_number_of_columns_in_multi_line_mode: 'settings_maximum_number_of_columns_in_multi_line_mode',\n    show_sensitive_media_files: 'settings_show_sensitive_media_files',\n    auto_expand_spoiler_text: 'settings_auto_expand_spoiler_text',\n    auto_load_new_status: 'settings_auto_load_new_status',\n    post_privacy: 'settings_post_privacy',\n    post_media_as_sensitive: 'settings_post_media_as_sensitive',\n    only_mention_target_user: 'settings_only_mention_target_user',\n\n    stream_label: 'settings_stream_label',\n    media_label: 'settings_media_label',\n    publishing_label: 'settings_publishing_label',\n    personality_label: 'settings_personality_label',\n    web_label: 'settings_web_label',\n\n    changes_successfully_saved: 'settings_changes_successfully_saved'\n  },\n\n  header: {\n\n  },\n\n  home: {\n\n  },\n\n  oauth: {\n    form_brand: 'oauth_form_brand',\n    login_hint: 'oauth_login_hint',\n    server_input_label: 'oauth_server_input_label',\n    please_input_server_url: 'oauth_please_input_server_url',\n    please_input_correct_server_url: 'oauth_please_input_correct_server_url',\n    register_app_error_message: 'oauth_register_app_error_message',\n    confirm_input: 'oauth_confirm_input'\n  },\n\n  postStatusDialog: {\n    do_discard_message_confirm: 'post_status_dialog_do_discard_message_confirm',\n    do_keep_message: 'post_status_dialog_do_keep_message',\n    do_discard_message: 'post_status_dialog_do_discard_message',\n    text_character_limit_exceed: 'post_status_dialog_text_character_limit_exceed'\n  },\n\n  notifications: {\n    someone_followed_you: 'notifications_someone_followed_you',\n    mentioned_you: 'notifications_mentioned_you',\n    boosted_your_status: 'notifications_boosted_your_status',\n    favourited_your_status: 'notifications_favourited_your_status'\n  }\n}\n"
  },
  {
    "path": "src/constant/index.ts",
    "content": "export { RoutersInfo } from './routers'\nexport { I18nTags, I18nLocales } from './i18n'\n\nconst AttachmentTypes = {\n  IMAGE: 'image',\n  VIDEO: 'video',\n  GIFV: 'gifv',\n  UNKNOWN: 'unknown'\n}\n\nconst TimeLineTypes = {\n  PUBLIC: 'public',\n  HOME: 'home',\n  DIRECT: 'direct',\n  TAG: 'tag',\n  LIST: 'list',\n  LOCAL: 'local'\n}\n\nconst VisibilityTypes = {\n  DIRECT: 'direct',\n  PRIVATE: 'private',\n  UNLISTED: 'unlisted',\n  PUBLIC: 'public'\n}\n\nconst UiWidthCheckConstants = {\n  DRAWER_DOCKING_BOUNDARY: 960,\n  POST_STATUS_DIALOG_TOGGLE_WIDTH: 530,\n  NOTIFICATION_DIALOG_TOGGLE_WIDTH: 530,\n  DRAWER_DESKTOP_WIDTH: 290,\n  DRAWER_MOBILE_WIDTH: 300,\n\n  STATUS_CARD_MAX_WIDTH: 530,\n  STATUS_CARD_MIN_WIDTH: 356,\n  TIMELINE_WATER_FALL_GUTTER: 24\n}\n\nconst ThemeNames = {\n  GOOGLE_PLUS: 'Google Plus',\n  DARK: 'Dark',\n  GREEN_LIGHT: 'Green Light',\n  CUCKOO_HUB: 'Cuckoo Hub'\n}\n\nconst NotificationTypes = {\n  MENTION: 'mention',\n  REBLOG: 'reblog',\n  FAVOURITE: 'favourite',\n  FOLLOW: 'follow'\n}\n\nconst StreamingEventTypes = {\n  UPDATE: 'update',\n  NOTIFICATION: 'notification',\n  DELETE: 'delete',\n  FILTERS_CHANGED: 'filters_changed'\n}\n\nconst TITLE = 'Cuckoo+'\n\nconst StatusCardTypes = {\n  LINK: 'link',\n  PHOTO: 'photo'\n}\n\nexport {\n  AttachmentTypes,\n  TimeLineTypes,\n  VisibilityTypes,\n  UiWidthCheckConstants,\n  ThemeNames,\n  NotificationTypes,\n  StreamingEventTypes,\n  TITLE,\n  StatusCardTypes\n}\n"
  },
  {
    "path": "src/constant/routers.ts",
    "content": "export const RoutersInfo = {\n  empty: {\n    path: '/',\n    name: 'empty'\n  },\n\n  timelines: {\n    path: '/timelines',\n    name: 'timelines'\n  },\n\n  defaulttimelines: {\n    path: ':timeLineType',\n    name: 'defaulttimelines'\n  },\n\n  tagtimelines: {\n    path: 'tag/:tagName',\n    name: 'tagtimelines'\n  },\n\n  listtimelines: {\n    path: 'list/:listName',\n    name: 'listtimelines'\n  },\n\n  statuses: {\n    path: '/statuses/:statusId',\n    name: 'statuses'\n  },\n\n  home: {\n    path: '/home',\n    name: 'home'\n  },\n\n  oauth: {\n    path: '/oauth',\n    name: 'oauth'\n  },\n\n  settings: {\n    path: '/settings',\n    name: 'settings'\n  },\n\n  accounts: {\n    path: '/accounts/:accountId',\n    name: 'accounts'\n  }\n};\n"
  },
  {
    "path": "src/directives.ts",
    "content": "import Vue from 'vue'\nimport * as Masonry from 'masonry-layout'\nimport ResizeObserver from 'resize-observer-polyfill'\nimport * as _ from 'underscore'\nimport { UiWidthCheckConstants } from '@/constant'\n\n{\n\n  const createDragOverLayer = (vNode) => {\n    const layer = document.createElement('div')\n    layer.className = 'mu-loading-wrap drag-over-layer'\n\n    const component = vNode.componentInstance\n    layer.innerText = component.$t(component.$i18nTags.common.drag_and_drop_to_upload)\n\n    layer.addEventListener('dragover', e => e.preventDefault())\n    layer.addEventListener('dragleave', e => {\n      e.preventDefault()\n      component.$emit('cuckooDragleave', e)\n    })\n    layer.addEventListener('drop', e => {\n      e.preventDefault()\n      component.$emit('cuckooDrop', e)\n    })\n\n    return layer\n  }\n\n  Vue.directive('drag-over', {\n    update (el: HTMLDivElement, binding, vNode) {\n      if (binding.value === binding.oldValue) return\n\n      if (binding.value === false) {\n        return el.removeChild(el.querySelector('.drag-over-layer'))\n      }\n\n      // todo use singleton, but there is some little bug\n      el.appendChild(createDragOverLayer(vNode))\n    }\n  } as any)\n\n}\n\n{\n  interface MasonryItem {\n    element: HTMLDivElement\n    position: { x: number, y: number }\n  }\n\n  interface MasonryContainer extends HTMLDivElement {\n    $masonryEl: {\n      size: { width: number, height: number }\n      layout()\n      addItems(el)\n      reloadItems()\n      layoutItems(masonryItemList: Array<MasonryItem>)\n      items: Array<MasonryItem>\n    }\n  }\n\n  const reLayoutMasonry = _.throttle(($masonryEl) => {\n    $masonryEl.layout()\n\n    $masonryEl.items.forEach((item: MasonryItem) => {\n      item.element.style.animation = 'fadein 1s';\n      item.element.style.opacity = '1'\n    })}, 200)\n\n  Vue.directive('masonry-container', {\n\n    inserted (el: MasonryContainer) {\n\n      el.$masonryEl = new Masonry(el, {\n        itemSelector: '.status-card-container',\n        transitionDuration: 0,\n        gutter: UiWidthCheckConstants.TIMELINE_WATER_FALL_GUTTER,\n        initLayout: false\n      })\n\n    },\n\n    update: (el: MasonryContainer) => {\n      if (!el.parentElement || (el.parentElement.style.display === 'none')) return\n\n      const $masonryEl = el.$masonryEl\n\n      setTimeout(() => {\n        const oldItemLength = $masonryEl.items.length\n        $masonryEl.reloadItems()\n\n        const hasItemsLengthChanged = oldItemLength !== $masonryEl.items.length\n        const IsSomeItemHided = $masonryEl.items.some(item => item.element.style.opacity === '0')\n\n        if (!hasItemsLengthChanged && !IsSomeItemHided) return\n\n        reLayoutMasonry($masonryEl)\n      })\n    }\n\n  } as any)\n\n  const onMasonryItemSizeChanged = _.throttle(($masonryEl) => {\n    $masonryEl.layout()\n  }, 200)\n\n  const ro = new ResizeObserver((entries) => {\n    const targetMasonryContainer = entries[0].target.parentNode as MasonryContainer\n\n    if (!targetMasonryContainer || !targetMasonryContainer.$masonryEl) return\n\n    // todo optimize\n    return onMasonryItemSizeChanged(targetMasonryContainer.$masonryEl)\n  })\n\n  Vue.directive('masonry-item', {\n\n    inserted (el: HTMLDivElement) {\n      el.style.opacity = '0'\n\n      ro.observe(el)\n    }\n\n  } as any)\n}\n"
  },
  {
    "path": "src/formatter.spec.ts",
    "content": "import 'mocha'\nimport { expect } from 'chai'\nimport Formatter from './formatter'\n\nconst formatter = new Formatter()\n\ndescribe('formatter.format', () => {\n    it('ignores strings with no dashes', () => {\n        expect(formatter.format(\"aaaaaa\")).to.equal(\"aaaaaa\")\n    })\n    it('ignores strings with a single dash', () => {\n        expect(formatter.format(\"-aaaaaa\")).to.equal(\"-aaaaaa\")\n        expect(formatter.format(\"aaaaaa-\")).to.equal(\"aaaaaa-\")\n        expect(formatter.format(\"aaa-aaa\")).to.equal(\"aaa-aaa\")\n        expect(formatter.format(\"aaa -aaa\")).to.equal(\"aaa -aaa\")\n        expect(formatter.format(\"aaa- aaa\")).to.equal(\"aaa- aaa\")\n        expect(formatter.format(\"aaa - aaa\")).to.equal(\"aaa - aaa\")\n    })\n    it('format strings with a pair of correct dashes', () => {\n        expect(formatter.format(\"-aaaaaa-\")).to.equal(\"<del>aaaaaa</del>\")\n        expect(formatter.format(\"-aaa- aaa\")).to.equal(\"<del>aaa</del> aaa\")\n        expect(formatter.format(\"aaa -aaa-\")).to.equal(\"aaa <del>aaa</del>\")\n        expect(formatter.format(\"aa -aa- aa\")).to.equal(\"aa <del>aa</del> aa\")\n    })\n    it('ignore strings with a pair of wrong dashes', () => {\n        expect(formatter.format(\"-aaa-aaa\")).to.equal(\"-aaa-aaa\")\n        expect(formatter.format(\"aaa-aaa-\")).to.equal(\"aaa-aaa-\")\n        expect(formatter.format(\"aa-aa-aa\")).to.equal(\"aa-aa-aa\")\n    })\n    it('format string with a pair of correct dashes and a wrong dash', () => {\n        expect(formatter.format(\"-aaa-aaa-\")).to.equal(\"<del>aaa-aaa</del>\")\n        expect(formatter.format(\"-a-aa- aaa\")).to.equal(\"<del>a-aa</del> aaa\")\n        expect(formatter.format(\"-aaa- a-aa\")).to.equal(\"<del>aaa</del> a-aa\")\n        expect(formatter.format(\"aaa -a-aa-\")).to.equal(\"aaa <del>a-aa</del>\")\n        expect(formatter.format(\"a-aa -aaa-\")).to.equal(\"a-aa <del>aaa</del>\")\n        expect(formatter.format(\"aa -a-a- aa\")).to.equal(\"aa <del>a-a</del> aa\")\n        expect(formatter.format(\"a-a -aa- aa\")).to.equal(\"a-a <del>aa</del> aa\")\n    })\n    it('format string with a pair of correct dashes and a correct dash', () => {\n        expect(formatter.format(\"-a -aa- aaa\")).to.equal(\"<del>a -aa</del> aaa\")\n        expect(formatter.format(\"-aaa- a -aa\")).to.equal(\"<del>aaa</del> a -aa\")\n        expect(formatter.format(\"aaa -a -aa-\")).to.equal(\"aaa <del>a -aa</del>\")\n        expect(formatter.format(\"a- aa -aaa-\")).to.equal(\"a- aa <del>aaa</del>\")\n        expect(formatter.format(\"aa -a- a- aa\")).to.equal(\"aa <del>a- a</del> aa\")\n        expect(formatter.format(\"aa -a -a- aa\")).to.equal(\"aa <del>a -a</del> aa\")\n        expect(formatter.format(\"a- a -aa- aa\")).to.equal(\"a- a <del>aa</del> aa\")\n    })\n  // todo fix this\n    it('format string with two pairs of correct dashes', () => {\n        // expect(formatter.format(\"a -a- aa -a- a\")).to.equal(\"a <del>a</del> aa <del>a</del> a\")\n        // expect(formatter.format(\"-aa- aa -a- a\")).to.equal(\"<del>aa</del> aa <del>a</del> a\")\n        // expect(formatter.format(\"a -a- aa -aa-\")).to.equal(\"a <del>a</del> aa <del>aa</del>\")\n        // expect(formatter.format(\"-aa- aa -aa-\")).to.equal(\"<del>aa</del> aa <del>aa</del>\")\n    })\n    it('format string with two enclosing pairs of correct dashes', () => {\n        expect(formatter.format(\"a -a -aa- a- a\")).to.equal(\"a <del>a -aa- a</del> a\")\n    })\n})\n"
  },
  {
    "path": "src/formatter.ts",
    "content": "import { mastodonentities } from \"@/interface\"\n\nclass Formatter {\n\n  private customEmojiRegex = /:\\w+:/g\n\n  // todo fix test\n  private delRegex = /(^|\\s)-.*-($|\\s|\\.|,|\\?|!|~)/g\n\n  private boldRegex = /(^|\\s)\\*.*\\*($|\\s|\\.|,|\\?|!|~)/g\n\n  private italicRegex = /(^|\\s)_.*_($|\\s|\\.|,|\\?|!|~)/g\n\n  private customEmojiMap: {\n    [index: string]: mastodonentities.Emoji\n  } = {}\n\n  constructor (customEmojis: Array<mastodonentities.Emoji> = []) {\n    customEmojis.forEach(emoji => {\n      this.customEmojiMap[emoji.shortcode] = emoji\n    })\n  }\n\n  private insertSomething (regex: RegExp, fragment: string, tag: string) {\n    return (text: string) => {\n      return text.replace(regex, (matchString: string, p1, p2, index) => {\n        const trimString = matchString.trim()\n\n        const isFinalCharacterDel = trimString[trimString.length - 1] === `${fragment}`\n        const isMatchStringFinalPixel = (index + matchString.length) === text.length && isFinalCharacterDel\n        const centralSubString = trimString.substring(1, trimString.length - ( isFinalCharacterDel ? 1 : 2 ))\n\n        return `${matchString[0] === ' ' ? ' ' : ''}<${tag}>${centralSubString}</${tag}>${isMatchStringFinalPixel ? '' : matchString[matchString.length - 1]}`\n      })\n    }\n  }\n\n  public insertDels (text: string): string {\n    return this.insertSomething(this.delRegex, '-', 'del')(text)\n  }\n\n  public insertBolds (text: string): string {\n    return this.insertSomething(this.boldRegex, '*', 'strong')(text)\n  }\n\n  public insetItalic (text) {\n    return this.insertSomething(this.italicRegex, '_', 'i')(text)\n  }\n\n  private insertCustomEmojis (text: string): string {\n    return text.replace(this.customEmojiRegex, (matchString: string) => {\n      const emojiShortCode = matchString.trim().slice(1, -1)\n\n      const targetEmoji = this.customEmojiMap[emojiShortCode]\n\n      if (!targetEmoji) return matchString\n\n      return `<img class=\"custom-emoji\" src=\"${targetEmoji.static_url}\"/>`\n    })\n  }\n\n  public updateCustomEmojiMap (customEmojis: Array<mastodonentities.Emoji> = []) {\n    customEmojis.forEach(emoji => {\n      this.customEmojiMap[emoji.shortcode] = emoji\n    })\n  }\n\n  public format (text: string, customEmojis: Array<mastodonentities.Emoji> = []): string {\n    return [this.insertDels, this.insertBolds, this.insetItalic, this.insertCustomEmojis].reduce((preValue, process) => {\n      return process.bind(this)(preValue)\n    }, text)\n  }\n}\n\nexport default Formatter\n"
  },
  {
    "path": "src/i18n/compare",
    "content": "#!/bin/bash\n# Usage: ./compare en.ts ja.ts\ndiff <(cut -f 1 -d: \"$1\") <(cut -f 1 -d: \"$2\")\n\n"
  },
  {
    "path": "src/i18n/de.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: 'Cuckoo Plus',\n  [I18nTags.oauth.login_hint]: 'Anmelden',\n  [I18nTags.oauth.server_input_label]: 'Mastodon URL',\n  [I18nTags.oauth.please_input_server_url]: 'Bitte Mastodon URL eingeben',\n  [I18nTags.oauth.please_input_correct_server_url]: 'Bitte Mastodon URL überprüfen',\n  [I18nTags.oauth.register_app_error_message]: 'Oops! Bitte Mastodon URL nochmals überprüfen',\n  [I18nTags.oauth.confirm_input]: 'Bestätigen'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: 'Öffentlich',\n  [I18nTags.common.status_visibility_unlisted]: 'Privat',\n  [I18nTags.common.status_visibility_private]: 'Nur Folgende',\n  [I18nTags.common.status_visibility_direct]: 'Direktnachrichten',\n  [I18nTags.common.status_visibility_public_desc]: 'In öffentlicher Zeitleiste teilen',\n  [I18nTags.common.status_visibility_unlisted_desc]: 'Nicht in öffentlicher Zeitleiste teilen',\n  [I18nTags.common.status_visibility_private_desc]: 'Nur mit den Folgers teilen',\n  [I18nTags.common.status_visibility_direct_desc]: 'Nur mit erwähnten Freunden teilen',\n  [I18nTags.common.drag_and_drop_to_upload]: 'Drag & Drop zum Hochladen',\n  [I18nTags.common.write_your_warning_here]: 'Bitte hier Warnung schreiben'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: 'Gibt\\'s etwas Neues?',\n  [I18nTags.statusCard.reply_to_main_status]: 'Kommentieren...',\n  [I18nTags.statusCard.reply_to_replier]: 'Antworten',\n  [I18nTags.statusCard.cancel_post]: 'Abbrechen',\n  [I18nTags.statusCard.submit_post]: 'Tröt',\n  [I18nTags.statusCard.show_content]: 'Mehr anzeigen',\n  [I18nTags.statusCard.hide_content]: 'Weniger anzeigen',\n  [I18nTags.statusCard.mute_status]: 'Stummschalten',\n  [I18nTags.statusCard.delete_status]: 'Löschen',\n  [I18nTags.statusCard.delete_status_confirm]: 'Möchtest du diesen Beitrag löschen?',\n  [I18nTags.statusCard.do_delete_status_btn]: 'Löschen',\n  [I18nTags.statusCard.cancel_delete_status_btn]: 'Abbrechen',\n  [I18nTags.statusCard.originally_shared_by]: 'Ursprünglich geteilt von {displayName}<span class=\"at-name\">@{atName}</span>',\n  [I18nTags.statusCard.sensitive_media_alert]: 'Ausgeblendeter Inhalt <br/> Tippen zum Anzeigen',\n  [I18nTags.statusCard.change_visibility]: 'Sichtbarkeit ändern',\n  [I18nTags.statusCard.add_photos]: 'Fotos hinzufügen'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: 'Übersicht',\n  [I18nTags.drawer.public]: 'Öffentlich',\n  [I18nTags.drawer.tag]: 'Tag',\n  [I18nTags.drawer.local]: 'Lokal',\n  [I18nTags.drawer.profile]: 'Profil',\n  [I18nTags.drawer.settings]: 'Einstellungen',\n  [I18nTags.drawer.logout]: 'Abmelden',\n  [I18nTags.drawer.do_logout_message_confirm]: 'Möchtest du abmelden?',\n  [I18nTags.drawer.do_logout_message_yes]: 'Ja',\n  [I18nTags.drawer.do_logout_message_no]: 'Nein',\n  [I18nTags.drawer.toHostInstance]: 'Aktuelle Instanzseite öffnen',\n  [I18nTags.drawer.search_input_placeholder]: 'Suchen',\n  [I18nTags.drawer.search_result_people_label]: 'Leute',\n  [I18nTags.drawer.search_result_hashtag_label]: 'Hashtag'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: 'Allgemeines',\n  [I18nTags.settings.choose_theme]: 'Themen',\n  [I18nTags.settings.export_theme_color_set]: 'Exportieren',\n  [I18nTags.settings.import_theme_color_set]: 'Importieren',\n  [I18nTags.settings.edit_theme_color_set]: 'Bearbeiten',\n  [I18nTags.settings.delete_theme_color_set]: 'Löschen',\n\n  [I18nTags.settings.choose_language]: 'Sprache',\n  [I18nTags.settings.use_multi_line_mode]: 'Mehrspaltiger Layout-Mode verwenden',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: 'Maximale Anzahl der Spalten im mehrspaltigen Layout-Mode',\n  [I18nTags.settings.show_sensitive_media_files]: 'Sensible Medien immer anzeigen',\n  [I18nTags.settings.auto_expand_spoiler_text]: 'Ausgeblendete Texte immer automatisch expandieren',\n  [I18nTags.settings.auto_load_new_status]: 'Neue Beiträge immer automatisch laden',\n  [I18nTags.settings.post_privacy]: 'Beitragssichtbarkeit',\n  [I18nTags.settings.post_media_as_sensitive]: 'Medien immer als sensibel markieren',\n  [I18nTags.settings.only_mention_target_user]: 'Nur für erwähnte Freunden sichtbar',\n\n  [I18nTags.settings.stream_label]: 'Stream',\n  [I18nTags.settings.media_label]: 'Medien',\n  [I18nTags.settings.personality_label]: 'Individualität',\n  [I18nTags.settings.publishing_label]: 'Veröffentlichung',\n  [I18nTags.settings.web_label]: 'Web',\n\n  [I18nTags.settings.changes_successfully_saved]: 'Änderungen gespeichert!'\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: 'Das ist alles!',\n  [I18nTags.timeLines.new_message_notice]: '{count} neue Beitrag | {count} neue Beiträge',\n  [I18nTags.timeLines.whats_new_with_you]: 'Gibt\\'s etwas Neues?'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: 'Möchtest du diesen Beitrag verwerfen?',\n  [I18nTags.postStatusDialog.do_keep_message]: 'Behalten',\n  [I18nTags.postStatusDialog.do_discard_message]: 'Verwerfen',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: 'Buchstabenbegrenzung von 500 überschritten'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: 'folgt dir',\n  [I18nTags.notifications.mentioned_you]: 'hat dich erwähnt',\n  [I18nTags.notifications.favourited_your_status]:'gefällt deinen Status',\n  [I18nTags.notifications.boosted_your_status]: 'hat deinen Beitrag erneut geteilt'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/i18n/en.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: 'Cuckoo Plus',\n  [I18nTags.oauth.login_hint]: 'Authorize Login',\n  [I18nTags.oauth.server_input_label]: 'Mastodon URL',\n  [I18nTags.oauth.please_input_server_url]: 'please input Mastodon URL',\n  [I18nTags.oauth.please_input_correct_server_url]: 'check your Mastodon URL',\n  [I18nTags.oauth.register_app_error_message]: 'Something went wrong! please check your Mastodon URL again',\n  [I18nTags.oauth.confirm_input]: 'CONFIRM'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: 'Public',\n  [I18nTags.common.status_visibility_unlisted]: 'Unlisted',\n  [I18nTags.common.status_visibility_private]: 'Followers-only',\n  [I18nTags.common.status_visibility_direct]: 'Direct',\n  [I18nTags.common.status_visibility_public_desc]: 'Post to public timelines',\n  [I18nTags.common.status_visibility_unlisted_desc]: 'Do not post to public timelines',\n  [I18nTags.common.status_visibility_private_desc]: 'Post to followers only',\n  [I18nTags.common.status_visibility_direct_desc]: 'Post to mentioned users only',\n  [I18nTags.common.drag_and_drop_to_upload]: 'Drag & Drop to upload',\n  [I18nTags.common.write_your_warning_here]: 'Write your warning here'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: 'What is on your mind?',\n  [I18nTags.statusCard.reply_to_main_status]: 'Add a comment...',\n  [I18nTags.statusCard.reply_to_replier]: 'REPLY',\n  [I18nTags.statusCard.cancel_post]: 'CANCEL',\n  [I18nTags.statusCard.submit_post]: 'POST',\n  [I18nTags.statusCard.show_content]: 'SHOW MORE',\n  [I18nTags.statusCard.hide_content]: 'SHOW LESS',\n  [I18nTags.statusCard.mute_status]: 'Mute Post',\n  [I18nTags.statusCard.mute_user]: 'Mute User',\n  [I18nTags.statusCard.delete_status]: 'Delete',\n  [I18nTags.statusCard.delete_status_confirm]: 'Are you sure you want to delete the post?',\n  [I18nTags.statusCard.do_delete_status_btn]: 'DELETE',\n  [I18nTags.statusCard.cancel_delete_status_btn]: 'CANCEL',\n  [I18nTags.statusCard.mute_status_confirm]: 'Are you sure you want to mute the post?',\n  [I18nTags.statusCard.do_mute_status_btn]: 'MUTE',\n  [I18nTags.statusCard.cancel_mute_status_btn]: 'CANCEL',\n  [I18nTags.statusCard.mute_user_confirm]: 'Are you sure you want to mute this user?',\n  [I18nTags.statusCard.do_mute_user_btn]: 'MUTE',\n  [I18nTags.statusCard.cancel_mute_user_btn]: 'CANCEL',\n  [I18nTags.statusCard.originally_shared_by]: 'Originally shared by {displayName}<span class=\"at-name\">@{atName}</span>',\n  [I18nTags.statusCard.sensitive_media_alert]: 'Hide content <br/> Click to view',\n  [I18nTags.statusCard.change_visibility]: 'Change Visibility',\n  [I18nTags.statusCard.add_photos]: 'Add Photos'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: 'Home',\n  [I18nTags.drawer.public]: 'Public',\n  [I18nTags.drawer.tag]: 'Tag',\n  [I18nTags.drawer.local]: 'Local',\n  [I18nTags.drawer.profile]: 'Profile',\n  [I18nTags.drawer.settings]: 'Settings',\n  [I18nTags.drawer.logout]: 'Logout',\n  [I18nTags.drawer.do_logout_message_confirm]: 'Are you sure you want to Logout?',\n  [I18nTags.drawer.do_logout_message_yes]: 'Yes',\n  [I18nTags.drawer.do_logout_message_no]: 'No',\n  [I18nTags.drawer.toHostInstance]: 'Open Current Instance Site',\n  [I18nTags.drawer.search_input_placeholder]: 'Search',\n  [I18nTags.drawer.search_result_people_label]: 'People',\n  [I18nTags.drawer.search_result_hashtag_label]: 'HashTag'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: 'General',\n  [I18nTags.settings.choose_theme]: 'Choose Theme:',\n  [I18nTags.settings.export_theme_color_set]: 'export',\n  [I18nTags.settings.import_theme_color_set]: 'import',\n  [I18nTags.settings.edit_theme_color_set]: 'edit',\n  [I18nTags.settings.delete_theme_color_set]: 'delete',\n\n  [I18nTags.settings.choose_language]: 'Choose Language:',\n  [I18nTags.settings.use_multi_line_mode]: 'Use multi-column layout mode:',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: 'Maximum number of columns in multi-column layout mode:',\n  [I18nTags.settings.show_sensitive_media_files]: 'Always show media marked as sensitive:',\n  [I18nTags.settings.auto_expand_spoiler_text]: 'Always auto expand spoiler text:',\n  [I18nTags.settings.auto_load_new_status]: 'Always auto load new post:',\n  [I18nTags.settings.post_privacy]: 'Post privacy:',\n  [I18nTags.settings.post_media_as_sensitive]: 'Always mark media as sensitive:',\n  [I18nTags.settings.only_mention_target_user]: 'Only mention target user',\n\n  [I18nTags.settings.stream_label]: 'Stream',\n  [I18nTags.settings.media_label]: 'Media',\n  [I18nTags.settings.personality_label]: 'Personality',\n  [I18nTags.settings.publishing_label]: 'Publishing',\n  [I18nTags.settings.web_label]: 'Web',\n\n  [I18nTags.settings.changes_successfully_saved]: 'Changes successfully saved!'\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: 'You have seen all posts.',\n  [I18nTags.timeLines.new_message_notice]: '{count} new post | {count} new posts',\n  [I18nTags.timeLines.whats_new_with_you]: 'What\\'s new with you?'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: 'Discard this post?',\n  [I18nTags.postStatusDialog.do_keep_message]: 'KEEP',\n  [I18nTags.postStatusDialog.do_discard_message]: 'DISCARD',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: 'Text character limit of 500 exceeded'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: 'followed you',\n  [I18nTags.notifications.mentioned_you]: 'mentioned you',\n  [I18nTags.notifications.favourited_your_status]:'favourited your status',\n  [I18nTags.notifications.boosted_your_status]: 'boosted your status'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/i18n/index.ts",
    "content": "import Vue from 'vue'\nimport VueI18n from 'vue-i18n'\nimport { I18nLocales } from '@/constant'\nimport store from '@/store'\n\nimport EN from './en'\nimport DE from './de'\nimport JA from './ja'\nimport ZH_CN from './zh-cn'\nimport ZH_HK from './zh-hk'\nimport ZH_TW from './zh-tw'\n\nVue.use(VueI18n)\n\nconst currentLocale = store.state.appStatus.settings.locale\n\nconst i18nMessages = {\n  [I18nLocales.EN]: EN,\n  [I18nLocales.DE]: DE,\n  [I18nLocales.JA]: JA,\n  [I18nLocales.ZH_CN]: ZH_CN,\n  [I18nLocales.ZH_HK]: ZH_HK,\n  [I18nLocales.ZH_TW]: ZH_TW\n}\n\nexport default new VueI18n({\n  locale: currentLocale,\n  messages: i18nMessages,\n  fallbackLocale: I18nLocales.EN\n})\n"
  },
  {
    "path": "src/i18n/ja.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: 'Cuckoo Plus',\n  [I18nTags.oauth.login_hint]: '連携ログイン',\n  [I18nTags.oauth.server_input_label]: 'マストドンのURL',\n  [I18nTags.oauth.please_input_server_url]: 'マストドンのURLを入力してください',\n  [I18nTags.oauth.please_input_correct_server_url]: 'マストドンのURLを確認してください',\n  [I18nTags.oauth.register_app_error_message]: '何かがおかしいです！ マストドンのURLを確認してください',\n  [I18nTags.oauth.confirm_input]: '確認'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: '公開',\n  [I18nTags.common.status_visibility_unlisted]: '未収載',\n  [I18nTags.common.status_visibility_private]: 'フォロワー限定',\n  [I18nTags.common.status_visibility_direct]: 'ダイレクト',\n  [I18nTags.common.status_visibility_public_desc]: '公開TLに投稿する',\n  [I18nTags.common.status_visibility_unlisted_desc]: '公開TLで表示しない',\n  [I18nTags.common.status_visibility_private_desc]: 'フォロワーだけに公開',\n  [I18nTags.common.status_visibility_direct_desc]: 'メンションしたユーザーだけに公開',\n  [I18nTags.common.drag_and_drop_to_upload]: 'ドラッグ＆ドロップでアップロード',\n  [I18nTags.common.write_your_warning_here]: 'ここに警告を書いてください'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: '今なにしてる？',\n  [I18nTags.statusCard.reply_to_main_status]: 'コメントを追加してください...',\n  [I18nTags.statusCard.reply_to_replier]: '返信',\n  [I18nTags.statusCard.cancel_post]: 'キャンセル',\n  [I18nTags.statusCard.submit_post]: '送信',\n  [I18nTags.statusCard.show_content]: '開く',\n  [I18nTags.statusCard.hide_content]: '閉じる',\n  [I18nTags.statusCard.mute_status]: '投稿をミュート',\n  [I18nTags.statusCard.mute_user]: 'ユーザーをミュート',\n  [I18nTags.statusCard.delete_status]: '削除',\n  [I18nTags.statusCard.delete_status_confirm]: 'この投稿を削除してもよろしいですか？',\n  [I18nTags.statusCard.do_delete_status_btn]: '削除',\n  [I18nTags.statusCard.cancel_delete_status_btn]: 'キャンセル',\n  [I18nTags.statusCard.mute_status_confirm]: 'この投稿をミュートしてもよろしいですか？',\n  [I18nTags.statusCard.do_mute_status_btn]: 'ミュート',\n  [I18nTags.statusCard.cancel_mute_status_btn]: 'キャンセル',\n  [I18nTags.statusCard.mute_user_confirm]: 'このユーザーをミュートしてもよろしいですか？',\n  [I18nTags.statusCard.do_mute_user_btn]: 'ミュート',\n  [I18nTags.statusCard.cancel_mute_user_btn]: 'キャンセル',\n  [I18nTags.statusCard.originally_shared_by]: '{displayName}<span class=\"at-name\">@{atName}</span> さんから',\n  [I18nTags.statusCard.sensitive_media_alert]: '隠されたメディア <br/> クリックで開きます',\n  [I18nTags.statusCard.change_visibility]: '表示/非表示を切り替え',\n  [I18nTags.statusCard.add_photos]: '画像を追加'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: 'ホーム',\n  [I18nTags.drawer.public]: 'パブリック',\n  [I18nTags.drawer.tag]: 'タグ',\n  [I18nTags.drawer.local]: 'ローカル',\n  [I18nTags.drawer.profile]: 'プロフィール',\n  [I18nTags.drawer.settings]: '設定',\n  [I18nTags.drawer.logout]: 'ログアウト',\n  [I18nTags.drawer.do_logout_message_confirm]: 'ログアウトしてもよろしいですか？' ,\n  [I18nTags.drawer.do_logout_message_yes]: 'はい',\n  [I18nTags.drawer.do_logout_message_no]: 'いいえ',\n  [I18nTags.drawer.toHostInstance]: '現在のインスタンスを開く',\n  [I18nTags.drawer.search_input_placeholder]: '検索',\n  [I18nTags.drawer.search_result_people_label]: 'ユーザー',\n  [I18nTags.drawer.search_result_hashtag_label]: 'ハッシュタグ'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: '一般',\n  [I18nTags.settings.choose_theme]: 'テーマ:',\n  [I18nTags.settings.export_theme_color_set]: 'エクスポート',\n  [I18nTags.settings.import_theme_color_set]: 'インポート',\n  [I18nTags.settings.edit_theme_color_set]: '編集',\n  [I18nTags.settings.delete_theme_color_set]: '削除',\n\n  [I18nTags.settings.choose_language]: '言語:',\n  [I18nTags.settings.use_multi_line_mode]: 'マルチカラムレイアウトを使う:',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: 'マルチカラムレイアウトの最大カラム数:',\n  [I18nTags.settings.show_sensitive_media_files]: 'メディアを常に閲覧注意としてマークする:',\n  [I18nTags.settings.auto_expand_spoiler_text]: '警告内容を自動的に表示する:',\n  [I18nTags.settings.auto_load_new_status]: '新しい投稿を常に自動的に読み込む:',\n  [I18nTags.settings.post_privacy]: '投稿の公開範囲:',\n  [I18nTags.settings.post_media_as_sensitive]: '自分が投稿するメディアを常に閲覧注意 (NSFW) に設定する:',\n  [I18nTags.settings.only_mention_target_user]: 'ターゲットユーザーのみをメンションする:',\n\n  [I18nTags.settings.stream_label]: 'ストリーム',\n  [I18nTags.settings.media_label]: 'メディア',\n  [I18nTags.settings.personality_label]: 'パーソナリティ',\n  [I18nTags.settings.publishing_label]: '投稿',\n  [I18nTags.settings.web_label]: 'ウェブ',\n\n  [I18nTags.settings.changes_successfully_saved]: '正常に変更されました！'\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: 'すべての投稿を見ました。',\n  [I18nTags.timeLines.new_message_notice]: '新しい投稿 {count} | 新しい投稿 {count}',\n  [I18nTags.timeLines.whats_new_with_you]: '最近の出来事を共有してみましょう'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: 'この投稿を破棄しますか？',\n  [I18nTags.postStatusDialog.do_keep_message]: '保持',\n  [I18nTags.postStatusDialog.do_discard_message]: '破棄',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: '文字数が500を超えています。'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: 'さんがあなたをフォローしています',\n  [I18nTags.notifications.mentioned_you]: '返信',\n  [I18nTags.notifications.favourited_your_status]:'お気に入り',\n  [I18nTags.notifications.boosted_your_status]: 'ブースト'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/i18n/zh-cn.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: '布谷鸟 Plus',\n  [I18nTags.oauth.login_hint]: '授权登录',\n  [I18nTags.oauth.server_input_label]: 'Mastodon 链接',\n  [I18nTags.oauth.please_input_server_url]: '请输入 Mastodon 链接',\n  [I18nTags.oauth.please_input_correct_server_url]: '请输入正确的 Mastodon 链接',\n  [I18nTags.oauth.register_app_error_message]: '出错啦！检查一下目标链接是否正确吧',\n  [I18nTags.oauth.confirm_input]: '确认'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: '公开',\n  [I18nTags.common.status_visibility_unlisted]: '不公开',\n  [I18nTags.common.status_visibility_private]: '仅关注者',\n  [I18nTags.common.status_visibility_direct]: '私信',\n  [I18nTags.common.status_visibility_public_desc]: '所有人可见，并会出现在公共时间轴上',\n  [I18nTags.common.status_visibility_unlisted_desc]: '所有人可见，但不会出现在公共时间轴上',\n  [I18nTags.common.status_visibility_private_desc]: '只有关注你的用户能看到',\n  [I18nTags.common.status_visibility_direct_desc]: '只有被提及的用户能看到',\n  [I18nTags.common.drag_and_drop_to_upload]: '将文件拖放至此处开始上传',\n  [I18nTags.common.write_your_warning_here]: '折叠部分的警告消息'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: '你最近有什么新鲜事要分享吗？',\n  [I18nTags.statusCard.reply_to_main_status]: '发表评论…',\n  [I18nTags.statusCard.reply_to_replier]: '回复',\n  [I18nTags.statusCard.cancel_post]: '取消',\n  [I18nTags.statusCard.submit_post]: '发布',\n  [I18nTags.statusCard.show_content]: '显示内容',\n  [I18nTags.statusCard.hide_content]: '隐藏内容',\n\n  [I18nTags.statusCard.mute_status]: '忽略嘟文',\n  [I18nTags.statusCard.mute_status_confirm]: '要忽略这条嘟文吗？',\n  [I18nTags.statusCard.do_mute_status_btn]: '忽略',\n  [I18nTags.statusCard.cancel_mute_user_btn]: '取消',\n\n  [I18nTags.statusCard.mute_user]: '忽略用户',\n  [I18nTags.statusCard.mute_user_confirm]: '要忽略该用户吗？',\n  [I18nTags.statusCard.do_mute_user_btn]: '忽略',\n  [I18nTags.statusCard.cancel_mute_user_btn]: '取消',\n\n  [I18nTags.statusCard.delete_status]: '删除',\n  [I18nTags.statusCard.delete_status_confirm]: '要删除这条嘟文吗?',\n  [I18nTags.statusCard.do_delete_status_btn]: '删除',\n  [I18nTags.statusCard.cancel_delete_status_btn]: '取消',\n  [I18nTags.statusCard.originally_shared_by]: '此信息最初是由{displayName}<span class=\"at-name\">@{atName}</span>分享的',\n  [I18nTags.statusCard.sensitive_media_alert]: '隐藏媒体内容 <br/> 点击显示'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: '主页',\n  [I18nTags.drawer.public]: '公共',\n  [I18nTags.drawer.local]: '本站时间轴',\n  [I18nTags.drawer.tag]: '标签',\n  [I18nTags.drawer.profile]: '个人资料',\n  [I18nTags.drawer.settings]: '设置',\n  [I18nTags.drawer.logout]: '注销',\n  [I18nTags.drawer.do_logout_message_confirm]: '你确定要注销Cuckoo吗？' ,\n  [I18nTags.drawer.do_logout_message_yes]: '是的',\n  [I18nTags.drawer.do_logout_message_no]: '我只是手滑',\n  [I18nTags.drawer.toHostInstance]: '打开当前实例站点',\n  [I18nTags.drawer.search_input_placeholder]: '搜索',\n  [I18nTags.drawer.search_result_people_label]: '用户',\n  [I18nTags.drawer.search_result_hashtag_label]: '话题标签'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: '常规',\n  [I18nTags.settings.choose_theme]: '选择主题：',\n  [I18nTags.settings.choose_language]: '选择语言：',\n  [I18nTags.settings.use_multi_line_mode]: '使用多列布局模式：',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: '多列布局模式下的最大列数：',\n  [I18nTags.settings.show_sensitive_media_files]: '总是显示被标记为敏感的媒体文件：',\n  [I18nTags.settings.auto_expand_spoiler_text]: '总是显示被警告折叠的文本内容：',\n  [I18nTags.settings.auto_load_new_status]: '总是自动加载新的嘟文：',\n  [I18nTags.settings.post_privacy]: '嘟文默认可见范围：',\n  [I18nTags.settings.post_media_as_sensitive]: '总是将我发送的媒体文件标记为敏感内容：',\n  [I18nTags.settings.only_mention_target_user]: '回复时仅提及目标使用者',\n\n  [I18nTags.settings.stream_label]: '信息流',\n  [I18nTags.settings.media_label]: '媒体内容',\n  [I18nTags.settings.personality_label]: '个性化',\n  [I18nTags.settings.publishing_label]: '发布',\n  [I18nTags.settings.web_label]: '站内',\n\n  [I18nTags.settings.changes_successfully_saved]: '更改保存成功！'\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: '没有更多啦！',\n  [I18nTags.timeLines.new_message_notice]: '{count}条新信息',\n  [I18nTags.timeLines.whats_new_with_you]: '你最近有什么新鲜事要分享吗？'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: '要舍弃这条信息吗？',\n  [I18nTags.postStatusDialog.do_keep_message]: '保留',\n  [I18nTags.postStatusDialog.do_discard_message]: '舍弃',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: '内容超过500个字符的限制了'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: '关注了你',\n  [I18nTags.notifications.mentioned_you]: '提及了你',\n  [I18nTags.notifications.favourited_your_status]:'喜欢了你的嘟文',\n  [I18nTags.notifications.boosted_your_status]: '转发了你的嘟文'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/i18n/zh-hk.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: '布穀鳥 Plus',\n  [I18nTags.oauth.login_hint]: '授權登錄',\n  [I18nTags.oauth.server_input_label]: 'Mastodon 連結',\n  [I18nTags.oauth.please_input_server_url]: '請輸入 Mastodon 連結',\n  [I18nTags.oauth.please_input_correct_server_url]: '請輸入準確的 Mastodon 連結',\n  [I18nTags.oauth.register_app_error_message]: '請檢查目標連結是否準確',\n  [I18nTags.oauth.confirm_input]: '確認'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: '公共',\n  [I18nTags.common.status_visibility_unlisted]: '公開',\n  [I18nTags.common.status_visibility_private]: '關注者',\n  [I18nTags.common.status_visibility_direct]: '私人訊息',\n  [I18nTags.common.status_visibility_public_desc]: '在公共時間軸顯示',\n  [I18nTags.common.status_visibility_unlisted_desc]: '公開，但不在公共時間軸顯示',\n  [I18nTags.common.status_visibility_private_desc]: '只有關注你用戶能看到',\n  [I18nTags.common.status_visibility_direct_desc]: '只有提及的用戶能看到',\n  [I18nTags.common.drag_and_drop_to_upload]: '將檔案拖放至此上載',\n  [I18nTags.common.write_your_warning_here]: '敏感警告訊息'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: '你有什麼新動態？',\n  [I18nTags.statusCard.reply_to_main_status]: '新增留言…',\n  [I18nTags.statusCard.reply_to_replier]: '回覆',\n  [I18nTags.statusCard.cancel_post]: '取消',\n  [I18nTags.statusCard.submit_post]: '發佈',\n  [I18nTags.statusCard.show_content]: '顯示內容',\n  [I18nTags.statusCard.hide_content]: '隱藏內容',\n  [I18nTags.statusCard.mute_status]: '忽略',\n  [I18nTags.statusCard.delete_status]: '刪除',\n  [I18nTags.statusCard.delete_status_confirm]: '你確定要刪除這則訊息嗎？',\n  [I18nTags.statusCard.do_delete_status_btn]: '刪除',\n  [I18nTags.statusCard.cancel_delete_status_btn]: '取消',\n  [I18nTags.statusCard.originally_shared_by]: '最初由{displayName}<span class=\"at-name\">@{atName}</span>分享',\n  [I18nTags.statusCard.sensitive_media_alert]: '隐藏內容 <br/> 點擊顯示'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: '主頁',\n  [I18nTags.drawer.public]: '跨站時間軸',\n  [I18nTags.drawer.tag]: '標籤',\n  [I18nTags.drawer.local]: '本站時間軸',\n  [I18nTags.drawer.profile]: '個人檔案',\n  [I18nTags.drawer.settings]: '設定',\n  [I18nTags.drawer.logout]: '登出',\n  [I18nTags.drawer.do_logout_message_confirm]: '確定登出Cuckoo？' ,\n  [I18nTags.drawer.do_logout_message_yes]: '是',\n  [I18nTags.drawer.do_logout_message_no]: '否',\n  [I18nTags.drawer.toHostInstance]: '前往當前實例站點',\n  [I18nTags.drawer.search_input_placeholder]: '搜索',\n  [I18nTags.drawer.search_result_people_label]: '用戶',\n  [I18nTags.drawer.search_result_hashtag_label]: '話題標籤'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: '一般',\n  [I18nTags.settings.choose_theme]: '選擇主題：',\n  [I18nTags.settings.choose_language]: '選擇語言：',\n  [I18nTags.settings.use_multi_line_mode]: '使用多列佈局模式：',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: '多列佈局模式下的最大列數：',\n  [I18nTags.settings.show_sensitive_media_files]: '總是顯示被標記為敏感的媒體文件：',\n  [I18nTags.settings.auto_expand_spoiler_text]: '總是顯示被警告折疊的文本內容：',\n  [I18nTags.settings.auto_load_new_status]: '總是自動加載最新嘟文：',\n  [I18nTags.settings.post_privacy]: '文章預設為：',\n  [I18nTags.settings.post_media_as_sensitive]: '預設我的內容為敏感內容：',\n  [I18nTags.settings.only_mention_target_user]: '回覆時僅提及目標使用者',\n\n  [I18nTags.settings.stream_label]: '訊息串',\n  [I18nTags.settings.media_label]: '媒體內容',\n  [I18nTags.settings.personality_label]: '個人化',\n  [I18nTags.settings.publishing_label]: '發佈',\n  [I18nTags.settings.web_label]: '站内',\n\n  [I18nTags.settings.changes_successfully_saved]: '已成功儲存修改'\n\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: '你已看完了所有訊息',\n  [I18nTags.timeLines.new_message_notice]: '{count}條新訊息',\n  [I18nTags.timeLines.whats_new_with_you]: '你有什麼新動態？'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: '確定要捨棄這則訊息嗎？',\n  [I18nTags.postStatusDialog.do_keep_message]: '保留',\n  [I18nTags.postStatusDialog.do_discard_message]: '捨棄',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: '內容超出500個字符的限制了'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: '關注了你',\n  [I18nTags.notifications.mentioned_you]: '提及了你',\n  [I18nTags.notifications.favourited_your_status]:'收藏了你的文章',\n  [I18nTags.notifications.boosted_your_status]: '轉推你的文章'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/i18n/zh-tw.ts",
    "content": "import { I18nTags } from '@/constant'\n\nconst oauth = {\n  [I18nTags.oauth.form_brand]: '布穀鳥 Plus',\n  [I18nTags.oauth.login_hint]: '授權登入',\n  [I18nTags.oauth.server_input_label]: 'Mastodon 連結',\n  [I18nTags.oauth.please_input_server_url]: '請輸入 Mastodon 連結',\n  [I18nTags.oauth.please_input_correct_server_url]: '請輸入準確的 Mastodon 連結',\n  [I18nTags.oauth.register_app_error_message]: '請檢查目標連結是否準確',\n  [I18nTags.oauth.confirm_input]: '確認'\n}\n\nconst common = {\n  [I18nTags.common.status_visibility_public]: '公開貼',\n  [I18nTags.common.status_visibility_unlisted]: '不列出來',\n  [I18nTags.common.status_visibility_private]: '關注貼',\n  [I18nTags.common.status_visibility_direct]: '直接貼',\n  [I18nTags.common.status_visibility_public_desc]: '貼到公開時間軸',\n  [I18nTags.common.status_visibility_unlisted_desc]: '不要貼到公開時間軸',\n  [I18nTags.common.status_visibility_private_desc]: '只貼給關注者',\n  [I18nTags.common.status_visibility_direct_desc]: '只貼給提到的使用者',\n  [I18nTags.common.drag_and_drop_to_upload]: '將檔案拖放至此上載',\n  [I18nTags.common.write_your_warning_here]: '敏感警告訊息'\n}\n\nconst statusCard = {\n  [I18nTags.statusCard.post_new_status_placeholder]: '最近有什麼新鮮事？',\n  [I18nTags.statusCard.reply_to_main_status]: '發表留言...',\n  [I18nTags.statusCard.reply_to_replier]: '回覆',\n  [I18nTags.statusCard.cancel_post]: '取消',\n  [I18nTags.statusCard.submit_post]: '發佈',\n  [I18nTags.statusCard.show_content]: '顯示內容',\n  [I18nTags.statusCard.hide_content]: '隱藏內容',\n  [I18nTags.statusCard.mute_status]: '忽略',\n  [I18nTags.statusCard.delete_status]: '刪除',\n  [I18nTags.statusCard.delete_status_confirm]: '確定要刪除這則訊息嗎？',\n  [I18nTags.statusCard.do_delete_status_btn]: '删除',\n  [I18nTags.statusCard.cancel_delete_status_btn]: '取消',\n  [I18nTags.statusCard.originally_shared_by]: '{displayName}<span class=\"at-name\">@{atName}</span>最先分享這則訊息',\n  [I18nTags.statusCard.sensitive_media_alert]: '隐藏內容 <br/> 點來看'\n}\n\nconst drawer = {\n  [I18nTags.drawer.home]: '首頁',\n  [I18nTags.drawer.public]: '聯盟時間軸',\n  [I18nTags.drawer.local]: '本地時間軸',\n  [I18nTags.drawer.tag]: '主題標籤',\n  [I18nTags.drawer.profile]: '個人資料',\n  [I18nTags.drawer.settings]: '設定',\n  [I18nTags.drawer.logout]: '登出',\n  [I18nTags.drawer.do_logout_message_confirm]: '確定登出Cuckoo？' ,\n  [I18nTags.drawer.do_logout_message_yes]: '是',\n  [I18nTags.drawer.do_logout_message_no]: '否',\n  [I18nTags.drawer.toHostInstance]: '前往當前實例站點',\n  [I18nTags.drawer.search_input_placeholder]: '搜索',\n  [I18nTags.drawer.search_result_people_label]: '用戶',\n  [I18nTags.drawer.search_result_hashtag_label]: '話題標籤'\n}\n\nconst settings = {\n  [I18nTags.settings.general_label]: '一般',\n  [I18nTags.settings.choose_theme]: '選擇主題：',\n  [I18nTags.settings.choose_language]: '選擇語言：',\n  [I18nTags.settings.use_multi_line_mode]: '使用多列佈局模式：',\n  [I18nTags.settings.maximum_number_of_columns_in_multi_line_mode]: '多列佈局模式下的最大列數：',\n  [I18nTags.settings.show_sensitive_media_files]: '總是顯示被標記為敏感的媒體文件：',\n  [I18nTags.settings.auto_expand_spoiler_text]: '總是顯示被警告折疊的文本內容：',\n  [I18nTags.settings.auto_load_new_status]: '總是自動加載最新嘟文：',\n  [I18nTags.settings.post_privacy]: '文章預設為：',\n  [I18nTags.settings.post_media_as_sensitive]: '預設我的內容為敏感內容：',\n  [I18nTags.settings.only_mention_target_user]: '回覆時僅提及目標使用者',\n\n  [I18nTags.settings.stream_label]: '訊息串',\n  [I18nTags.settings.media_label]: '媒體內容',\n  [I18nTags.settings.personality_label]: '個人化',\n  [I18nTags.settings.publishing_label]: '發佈',\n  [I18nTags.settings.web_label]: '站内',\n\n  [I18nTags.settings.changes_successfully_saved]: '已成功儲存修改'\n}\n\nconst timeLines = {\n  [I18nTags.timeLines.no_load_more_status_notice]: '你已看完了所有訊息',\n  [I18nTags.timeLines.new_message_notice]: '{count}條新訊息',\n  [I18nTags.timeLines.whats_new_with_you]: '最近有什麼新鮮事？'\n}\n\nconst postStatusDialog = {\n  [I18nTags.postStatusDialog.do_discard_message_confirm]: '確定要捨棄這則訊息嗎？',\n  [I18nTags.postStatusDialog.do_keep_message]: '保留',\n  [I18nTags.postStatusDialog.do_discard_message]: '捨棄',\n  [I18nTags.postStatusDialog.text_character_limit_exceed]: '內容超出500個字符的限制了'\n}\n\nconst notifications = {\n  [I18nTags.notifications.someone_followed_you]: '關注了你',\n  [I18nTags.notifications.mentioned_you]: '提及了你',\n  [I18nTags.notifications.favourited_your_status]:'收藏了你的文章',\n  [I18nTags.notifications.boosted_your_status]: '轉推你的文章'\n}\n\nexport default {\n  ...oauth,\n  ...common,\n  ...statusCard,\n  ...timeLines,\n  ...drawer,\n  ...settings,\n  ...postStatusDialog,\n  ...notifications\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "const Toast = require('muse-ui-toast').default\nconst Message = require('muse-ui-message').default\nconst Loading = require('muse-ui-loading').default\nconst NProgress = require('muse-ui-progress').default\nimport Vue from 'vue'\nimport MuseUI from 'muse-ui'\nimport 'muse-ui-loading/dist/muse-ui-loading.css'\nimport 'muse-ui-progress/dist/muse-ui-progress.css'\nimport VueResource from 'vue-resource'\nimport i18n from './i18n'\nimport store from './store'\nimport router from './router'\nimport App from './App.vue'\nimport * as moment from 'moment'\nimport { I18nTags, RoutersInfo, I18nLocales } from '@/constant'\nimport ThemeManager from '@/themes'\nimport './directives'\n\nVue.use({\n  install (Vue) {\n    Vue.prototype.$i18nTags = I18nTags;\n    Vue.prototype.$routersInfo = RoutersInfo;\n  }\n})\n\nVue.use(MuseUI)\nVue.use(VueResource)\nVue.use(Toast, {\n  position: 'bottom-start'\n})\nVue.use(Message)\nVue.use(NProgress, {\n  color: 'primary',\n  zIndex: 9999999999,\n})\nVue.use(Loading, {\n  overlayColor: 'hsla(0,0%,100%,.9)',\n  size: 48,\n  color: 'primary',\n})\n\nconst currentLocale = store.state.appStatus.settings.locale\n\nmoment.locale(currentLocale)\n\nconst httpInterceptor: any = (request) => {\n  request.headers.set('Authorization', `Bearer ${store.state.OAuthInfo.accessToken}`);\n}\n\nVue.http.interceptors.push(httpInterceptor)\n\n// @ts-ignore\nif (window.Notification) {\n  Notification.requestPermission()\n}\n\nThemeManager.setTheme(store.state.appStatus.settings.theme)\n\nif ('serviceWorker' in navigator && (process.env.NODE_ENV !== 'develop')) {\n  navigator.serviceWorker.register('/sw.js')\n}\n\nif (process.env.NODE_ENV === 'develop') {\n  navigator.serviceWorker && navigator.serviceWorker.getRegistrations()\n    .then(registrations => {\n      for(let registration of registrations) {\n        registration.unregister()\n      }\n    })\n}\n\nnew Vue({\n  el: '#app',\n  store,\n  router,\n  i18n,\n  render(h) {\n    return h(App)\n  }\n});\n"
  },
  {
    "path": "src/interface/definition/vue-extend.d.ts",
    "content": "import VueRouter, { Route } from \"vue-router\";\n\ninterface routerInfo { path: string, name: string }\n\ndeclare module \"vue/types/vue\" {\n  interface Vue {\n    $router: VueRouter;\n    $route: Route;\n\n    $routersInfo: {\n      empty: routerInfo\n      home: routerInfo\n      oauth: routerInfo\n      settings: routerInfo\n      statuses: routerInfo\n      timelines: routerInfo\n      defaulttimelines: routerInfo\n      tagtimelines: routerInfo\n      listtimelines: routerInfo\n      accounts: routerInfo\n    }\n\n    $i18nTags: {\n      statusCard: {\n        post_new_status_placeholder: string\n        reply_to_replier: string\n        reply_to_main_status: string\n        cancel_post: string\n        submit_post: string\n        show_content: string\n        hide_content: string\n        mute_status: string\n        delete_status: string\n        delete_status_confirm: string\n        do_delete_status_btn: string\n        cancel_delete_status_btn: string\n        originally_shared_by: string\n        sensitive_media_alert: string\n        change_visibility: string\n        add_photos: string\n      },\n      common: {\n        status_visibility_public: string\n        status_visibility_private: string\n        status_visibility_unlisted: string\n        status_visibility_direct: string\n        status_visibility_public_desc: string,\n        status_visibility_private_desc: string,\n        status_visibility_unlisted_desc: string,\n        status_visibility_direct_desc: string,\n        drag_and_drop_to_upload: string\n        write_your_warning_here: string\n      },\n      timeLines: {\n        no_load_more_status_notice: string\n        new_message_notice: string\n        whats_new_with_you: string\n      },\n      header: {\n\n      },\n      drawer: {\n        home: string\n        public: string\n        tag: string\n        profile: string\n        settings: string\n        toHostInstance: string\n        search_input_placeholder: string\n        search_result_people_label: string\n        search_result_hashtag_label: string\n        do_logout_message_confirm: string\n        do_logout_message_yes: string\n        do_logout_message_no: string\n      },\n      settings: {\n        general_label: string\n        choose_theme: string\n        export_theme_color_set: string\n        import_theme_color_set: string\n        edit_theme_color_set: string\n        delete_theme_color_set: string\n\n        choose_language: string\n        use_multi_line_mode: string\n        show_sensitive_media_files: string\n        auto_load_new_status: string\n        post_privacy: string\n        post_media_as_sensitive: string\n        only_mention_target_user: string\n        maximum_number_of_columns_in_multi_line_mode: string\n        auto_expand_spoiler_text: string\n\n        stream_label: string\n        media_label: string\n        publishing_label: string\n        personality_label: string\n        web_label: string\n        changes_successfully_saved: string\n      },\n      home: {\n\n      },\n      oauth: {\n        form_brand: string\n        login_hint: string\n        server_input_label: string\n        please_input_server_url: string\n        please_input_correct_server_url: string\n        register_app_error_message: string\n        confirm_input: string\n      },\n      postStatusDialog: {\n        do_discard_message_confirm: string\n        do_keep_message: string\n        do_discard_message: string\n        text_character_limit_exceed: string\n      },\n      notifications: {\n        someone_followed_you: string\n        mentioned_you: string\n        boosted_your_status: string\n        favourited_your_status: string\n      }\n    }\n\n    $toast: {\n      error: (msg: string) => void\n    }\n\n    $confirm: (message: string, title: string, options) => Promise<{ result: boolean }>\n  }\n}\n"
  },
  {
    "path": "src/interface/definition/vue-shims.d.ts",
    "content": "declare module \"*.vue\" {\n  import Vue from \"vue\";\n  export default Vue;\n}\n"
  },
  {
    "path": "src/interface/entities.ts",
    "content": "export namespace mastodonentities {\n\n  export interface Application {\n\n  }\n\n  export interface Account {\n    // The ID of the account\n    id: string\n    // The username of the account\n    username: string\n    // Equals username for local users, includes @domain for remote ones\n    acct: string\n    // The account's display name\n    display_name: string\n    // Boolean for when the account cannot be followed without waiting for approval first\n    locked: string\n    // The time the account was created\n    created_at: string\n    // The number of followers for the account\n    followers_count: string\n    // The number of accounts the given account is following\n    following_count: string\n    // The number of statuses the account has made\n    statuses_count: string\n    // Biography of user\n    note: string\n    // URL of the user's profile page (can be remote)\n    url: string\n    // URL to the avatar image\n    avatar: string\n    // URL to the avatar static image (gif)\n    avatar_static: string\n    // URL to the header image\n    header: string\n    // URL to the header static image (gif)\n    header_static: string\n    // Array of Emoji in account username and note\n    emojis: Array<string>\n    // If the owner decided to switch accounts, new account is in this attribute\n    moved?: any\n    // Array of profile metadata field, each element has 'name' and 'value'\n    fields?: Array<any>\n    // Boolean to indicate that the account performs automated actions\n    bot?: boolean\n  }\n\n  export interface AuthenticatedAccount extends Account {\n    source: {\n      // Selected preference: Default privacy of new toots\n      privacy: string\n      // Selected preference: Mark media as sensitive by default?\n      sensitive: boolean\n      // Plain-text version of the account's note\n      note: string\n      // Array of profile metadata, each element has 'name' and 'value'\n      fields: Array<any>\n    }\n  }\n\n  export interface Status {\n    // The ID of the status\n    id: string\n    // A Fediverse-unique resource ID\n    uri: string\n    // URL to the status page (can be remote)\n    url?: string\n    // The Account which posted the status\n    account: Account\n    // null or the ID of the status it replies to\n    // in_reply_to_id and in_reply_to_account_id are null if the status that is replied to is unknown\n    in_reply_to_id?: string\n    // null or the ID of the account it replies to\n    in_reply_to_account_id?: string\n    // null or the reblogged Status\n    reblog?: Status\n    // Body of the status; this will contain HTML (remote HTML already sanitized)\n    content: string\n    // The time the status was created\n    created_at: string\n    // An array of Emoji\n    emojis: Array<Emoji>\n    // The number of replies for the status\n    replies_count: number\n    // The number of reblogs for the status\n    reblogs_count: number\n    // The number of favourites for the status\n    favourites_count: number\n    // Whether the authenticated user has reblogged the status\n    reblogged?: boolean\n    // Whether the authenticated user has favourited the status\n    favourited?: boolean\n    // Whether the authenticated user has muted the conversation this status from\n    muted?: boolean\n    // Whether media attachments should be hidden by default\n    // NOTE: When spoiler_text is present, sensitive is true\n    sensitive?: boolean\n    // If not empty, warning text that should be displayed before the actual content\n    spoiler_text: string\n    // One of: public, unlisted, private, direct\n    visibility: string\n    // An array of Attachments\n    media_attachments: Array<Attachment>\n    // An array of Mentions\n    mentions: Array<Mention>\n    // An array of Tags\n    tags: Array<Tag>\n    // Application from which the status was posted\n    application?: Application\n    // The detected language for the status, if detected\n    language?: string\n    // Whether this is the pinned status for the account that posted it\n    pinned?: boolean\n    // for pawoo, pixiv_cards info\n    pixiv_cards?: Array<{ image_url: string, url: string }>\n  }\n\n  export interface Context {\n    ancestors: Array<Status>\n    descendants: Array<Status>\n  }\n\n  export interface Emoji {\n\n  }\n\n  export interface Attachment {\n    // ID of the attachment\n    id: string\n    // One of: \"image\", \"video\", \"gifv\", \"unknown\"\n    type: \"image\" | \"video\" | \"gifv\" | \"unknown\"\n    // URL of the locally hosted version of the image\n    url: string\n    // For remote images, the remote URL of the original image\n    remote_url?: string\n    // URL of the preview image\n    preview_url: string\n    // Shorter URL for the image, for insertion into text (only present on local images)\n    text_url?: string\n    /**\n     * May contain small and original (referring to the preview and the original file).\n     * Images may contain width, height, size, aspect,\n     * while videos (including GIFV) may contain width, height,\n     * frame_rate, duration and bitrate. There may be another top-level object,\n     * focus with the coordinates x and y.\n     * These coordinates can be used for smart thumbnail cropping\n     **/\n    meta?: ImageMeta | GifvMeta\n    // A description of the image for the visually impaired (maximum 420 characters), or null if none provided\n    description?: string\n  }\n\n  interface ImageSizeMetaItem {\n    aspect: number\n    width: number\n    height: number\n    size: string\n  }\n\n  export interface ImageMeta {\n    focus: { x: number, y: number }\n    original: ImageSizeMetaItem\n    small: ImageSizeMetaItem\n  }\n\n  export interface GifvMeta extends ImageSizeMetaItem {\n    duration: number\n    fps: number\n    length: string\n    original: {\n      bitrate: number\n      duration: number\n      frame_rate: string\n      height: number\n      width: number\n    }\n    small: ImageSizeMetaItem\n  }\n\n  export interface Mention {\n    // URL of user's profile (can be remote)\n    url: string\n    // The username of the account\n    username: string\n    // Equals username for local users, includes @domain for remote ones\n    acct: string\n    // Account ID\n    id: string\n  }\n\n  export interface Tag {\n\n  }\n\n  export interface Notification {\n    // The notification ID\n    id: string\n    // One of: \"mention\", \"reblog\", \"favourite\", \"follow\"\n    type: NotificationType\n    // The time the notification was created\n    created_at: string\n    // The Account sending the notification to the user\n    account: Account\n    // The Status associated with the notification, if applicable\n    status?: Status\n  }\n\n  export type NotificationType = \"mention\" | \"reblog\" | \"favourite\" | \"follow\"\n\n  export interface SearchResults {\n    accounts: Array<Account>\n    statuses: Array<Status>\n    hashtags: Array<string>\n  }\n\n  export interface Emoji {\n    // The shortcode of the emoji\n    shortcode: string\n    // URL to the emoji static image\n    static_url: string\n    // URL to the emoji image\n    url: string\n    // that indicates if the emoji is visible in picker\n    visible_in_picker: boolean\n  }\n\n  export interface Relationship {\n    // Target account id\n    id: string\n    // Whether the user is currently following the account\n    following: boolean\n    // Whether the user is currently being followed by the account\n    followed_by: boolean\n    // Whether the user is currently blocking the account\n    blocking: boolean\n    // Whether the user is currently muting the account\n    muting: boolean\n    // Whether the user is also muting notifications\tno\n    muting_notifications: boolean\n    // Whether the user has requested to follow the account\n    requested: boolean\n    // Whether the user is currently blocking the accounts's domain\n    domain_blocking: boolean\n    // Whether the user's reblogs will show up in the home timeline\n    showing_reblogs: boolean\n    // Whether the user is currently endorsing the account\n    endorsed: boolean\n  }\n\n  export interface Card {\n    // The url associated with the card\n    url: string\n    // The title of the card\n    title: string\n    // The card description\n    description: string\n    // The image associated with the card, if any\tyes\n    image: string\n    // \"link\", \"photo\", \"video\", or \"rich\"\n    type: string\n    // OEmbed data\n    author_name: string\n    // OEmbed data\n    author_url: string\n    // OEmbed data\tyes\n    provider_name: string\n    // OEmbed data\tyes\n    provider_url: string\n    // OEmbed data\tyes\n    html: string\n    // OEmbed data\tyes\n    width: number\n    // OEmbed data\n    height: number\n  }\n}\n"
  },
  {
    "path": "src/interface/index.ts",
    "content": "export { cuckoostore } from '@/interface/store'\nexport { mastodonentities } from '@/interface/entities'"
  },
  {
    "path": "src/interface/store.ts",
    "content": "import { mastodonentities } from './entities'\n\nexport namespace cuckoostore {\n\n  export interface stateInfo {\n    OAuthInfo: OAuthInfo\n    mastodonServerUri: string\n    currentUserAccount: mastodonentities.Account\n\n    timelines: {\n      home: Array<string>\n      public: Array<string>\n      direct: Array<string>\n      local: Array<string>\n      tag: {\n        [index: string]: Array<string>\n      }\n      list: {\n        [index: string]: Array<string>\n      }\n    }\n\n    contextMap: {\n      [statusId: string]: {\n        ancestors: Array<string>\n        descendants: Array<string>\n      }\n    }\n\n    cardMap: {\n      [statusId: string]: mastodonentities.Card\n    }\n\n    statusMap: {\n      [statusId: string]: mastodonentities.Status\n    }\n\n    notifications: Array<mastodonentities.Notification>\n\n    relationships: {\n      [id: string]: mastodonentities.Relationship\n    }\n\n    customEmojis: Array<mastodonentities.Emoji>\n\n    appStatus: {\n      documentWidth: number\n      isDrawerOpened: boolean\n      isNotificationsPanelOpened: boolean\n      unreadNotificationCount: number\n      isEditingThemeMode: boolean\n      shouldShowThemeEditPanel: boolean\n      streamStatusesPool: {\n        home: Array<string>\n        public: Array<string>\n        direct: Array<string>\n        local: Array<string>\n        tag: {\n          [index: string]: Array<string>\n        }\n        list: {\n          [index: string]: Array<string>\n        }\n      }\n      settings: {\n        multiLineMode: boolean\n        maximumNumberOfColumnsInMultiLineMode: number\n        showSensitiveContentMode: boolean\n        postMediaAsSensitiveMode: boolean\n        realTimeLoadStatusMode: boolean\n        autoExpandSpoilerTextMode: boolean\n        onlyMentionTargetUserMode: boolean\n        theme: string,\n        tags: Array<string>\n        locale: string,\n        postPrivacy: string\n        muteMap: {\n          statusList: Array<string>\n          userList: Array<string>\n        }\n      }\n    }\n  }\n\n  export interface OAuthInfo {\n    clientId: string\n    clientSecret: string\n    accessToken: string\n    code: string\n  }\n\n}\n"
  },
  {
    "path": "src/pages/Accounts/AccountHeader.vue",
    "content": "<template>\n  <div class=\"account-header\">\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component } from 'vue-property-decorator'\n  import {  } from 'vuex-class'\n\n  @Component({})\n  class AccountHeader extends Vue {\n\n    $route\n\n    mounted () {\n      const accountId = this.$route.params.accountId\n    }\n\n  }\n\n  export default AccountHeader\n</script>\n\n<style lang=\"less\" scoped>\n\n</style>"
  },
  {
    "path": "src/pages/Accounts/index.vue",
    "content": "<template>\n  <div class=\"account-container\">\n    <account-header />\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component } from 'vue-property-decorator'\n  import { } from 'vuex-class'\n  import AccountHeader from './AccountHeader'\n\n  @Component({\n    components: {\n      'account-header': AccountHeader\n    }\n  })\n  class Accounts extends Vue {\n\n  }\n\n  export default Accounts\n</script>\n\n<style lang=\"less\" scoped>\n\n</style>\n"
  },
  {
    "path": "src/pages/OAuth.vue",
    "content": "<template>\n  <section class=\"oauth-container\">\n\n    <div class=\"form-container\">\n      <p class=\"oauth-form-brand\">{{$t($i18nTags.oauth.form_brand)}}</p>\n\n      <p class=\"oauth-login-hint\">{{$t($i18nTags.oauth.login_hint)}}</p>\n\n      <mu-form ref=\"form\" :model=\"validateForm\">\n\n        <mu-form-item class=\"server-input-form-item\" prop=\"mastodonServerUri\" :rules=\"uriRules\" :label=\"$t($i18nTags.oauth.server_input_label)\">\n          <mu-auto-complete prop=\"mastodonServerUri\" class=\"server-input\" :data=\"mastodonServerUriList\" :full-width=\"true\"\n                            :max-search-results=\"5\" label-float :prefix=\"prefix\" @keydown.enter=\"onSubmitServerName\"\n                            v-model=\"validateForm.mastodonServerUri\" avatar>\n            <template slot-scope=\"scope\">\n              <mu-list-item-action>\n                <mu-avatar>\n                  <img :src=\"scope.item.favicon\">\n                </mu-avatar>\n              </mu-list-item-action>\n              <mu-list-item-content v-html=\"scope.item.value\">\n              </mu-list-item-content>\n            </template>\n          </mu-auto-complete>\n        </mu-form-item>\n\n        <mu-button class=\"submit-server-name-btn\" color=\"primary\"\n                   @click=\"onSubmitServerName\">{{$t($i18nTags.oauth.confirm_input)}}</mu-button>\n\n      </mu-form>\n\n    </div>\n\n  </section>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component } from 'vue-property-decorator'\n  import { Mutation, State } from 'vuex-class'\n  import { apps } from '@/api'\n  import { cuckoostore } from '@/interface'\n\n  // the first step, ask for mastodon OAuth Access token\n  // and store this token\n\n  const mastodonServerUriList = [\n    { value: 'pawoo.net', favicon: 'https://pawoo.net/favicon.png' },\n    // todo get mastodon favicon\n    { value: 'mastodon.social', favicon: 'https://raw.githubusercontent.com/tootsuite/mastodon/master/public/favicon.ico' }\n  ]\n\n  function isURL(str) {\n    const pattern = /(http|https):\\/\\/(\\w+:{0,1}\\w*)?(\\S+)(:[0-9]+)?(\\/|\\/([\\w#!:.?+=&%!\\-\\/]))?/\n    return pattern.test(str);\n  }\n\n  @Component({})\n  class OAuth extends Vue {\n\n    @State('OAuthInfo') OAuthInfo: cuckoostore.OAuthInfo\n\n    @Mutation('updateMastodonServerUri') updateMastodonServerUri\n\n    @Mutation('updateClientInfo') updateClientInfo\n\n    prefix = 'https://'\n\n    mastodonServerUriList = mastodonServerUriList\n\n    get uriRules () {\n      return [\n        // @ts-ignore\n        { validate: (val) => !!val, message: this.$t(this.$i18nTags.oauth.please_input_server_url) },\n        // @ts-ignore\n        { validate: (val) => isURL(this.prefix + val), message: this.$t(this.$i18nTags.oauth.please_input_correct_server_url) }\n      ]\n    }\n\n    validateForm = {\n      mastodonServerUri: ''\n    }\n\n    onSubmitServerName () {\n      (this.$refs as any).form.validate().then(async (pass) => {\n        if (!pass) return\n\n        this.updateMastodonServerUri(this.prefix + this.validateForm.mastodonServerUri)\n\n        try {\n          const result = await apps.registerApplication()\n\n          this.updateClientInfo({\n            clientId: result.data.client_id,\n            clientSecret: result.data.client_secret\n          })\n\n          this.goToMastodonServerForOAuth()\n        } catch (e) {\n          // @ts-ignore\n          this.$toast.error(this.$t(this.$i18nTags.oauth.register_app_error_message))\n        }\n      });\n    }\n\n    goToMastodonServerForOAuth () {\n      window.location.href = `${this.prefix + this.validateForm.mastodonServerUri}/oauth/authorize` +\n        `?client_id=` + encodeURIComponent(this.OAuthInfo.clientId) +\n        `&redirect_uri=${location.origin}` +\n        `&response_type=code&scope=read write follow`\n    }\n  }\n\n  export default OAuth\n</script>\n\n<style lang=\"less\" scoped>\n  .oauth-container {\n    margin-top: 30px;\n    padding: 20px;\n\n    .form-container {\n      padding-right: 15px;\n      padding-left: 15px;\n      margin-right: auto;\n      margin-left: auto;\n    }\n\n    @media (min-width: 768px) {\n      .form-container {\n        width: 360px;\n      }\n    }\n\n    .oauth-form-brand {\n      text-align: center;\n      font-size: 20px;\n      font-weight: 700;\n      line-height: 50px;\n      padding: 0 14px;\n      margin-top: 0;\n      margin-bottom: 10px;\n    }\n\n    .oauth-login-hint {\n      text-align: center;\n      padding: 14px;\n      font-size: 14px;\n      margin: 0;\n      font-weight: bold;\n    }\n\n    .server-input-form-item {\n      margin: 0 auto;\n    }\n  }\n\n  .submit-server-name-btn {\n    width: 100%;\n    margin: 20px auto 0;\n    display: block;\n  }\n</style>\n"
  },
  {
    "path": "src/pages/Settings.vue",
    "content": "<template>\n  <div class=\"setting-page-container\">\n    <mu-card v-loading=\"isLoading\">\n      <mu-card-actions class=\"setting-card\">\n        <p class=\"card-label\">{{$t($i18nTags.settings.general_label)}}</p>\n\n        <div class=\"setting-row select-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.choose_theme)}}</span>\n          <mu-select class=\"setting-select\" v-model=\"themeName\">\n            <mu-option v-for=\"(themeInfo, index) in themeOptions\" :key=\"index\" :disabled=\"appStatus.isEditingThemeMode\"\n                       :label=\"themeInfo.value\" :value=\"themeInfo.value\">\n            </mu-option>\n          </mu-select>\n        </div>\n\n        <div class=\"foot-note secondary-read-text-color\">\n          <span @click=\"onSelectThemeColorSetFile\">{{$t($i18nTags.settings.import_theme_color_set)}}</span>\n          /\n          <span @click=\"shouldOpenThemeColorSetExportDialog = true\">{{$t($i18nTags.settings.export_theme_color_set)}}</span>\n          /\n          <span @click=\"onShowEditThemePanel\">{{$t($i18nTags.settings.edit_theme_color_set)}}</span>\n          /\n          <span @click=\"onOpenDeleteThemeColorSetPanel\">{{$t($i18nTags.settings.delete_theme_color_set)}}</span>\n        </div>\n        <mu-dialog :title=\"$t($i18nTags.settings.export_theme_color_set)\" :open.sync=\"shouldOpenThemeColorSetExportDialog\">\n          <div class=\"setting-row select-row dialog-setting-row\">\n            <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.choose_theme)}}</span>\n            <mu-select class=\"setting-select\" v-model=\"themeNameToExport\">\n              <mu-option v-for=\"(themeInfo, index) in themeOptions\" :key=\"index\"\n                         :label=\"themeInfo.value\" :value=\"themeInfo.value\">\n              </mu-option>\n            </mu-select>\n          </div>\n\n          <mu-button slot=\"actions\" flat color=\"secondary\" @click=\"shouldOpenThemeColorSetExportDialog = false\">Cancel</mu-button>\n          <mu-button slot=\"actions\" flat class=\"secondary-theme-text-color\"\n                     :disabled=\"!themeNameToExport\" @click=\"onExportThemeColorSet\">Export</mu-button>\n        </mu-dialog>\n\n\n        <mu-dialog :title=\"$t($i18nTags.settings.delete_theme_color_set)\" :open.sync=\"shouldOpenThemeDeleteDialog\">\n          <div class=\"setting-row select-row dialog-setting-row\">\n            <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.choose_theme)}}</span>\n            <mu-select class=\"setting-select\" v-model=\"themeNameToDelete\">\n              <mu-option v-for=\"(themeInfo, index) in customThemeOptions\" :key=\"index\"\n                         :label=\"themeInfo.value\" :value=\"themeInfo.value\">\n              </mu-option>\n            </mu-select>\n          </div>\n\n          <mu-button slot=\"actions\" flat color=\"secondary\" @click=\"shouldOpenThemeDeleteDialog = false\">Cancel</mu-button>\n          <mu-button slot=\"actions\" flat class=\"secondary-theme-text-color\"\n                     :disabled=\"!themeNameToDelete\" @click=\"onDeleteThemeColorSet\">Delete</mu-button>\n        </mu-dialog>\n\n\n        <div class=\"setting-row select-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.choose_language)}}</span>\n          <mu-select class=\"setting-select\" v-model=\"locale\">\n            <mu-option v-for=\"(localeInfo, index) in localesOptions\" :key=\"index\"\n                       :label=\"localeInfo.label\" :value=\"localeInfo.value\" />\n          </mu-select>\n        </div>\n\n        <p class=\"card-label\">{{$t($i18nTags.settings.stream_label)}}</p>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.auto_load_new_status)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"realTimeLoadStatusMode\" />\n        </div>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.use_multi_line_mode)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"multiLineMode\" />\n        </div>\n\n        <div class=\"setting-row select-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.maximum_number_of_columns_in_multi_line_mode)}}</span>\n          <mu-select class=\"setting-select\" v-model=\"maximumNumberOfColumnsInMultiLineMode\">\n            <mu-option v-for=\"(info, index) in maximumColumnsOptions\" :key=\"index\"\n                       :label=\"info.label\" :value=\"info.value\" />\n          </mu-select>\n        </div>\n\n        <!--<p class=\"card-label\">{{$t($i18nTags.settings.media_label)}}</p>-->\n\n        <p class=\"card-label\">{{$t($i18nTags.settings.publishing_label)}}</p>\n\n        <div class=\"setting-row select-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.post_privacy)}}</span>\n          <mu-select class=\"setting-select\" v-model=\"postPrivacy\">\n            <mu-option v-for=\"(visibilityInfo, index) in postPrivacyOptions\" :key=\"index\"\n                       :label=\"visibilityInfo.label\" :value=\"visibilityInfo.value\" />\n          </mu-select>\n        </div>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.post_media_as_sensitive)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"postMediaAsSensitiveMode\" />\n        </div>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.only_mention_target_user)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"onlyMentionTargetUserMode\" />\n        </div>\n\n        <p class=\"card-label\">{{$t($i18nTags.settings.web_label)}}</p>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.show_sensitive_media_files)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"showSensitiveContentMode\" />\n        </div>\n\n        <div class=\"setting-row\">\n          <span class=\"setting-label primary-read-text-color\">{{$t($i18nTags.settings.auto_expand_spoiler_text)}}</span>\n          <mu-switch class=\"setting-switch\" v-model=\"autoExpandSpoilerTextMode\"/>\n        </div>\n\n      </mu-card-actions>\n    </mu-card>\n\n    <input ref=\"importThemeInput\" type=\"file\" @change=\"onImportThemeColorSet\"\n           accept=\".json\"\n           style=\"display: none\"/>\n\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Mutation, Action } from 'vuex-class'\n  import { ThemeNames, I18nLocales, VisibilityTypes } from '@/constant'\n  import * as moment from 'moment'\n  import ThemeManager from '@/themes'\n\n  const ADD_NEW_THEME_OPTION = 'ADD_NEW_THEME_OPTION'\n\n  const presetThemeOptions = [\n    { value: ThemeNames.GOOGLE_PLUS },\n    { value: ThemeNames.DARK },\n    { value: ThemeNames.GREEN_LIGHT },\n    { value: ThemeNames.CUCKOO_HUB },\n  ]\n\n  @Component({})\n  class Setting extends Vue {\n\n    $i18n\n\n    $i18nTags\n\n    $t\n\n    $toast\n\n    $refs: {\n      importThemeInput: HTMLInputElement\n    }\n\n    @State('appStatus') appStatus\n\n    @Mutation('updateTheme') updateTheme\n    @Mutation('updateMultiLineMode') updateMultiLineMode\n    @Mutation('updateMaximumNumberOfColumnsInMultiLineMode') updateMaximumNumberOfColumnsInMultiLineMode\n    @Mutation('updateShowSensitiveContentMode') updateShowSensitiveContentMode\n    @Mutation('updateRealTimeLoadStatusMode') updateRealTimeLoadStatusMode\n    @Mutation('updateLocale') updateLocale\n    @Mutation('updateOnlyMentionTargetUserMode') updateOnlyMentionTargetUserMode\n    @Mutation('updateAutoExpandSpoilerTextMode') updateAutoExpandSpoilerTextMode\n    @Mutation('updateIsEditingThemeMode') updateIsEditingThemeMode\n    @Mutation('updateShouldShowThemeEditPanel') updateShouldShowThemeEditPanel\n\n    @Mutation('updatePostPrivacy') mutationUpdatePostPrivacy\n\n    @Action('updatePostPrivacy') actionUpdatePostPrivacy\n    @Action('updatePostMediaAsSensitiveMode') updatePostMediaAsSensitiveMode\n\n    isLoading: boolean = false\n\n    shouldOpenThemeColorSetExportDialog: boolean = false\n\n    shouldOpenThemeDeleteDialog: boolean = false\n\n    themeNameToExport = ''\n\n    themeNameToDelete = ''\n\n    shouldUpdateThemeOptions = 1\n\n    get themeOptions () {\n      this.themeName\n      return ThemeManager.getThemeOptionsList()\n    }\n\n    get customThemeOptions () {\n      this.themeName\n      return ThemeManager.getCustomThemeOptionsList()\n    }\n\n    localesOptions = [\n      { label: 'English', value: I18nLocales.EN },\n      { label: 'Deutsch', value: I18nLocales.DE },\n      { label: '日本語', value: I18nLocales.JA },\n      { label: '简体中文', value: I18nLocales.ZH_CN },\n      { label: '繁體中文（香港）', value: I18nLocales.ZH_HK },\n      { label: '繁體中文（台灣）', value: I18nLocales.ZH_TW }\n    ]\n\n    maximumColumnsOptions = [\n      { label: '2', value: 2 },\n      { label: '3', value: 3 },\n      { label: '4', value: 4 },\n      { label: '5', value: 5 },\n      { label: '6', value: 6 }\n    ]\n\n    showSuccessChangedToast () {\n      this.$toast.success(this.$t(this.$i18nTags.settings.changes_successfully_saved))\n    }\n\n    get postPrivacyOptions () {\n      return [VisibilityTypes.PUBLIC, VisibilityTypes.UNLISTED, VisibilityTypes.PRIVATE].map(visibilityType => {\n        return { label: this.$t(visibilityType), value: visibilityType }\n      })\n    }\n\n    get themeName (): string {\n      return this.appStatus.settings.theme\n    }\n\n    set themeName (val) {\n      this.updateTheme(val)\n      ThemeManager.setTheme(val)\n    }\n\n    get locale () {\n      return this.appStatus.settings.locale\n    }\n\n    set locale (val) {\n      this.$i18n.locale = val\n      moment.locale(val)\n      this.updateLocale(val)\n    }\n\n    get multiLineMode () {\n      return this.appStatus.settings.multiLineMode\n    }\n\n    set multiLineMode (val) {\n      this.updateMultiLineMode(val)\n    }\n\n    get maximumNumberOfColumnsInMultiLineMode () {\n      return this.appStatus.settings.maximumNumberOfColumnsInMultiLineMode\n    }\n\n    set maximumNumberOfColumnsInMultiLineMode (val) {\n      this.updateMaximumNumberOfColumnsInMultiLineMode(val)\n    }\n\n    get showSensitiveContentMode () {\n      return this.appStatus.settings.showSensitiveContentMode\n    }\n\n    set showSensitiveContentMode (val) {\n      this.updateShowSensitiveContentMode(val)\n    }\n\n    get realTimeLoadStatusMode () {\n      return this.appStatus.settings.realTimeLoadStatusMode\n    }\n\n    set realTimeLoadStatusMode (val) {\n      this.updateRealTimeLoadStatusMode(val)\n    }\n\n    get postPrivacy () {\n      return this.appStatus.settings.postPrivacy\n    }\n\n    set postPrivacy (val) {\n      // todo muse-select has a bug, if only use updatePostPrivacy action here, select component will re-open after action complete\n      this.mutationUpdatePostPrivacy(val)\n      this._updatePostPrivacy(val)\n    }\n\n    async _updatePostPrivacy (val) {\n      this.isLoading = true\n      await this.actionUpdatePostPrivacy(val)\n      this.isLoading = false\n      this.showSuccessChangedToast()\n    }\n\n    get postMediaAsSensitiveMode () {\n      return this.appStatus.settings.postMediaAsSensitiveMode\n    }\n\n    set postMediaAsSensitiveMode (val) {\n      this._updatePostMediaAsSensitiveMode(val)\n    }\n\n    async _updatePostMediaAsSensitiveMode (val) {\n      this.isLoading = true\n      await this.updatePostMediaAsSensitiveMode(val)\n      this.isLoading = false\n      this.showSuccessChangedToast()\n    }\n\n    get onlyMentionTargetUserMode () {\n      return this.appStatus.settings.onlyMentionTargetUserMode\n    }\n\n    set onlyMentionTargetUserMode (val) {\n      this.updateOnlyMentionTargetUserMode(val)\n    }\n\n    get autoExpandSpoilerTextMode () {\n      return this.appStatus.settings.autoExpandSpoilerTextMode\n    }\n\n    set autoExpandSpoilerTextMode (val) {\n      this.updateAutoExpandSpoilerTextMode(val)\n    }\n\n    @Watch('shouldOpenThemeColorSetExportDialog')\n    onShouldOpenThemeColorSetExportDialogChanged () {\n      if (this.shouldOpenThemeColorSetExportDialog) {\n        this.themeNameToExport = this.themeName\n      }\n    }\n\n    @Watch('shouldOpenThemeDeleteDialog')\n    onShouldOpenThemeDeleteDialogChanged () {\n      if (this.shouldOpenThemeDeleteDialog) {\n        this.themeNameToDelete = this.customThemeOptions[0].value\n      }\n    }\n\n    onShowEditThemePanel () {\n      if (this.appStatus.isEditingThemeMode) {\n        this.updateShouldShowThemeEditPanel(true)\n      } else {\n        this.updateIsEditingThemeMode(true)\n      }\n    }\n\n    onExportThemeColorSet () {\n      ThemeManager.exportTheme(this.themeNameToExport)\n      this.shouldOpenThemeColorSetExportDialog = false\n    }\n\n    onOpenDeleteThemeColorSetPanel () {\n      if (!this.customThemeOptions.length) {\n        return this.$toast.warning('There is no custom theme to delete')\n      }\n\n      this.shouldOpenThemeDeleteDialog = true\n    }\n\n    onDeleteThemeColorSet () {\n      ThemeManager.deleteTheme(this.themeNameToDelete)\n      if (this.themeNameToDelete === this.themeName) {\n        this.themeName = ThemeNames.GOOGLE_PLUS\n      }\n      this.shouldUpdateThemeOptions = this.shouldUpdateThemeOptions + 1\n      this.shouldOpenThemeDeleteDialog = false\n    }\n\n    onSelectThemeColorSetFile () {\n      this.$refs.importThemeInput.click()\n    }\n\n    onImportThemeColorSet () {\n      const file = this.$refs.importThemeInput.files[0]\n      const fileName = file.name.replace(/.json$/, '')\n\n      if (this.themeOptions.find(opt => opt.value === fileName)) {\n        return this.$toast.warning('Same name theme conflicts')\n      }\n\n      const fileReader = new FileReader()\n      fileReader.readAsText(file)\n\n      fileReader.onload = e => {\n        if((<FileReader>e.target).readyState !== 2) return\n        if((<FileReader>e.target).error) return\n\n        const themeColorSet = JSON.parse((<FileReader>e.target).result)\n        ThemeManager.importTheme(themeColorSet, fileName)\n        this.shouldUpdateThemeOptions = this.shouldUpdateThemeOptions + 1\n        this.themeName = fileName\n      }\n\n    }\n\n    mounted () {\n      if (!this.themeOptions.find(opt => opt.value === this.themeName)) {\n        this.themeName = ThemeNames.GOOGLE_PLUS\n      }\n    }\n  }\n\n  export default Setting\n</script>\n\n<style lang=\"less\" scoped>\n  .setting-page-container {\n    max-width: 600px;\n    min-width: 320px;\n    margin: 0 auto;\n\n    p {\n      margin: 0;\n    }\n\n    .setting-card {\n      padding: 10px;\n\n      .card-label {\n        font-size: 16px;\n        font-weight: bold;\n        margin-top: 5px;\n      }\n\n    }\n  }\n\n  .setting-row {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    margin: 10px 0;\n\n    .setting-label {\n      font-size: 13px;\n    }\n\n    .setting-switch {\n      margin-right: 12px;\n    }\n\n    .setting-input {\n      min-height: unset;\n      margin: 0;\n      padding: 0;\n    }\n\n    &.dialog-setting-row {\n      .setting-label {\n        margin-right: 20px;\n      }\n    }\n  }\n\n  .foot-note {\n    text-align: right;\n    margin-right: 12px;\n    margin-top: -6px;\n\n    span {\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n\n  }\n\n  .select-row {\n    .setting-select {\n      width: 170px;\n      padding: 0;\n      margin: 0;\n      min-height: unset;\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/pages/Statuses.vue",
    "content": "<template>\n  <div class=\"statuses-page-container\" v-loading=\"!status\">\n    <status-card class=\"status-card-container\" v-if=\"status\" :status=\"status\" />\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Action } from 'vuex-class'\n  import { mastodonentities } from '@/interface'\n  import StatusCard from '@/components/StatusCard'\n\n  @Component({\n    components: {\n      'status-card': StatusCard,\n    }\n  })\n  class Statuses extends Vue {\n\n    $route\n\n    $progress\n\n    @Action('fetchStatusById') fetchStatusById\n\n    @State('statusMap') statusMap: {\n      [statusId: string]: mastodonentities.Status\n    }\n\n    @Watch('$route')\n    onRouteChanged () {\n      this.fetchTargetStatus()\n    }\n\n    get status (): mastodonentities.Status {\n      return this.statusMap[this.$route.params.statusId]\n    }\n\n    mounted () {\n      this.fetchTargetStatus()\n    }\n\n    async fetchTargetStatus () {\n      this.$progress.start()\n      await this.fetchStatusById(this.$route.params.statusId)\n      this.$progress.done()\n    }\n  }\n\n  export default Statuses\n</script>\n\n<style lang=\"less\" scoped>\n  .statuses-page-container {\n    max-width: 530px;\n    padding-top: 8px;\n    margin: 0 auto 40px;\n\n    .status-card-container {\n      @media (max-width: 530px) {\n        height: 100%;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/pages/Timelines/NewStatusNoticeButton.vue",
    "content": "<template>\n  <mu-button v-if=\"!appStatus.settings.realTimeLoadStatusMode\" v-show=\"currentTimeLineStreamPool.length\"\n             class=\"new-status-notice-button\" round color=\"primary\" @click=\"onNoticeButtonClick\" :style=\"buttonStyle\">\n    <svg style=\"margin-left: 6px\" width=\"18px\" height=\"18px\" viewBox=\"0 0 48 48\" fill=\"#fff\">\n      <path fill=\"none\" d=\"M0 0h48v48H0V0z\"></path>\n      <path d=\"M8 24l2.83 2.83L22 15.66V40h4V15.66l11.17 11.17L40 24 24 8 8 24z\"></path>\n    </svg>\n    {{$tc($i18nTags.timeLines.new_message_notice, currentTimeLineStreamPool.length, { count: currentTimeLineStreamPool.length })}}\n  </mu-button>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { State, Mutation, Action } from 'vuex-class'\n  import { animatedScrollTo } from '@/util'\n  import { getTimeLineTypeAndHashName, isBaseTimeLine, getTargetStatusesList } from '@/util'\n\n  (window as any).animatedScrollTo = animatedScrollTo\n\n  @Component({})\n  class NewStatusNoticeButton extends Vue {\n\n    $route\n\n    @State('appStatus') appStatus\n\n    @State('statusMap') statusMap\n\n    @Mutation('unShiftTimeLineStatuses') unShiftTimeLineStatuses\n\n    @Action('loadStreamStatusesPool') loadStreamStatusesPool\n\n    translateY: number = 0\n\n    get currentTimeLineStreamPool () {\n      const { timeLineType, hashName } = getTimeLineTypeAndHashName(this.$route)\n\n      if (timeLineType === '') return []\n\n      const targetStreamPool = getTargetStatusesList(this.appStatus.streamStatusesPool, timeLineType, hashName)\n\n      // filter root status\n      return targetStreamPool.filter(id => this.statusMap[id] && !this.statusMap[id].in_reply_to_id)\n    }\n\n    get buttonStyle () {\n      return { transform: `translate(-50%, ${this.translateY}px)` }\n    }\n\n    mounted () {\n      this.initWindowScrollEvent()\n    }\n\n    initWindowScrollEvent () {\n      let preScrollY = window.scrollY\n\n      const minTranslateY = -110\n      const maxTranslateY = 0\n\n      window.addEventListener('scroll', () => {\n        if (!this.currentTimeLineStreamPool.length) return\n\n        if (this.translateY >= minTranslateY && this.translateY <= maxTranslateY) {\n          this.translateY -= window.scrollY - preScrollY\n\n          if (this.translateY < minTranslateY) this.translateY = minTranslateY\n          if (this.translateY > maxTranslateY) this.translateY = maxTranslateY\n        }\n\n        preScrollY = window.scrollY\n\n      }, { passive: true })\n    }\n\n    onNoticeButtonClick () {\n      animatedScrollTo(document.querySelector('html'), 0, 400, () => {\n        this.loadStreamStatusesPool({ ...getTimeLineTypeAndHashName(this.$route) })\n      })\n    }\n  }\n\n  export default NewStatusNoticeButton\n</script>\n\n<style lang=\"less\" scoped>\n\n</style>\n"
  },
  {
    "path": "src/pages/Timelines/PostStatusStampCard.vue",
    "content": "<template>\n  <mu-card @click=\"onStampCardClick\" class=\"post-status-stamp-card\">\n    <div class=\"left-area\">\n      <mu-avatar @click.stop=\"onCheckUserAccountPage\" size=\"36\">\n        <img :src=\"currentUserAccount.avatar\">\n      </mu-avatar>\n\n      <div class=\"post-new-status-hint placeholder-read-text-color\">\n        {{$t($i18nTags.timeLines.whats_new_with_you)}}\n      </div>\n    </div>\n\n    <div class=\"right-area\">\n      <mu-button @click.stop=\"onUploadMedia\" class=\"circle-btn unset-display\" icon>\n        <mu-icon class=\"camera-icon\" value=\"camera_alt\" />\n      </mu-button>\n    </div>\n  </mu-card>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component } from 'vue-property-decorator'\n  import { State } from 'vuex-class'\n  import { mastodonentities } from '@/interface'\n\n  @Component({})\n  class PostStatusStampCard extends Vue {\n\n    @State('currentUserAccount') currentUserAccount: mastodonentities.AuthenticatedAccount\n\n    onCheckUserAccountPage () {\n      window.open(this.currentUserAccount.url, \"_blank\")\n    }\n\n    onStampCardClick () {\n      this.$emit('click')\n    }\n\n    onUploadMedia () {\n\n    }\n  }\n\n  export default PostStatusStampCard\n</script>\n\n<style lang=\"less\" scoped>\n  .post-status-stamp-card {\n    width: 100%;\n    height: 68px;\n    opacity: 0;\n    padding: 16px;\n    cursor: pointer;\n\n    display: flex;\n    justify-content: space-between;\n\n    .left-area {\n      display: flex;\n      justify-content: center;\n      align-items: center;\n\n      .post-new-status-hint {\n        font-size: 16px;\n        cursor: text;\n        margin-left: 8px;\n        flex-grow: 1;\n      }\n    }\n\n\n    .circle-btn {\n      margin: 0;\n\n      .camera-icon {\n        font-size: 18px;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "src/pages/Timelines/index.vue",
    "content": "<template>\n  <div class=\"timelines-container\" ref=\"timelinesContainer\" v-loading=\"isInitLoading\">\n\n    <template v-for=\"(timeLineName, index) in allTimeLineNameList\">\n      <mu-load-more :key=\"index\" @load=\"loadStatuses(true)\" v-show=\"isTimeLineNameEqualCurrentRoute(timeLineName)\"\n                    :loading=\"!isInitLoading && isLoading\" loading-text=\"\">\n        <div v-masonry-container :style=\"statusCardsContainerStyle\" class=\"status-cards-container\">\n\n          <post-status-stamp-card @click=\"showNewPostDialogPanel\"\n            class=\"status-card-container\" :style=\"[statusCardStyle,\n             isTimeLineNameEqualCurrentRoute(timeLineName) && currentFocusCardId === '-1' && cardFocusStyle]\" />\n\n          <template v-for=\"status in getRootStatuses(timeLineName.split('/')[0], timeLineName.split('/')[1])\">\n            <status-card v-masonry-item class=\"status-card-container\" :ref=\"`${timeLineName}_statusCard_${status.id}`\"\n                         @statusCardFocus=\"onStatusCardFocus(status.id)\"\n                         :shouldCollapseContent=\"true\"\n                         :key=\"status.id\" :status=\"status\" :style=\"[statusCardStyle,\n                         isTimeLineNameEqualCurrentRoute(timeLineName) &&\n                         currentFocusCardId === status.id && cardFocusStyle]\"/>\n          </template>\n\n          <p class=\"no-more-status-notice secondary-read-text-color\"\n             v-if=\"currentTimeLineCannotLoadMore && (count === waterfallLineCount) \">\n            {{$t($i18nTags.timeLines.no_load_more_status_notice)}}\n          </p>\n\n        </div>\n      </mu-load-more>\n    </template>\n\n    <!-- todo move those widgets to a common area -->\n    <mu-snackbar position=\"top\" color=\"info\" :open.sync=\"isSnackBarOpening\">\n      <mu-icon left value=\"info\"></mu-icon>\n      {{snackBarMessage}}\n      <mu-button flat slot=\"action\" color=\"#fff\" @click=\"isSnackBarOpening = false\">Close</mu-button>\n    </mu-snackbar>\n\n    <mu-button fab class=\"post-new-status-button\" color=\"primary\" v-show=\"!isPostStatusDialogOpening\"\n               @click=\"showNewPostDialogPanel\">\n      <mu-icon value=\"edit\" />\n    </mu-button>\n\n    <post-status-dialog :open.sync=\"isPostStatusDialogOpening\"/>\n\n    <new-status-notice-button />\n  </div>\n</template>\n\n<script lang=\"ts\">\n  import { Vue, Component, Watch } from 'vue-property-decorator'\n  import { Action, State, Getter } from 'vuex-class'\n  import { TimeLineTypes, UiWidthCheckConstants, ThemeNames } from '@/constant'\n  import { mastodonentities } from '@/interface'\n  import { getTimeLineTypeAndHashName, isBaseTimeLine, animatedScrollTo, documentGlobalEventBus } from '@/util'\n  import StatusCard from '@/components/StatusCard'\n  import PostStatusDialog from '@/components/PostStatusDialog'\n  import NewStatusNoticeButton from './NewStatusNoticeButton'\n  import PostStatusStampCard from './PostStatusStampCard'\n\n  const noneCardFocusId = '-2'\n  const stampCardFocusId = '-1'\n\n  const autoFocusScrollPadding = 24\n  const headerHeight = 64\n\n  const timelineInitStatusMap = {}\n\n  function hasCurrentTimeLineInit ($route) {\n    const { timeLineType, hashName } = getTimeLineTypeAndHashName($route)\n\n    const key = hashName ? `${timeLineType}/${hashName}` : timeLineType\n    return timelineInitStatusMap[key]\n  }\n\n  function setCurrentTimeLineHasInit ($route) {\n    const { timeLineType, hashName } = getTimeLineTypeAndHashName($route)\n\n    const key = hashName ? `${timeLineType}/${hashName}` : timeLineType\n    timelineInitStatusMap[key] = true\n  }\n\n  function getFitStatusWidth (containerWidth, lineCount): number {\n    return (containerWidth - (lineCount - 1) * UiWidthCheckConstants.TIMELINE_WATER_FALL_GUTTER) / lineCount\n  }\n\n  function calcFitWaterFallLineCount (containerWidth: number, testLineCount: number) {\n    if (testLineCount <= 1) return 1\n\n    const testStatusCardWidth = getFitStatusWidth(containerWidth, testLineCount)\n\n    if (testStatusCardWidth > UiWidthCheckConstants.STATUS_CARD_MIN_WIDTH) {\n      return testLineCount\n    } else {\n      return calcFitWaterFallLineCount(containerWidth, testLineCount - 1)\n    }\n  }\n\n  @Component({\n    components: {\n      'status-card': StatusCard,\n      'post-status-dialog': PostStatusDialog,\n      'new-status-notice-button': NewStatusNoticeButton,\n      'post-status-stamp-card': PostStatusStampCard\n    }\n  })\n  class TimeLines extends Vue {\n\n    $refs: {\n      timelinesContainer: HTMLDivElement\n    }\n\n    $route;\n\n    $progress;\n\n    @State('appStatus') appStatus\n\n    @State('timelines') timelines\n\n    @Getter('getRootStatuses') getRootStatuses\n\n    @Action('updateTimeLineStatuses') updateTimeLineStatuses\n\n    @Action('loadStreamStatusesPool') loadStreamStatusesPool\n\n    /**\n     * @description 这种loading应该是全屏白色遮罩\n     **/\n    isInitLoading: boolean = false\n\n    /**\n     * @description 这种则只需要转圈就行\n     * */\n    isLoading: boolean = false\n\n    noLoadMoreTimeLineList: Array<string> = []\n\n    isSnackBarOpening: boolean = false\n\n    snackBarMessage: string = ''\n\n    isPostStatusDialogOpening: boolean = false\n\n    currentFocusCardId: string = noneCardFocusId\n\n    get cardFocusStyle () {\n      const darkThemeList = [ ThemeNames.DARK, ThemeNames.CUCKOO_HUB ]\n      const shadowBaseColor = darkThemeList.indexOf(this.appStatus.settings.theme) !== -1 ? 255 : 0\n      return {\n        'box-shadow': `0 0 20px rgba(${shadowBaseColor}, ${shadowBaseColor}, ${shadowBaseColor},0.3)`\n      }\n    }\n\n    get allTimeLineNameList (): Array<string> {\n      const result = [\n        TimeLineTypes.HOME, TimeLineTypes.PUBLIC, TimeLineTypes.LOCAL\n      ];\n      [TimeLineTypes.TAG, TimeLineTypes.LIST].forEach(secondType => {\n        Object.keys(this.timelines[secondType]).forEach(hashName => {\n          if (Array.isArray(this.timelines[secondType][hashName])) {\n            result.push(`${secondType}/${hashName}`)\n          }\n        })\n      })\n\n      return result\n    }\n\n    get isCurrentTimeLineRoute () {\n      // todo use meta?\n      return this.$route.path.startsWith('/timelines/')\n    }\n\n    get currentRootStatuses (): Array<mastodonentities.Status> {\n      if (!this.isCurrentTimeLineRoute) return\n\n      const { timeLineType, hashName } = getTimeLineTypeAndHashName(this.$route)\n\n      return this.getRootStatuses(timeLineType, hashName).filter(status => status.id)\n    }\n\n    get currentTimeLineCannotLoadMore () {\n      if (!this.isCurrentTimeLineRoute) return\n\n      const { timeLineType, hashName } = getTimeLineTypeAndHashName(this.$route)\n\n      return this.noLoadMoreTimeLineList.indexOf(`${timeLineType}/${hashName}`) !== -1\n    }\n\n    get contentAreaWidth (): number {\n      return this.appStatus.documentWidth - UiWidthCheckConstants.DRAWER_DESKTOP_WIDTH\n    }\n\n    @Watch('$route')\n    async onRouteChanged () {\n      if (!this.isCurrentTimeLineRoute) return\n\n      this.currentFocusCardId = noneCardFocusId\n\n      if (!hasCurrentTimeLineInit(this.$route)) {\n        this.currentRootStatuses.length ? this.$progress.start() : this.isInitLoading = true\n        await this.loadStatuses()\n        this.$progress.done()\n        this.isInitLoading = false\n        setCurrentTimeLineHasInit(this.$route)\n      } else {\n        this.loadStreamStatusesPool({...getTimeLineTypeAndHashName(this.$route)})\n        this.loadStatuses(false, true)\n      }\n\n    }\n\n    @Watch('currentRootStatuses')\n    onCurrentRootStatusesChanged () {\n      this.$nextTick(async () => {\n        // load more to show scroll\n        // todo maybe we could find a better way to serve this?\n        // if (this.$refs.timelinesContainer.clientHeight < window.screen.availHeight) {\n        //   this.isInitLoading = true\n        //   this.isLoading = false\n        //   await this.loadStatuses(true)\n        //   this.isInitLoading = false\n        // }\n      })\n    }\n\n    async mounted () {\n      this.onRouteChanged()\n      // @todo 可能存在重复绑定事件问题\n      documentGlobalEventBus.on('keydown', e => this.onTimeLinePageKeyDown(e), true)\n    }\n\n    async loadStatuses (isLoadMore: boolean = false, isFetchMore: boolean = false) {\n\n      if (isLoadMore && this.currentTimeLineCannotLoadMore) return\n\n      if (!this.isCurrentTimeLineRoute) return\n\n      if (this.isLoading) return\n\n      this.isLoading = true\n      this.$progress.start()\n\n      const preStatusesLength = this.currentRootStatuses.length\n      const { timeLineType, hashName } = getTimeLineTypeAndHashName(this.$route)\n      await this.updateTimeLineStatuses({\n        isLoadMore,\n        isFetchMore,\n        timeLineType,\n        hashName\n      })\n\n      const newStatusesLength = this.currentRootStatuses.length\n\n      if (isLoadMore && (preStatusesLength === newStatusesLength)) {\n        this.noLoadMoreTimeLineList.push(`${timeLineType}/${hashName}`)\n      }\n\n      this.$progress.done()\n      this.isLoading = false\n    }\n\n    showNewPostDialogPanel () {\n      // todo handle history.back() event\n      // use vue router?\n      this.isPostStatusDialogOpening = true\n    }\n\n    isTimeLineNameEqualCurrentRoute (timeLineName: string): boolean {\n      const { timeLineType, hashName } = getTimeLineTypeAndHashName(this.$route)\n\n      if (isBaseTimeLine(timeLineName)) {\n        return timeLineType === timeLineName\n      } else {\n        return `${timeLineType}/${hashName}` === timeLineName\n      }\n    }\n\n    get waterfallLineCount () {\n      if (!this.appStatus.settings.multiLineMode) return 1\n\n      return calcFitWaterFallLineCount(this.contentAreaWidth - UiWidthCheckConstants.TIMELINE_WATER_FALL_GUTTER * 2, this.appStatus.settings.maximumNumberOfColumnsInMultiLineMode)\n    }\n\n    get statusCardsContainerStyle () {\n      if (this.waterfallLineCount === 1) {\n        return {\n          maxWidth: `${UiWidthCheckConstants.STATUS_CARD_MAX_WIDTH}px`\n        }\n      } else {\n        const width = this.statusCardMultiLineFinalWidth * this.waterfallLineCount +\n          UiWidthCheckConstants.TIMELINE_WATER_FALL_GUTTER * (this.waterfallLineCount - 1)\n\n        return { width: `${width}px` }\n      }\n    }\n\n    get statusCardMultiLineFinalWidth () {\n      let fitWidth = getFitStatusWidth(this.contentAreaWidth - UiWidthCheckConstants.TIMELINE_WATER_FALL_GUTTER * 2, this.waterfallLineCount)\n\n      if (fitWidth > UiWidthCheckConstants.STATUS_CARD_MAX_WIDTH) fitWidth = UiWidthCheckConstants.STATUS_CARD_MAX_WIDTH\n\n      return fitWidth\n    }\n\n    get statusCardStyle () {\n      if (this.waterfallLineCount === 1) return null\n\n      return {\n        width: `${this.statusCardMultiLineFinalWidth}px`\n      }\n    }\n\n    onTimeLinePageKeyDown (e: KeyboardEvent) {\n      if (!this.isCurrentTimeLineRoute || this.isPostStatusDialogOpening) return\n\n      const knownKeyList = ['j', 'k', 'Enter']\n      if (knownKeyList.indexOf(e.key) === -1) return\n\n      e.stopPropagation()\n      e.preventDefault()\n\n      switch (e.key) {\n        case 'j': {\n          return this.onFocusNextCard()\n        }\n\n        case 'k': {\n          return this.onFocusPreviousCard()\n        }\n\n        case 'Enter': {\n          return this.activeCard()\n        }\n      }\n    }\n\n    onFocusNextCard () {\n      if (this.currentFocusCardId === this.currentRootStatuses[this.currentRootStatuses.length - 1].id) return\n\n      if (this.currentFocusCardId === noneCardFocusId) {\n        this.currentFocusCardId = stampCardFocusId\n      } else if (this.currentFocusCardId === stampCardFocusId) {\n        this.currentFocusCardId = this.currentRootStatuses[0].id\n      } else {\n        const currentIndex = this.currentRootStatuses\n          .findIndex(status => status.id === this.currentFocusCardId)\n\n        if (currentIndex === -1) return console.error('what the f?')\n\n        this.currentFocusCardId = this.currentRootStatuses[currentIndex + 1].id\n      }\n\n      this.showTargetCardInViewPort(true)\n    }\n\n    onFocusPreviousCard () {\n      if (this.currentFocusCardId === noneCardFocusId) return\n\n      if (this.currentFocusCardId === stampCardFocusId) {\n        this.currentFocusCardId = noneCardFocusId\n      } else {\n        const currentIndex = this.currentRootStatuses\n          .findIndex(status => status.id === this.currentFocusCardId)\n\n        if (currentIndex === -1) return console.error('what the f?')\n\n        if (currentIndex === 0) {\n          this.currentFocusCardId = stampCardFocusId\n        } else {\n          this.currentFocusCardId = this.currentRootStatuses[currentIndex - 1].id\n        }\n      }\n\n      this.showTargetCardInViewPort(false)\n    }\n\n    activeCard () {\n      if (this.currentFocusCardId === noneCardFocusId) return\n\n      if (this.currentFocusCardId === stampCardFocusId) {\n        return this.isPostStatusDialogOpening = true\n      }\n\n      const { timeLineType } = getTimeLineTypeAndHashName(this.$route)\n      const targetStatusCard = this.$refs[`${timeLineType}_statusCard_${this.currentFocusCardId}`][0]\n      targetStatusCard.onReplyToStatus(targetStatusCard.status)\n    }\n\n    onStatusCardFocus (statusId: string) {\n      this.currentFocusCardId = statusId\n    }\n\n    showTargetCardInViewPort (toNext: boolean) {\n      let scrollToTarget = null\n      const $html = document.querySelector('html')\n\n      if (this.currentFocusCardId === stampCardFocusId || this.currentFocusCardId === noneCardFocusId) {\n        scrollToTarget = 0\n      } else {\n\n        const { timeLineType } = getTimeLineTypeAndHashName(this.$route)\n        const $targetStatusCardRef: HTMLDivElement = this.$refs[`${timeLineType}_statusCard_${this.currentFocusCardId}`][0].$el\n\n        const bounding = $targetStatusCardRef.getBoundingClientRect()\n\n        const topDistance = bounding.top - autoFocusScrollPadding - headerHeight\n        const bottomDistance = bounding.bottom + autoFocusScrollPadding - window.innerHeight\n\n        if (topDistance < 0) {\n          scrollToTarget = $html.scrollTop + topDistance\n        } else if (toNext && (bottomDistance > 0)) {\n          if (bottomDistance > 0) {\n            scrollToTarget = $html.scrollTop + bottomDistance\n          }\n\n          if (this.currentFocusCardId === this.currentRootStatuses[this.currentRootStatuses.length - 1].id) {\n            scrollToTarget = $html.scrollHeight\n          }\n        }\n\n      }\n\n      if (scrollToTarget !== null) {\n        animatedScrollTo($html, scrollToTarget, 400)\n      }\n\n    }\n  }\n\n  export default TimeLines\n</script>\n\n<style lang=\"less\" scoped>\n  .timelines-container {\n\n    .post-new-status-button {\n      position: fixed;\n      right: 16px;\n      bottom: 16px;\n\n      @media (min-width: 768px) {\n        right: 32px;\n        bottom: 32px;\n      }\n    }\n\n    .status-cards-container {\n      margin: 0 auto;\n      padding-top: 24px;\n\n      .new-status-stamp {\n        height: 60px;\n      }\n\n      .status-card-container, .no-more-status-notice {\n        max-width: 530px;\n        margin: 0 auto 24px;\n        break-inside: avoid;\n        box-sizing: border-box;\n      }\n\n      .no-more-status-notice {\n        text-align: center;\n      }\n\n    }\n\n    .new-status-notice-button {\n      position: fixed;\n      top: 70px;\n      left: 50%;\n    }\n\n  }\n</style>\n"
  },
  {
    "path": "src/router/index.ts",
    "content": "import { TimeLineTypes } from \"../constant\";\n\nconst Loading = require('muse-ui-loading').default\nimport Vue from 'vue'\nimport Router, { Route } from 'vue-router'\nimport store from '../store'\nimport { RoutersInfo } from '@/constant'\nimport * as Api from '@/api'\nimport { isBaseTimeLine } from '@/util'\n\nimport TimeLinesPage from '@/pages/Timelines'\nimport OAuthPage from '@/pages/OAuth'\nimport StatusesPage from '@/pages/Statuses'\nimport Settings from '@/pages/Settings'\nimport AccountsPage from '@/pages/Accounts'\n\nVue.use(Router)\n\nconst homePath = '/timelines/home'\nconst localPath = '/timelines/local'\nconst publicPath = '/timelines/public'\n\nconst router = new Router({\n  routes: [\n\n    {\n      path: RoutersInfo.empty.path,\n      redirect: homePath\n    },\n\n    {\n      path: RoutersInfo.timelines.path,\n      redirect: homePath\n    },\n\n    {\n      path: RoutersInfo.accounts.path,\n      name: RoutersInfo.accounts.name,\n      component: AccountsPage\n    },\n\n    {\n      path: RoutersInfo.statuses.path,\n      name: RoutersInfo.statuses.name,\n      component: StatusesPage\n    },\n\n    {\n      path: RoutersInfo.timelines.path,\n      name: RoutersInfo.timelines.name,\n      component: TimeLinesPage,\n      meta: {\n        needOAuth: true\n      },\n      children: [\n        {\n          path: RoutersInfo.defaulttimelines.path,\n          name: RoutersInfo.defaulttimelines.name,\n          meta: {\n            keepAlive: true,\n            needOAuth: true\n          }\n        },\n        {\n          path: RoutersInfo.tagtimelines.path,\n          name: RoutersInfo.tagtimelines.name,\n          meta: {\n            keepAlive: true,\n            needOAuth: true\n          }\n        },\n        {\n          path: RoutersInfo.listtimelines.path,\n          name: RoutersInfo.listtimelines.name,\n          meta: {\n            keepAlive: true,\n            needOAuth: true\n          }\n        }\n      ]\n    },\n\n    {\n      path: RoutersInfo.oauth.path,\n      name: RoutersInfo.oauth.name,\n      component: OAuthPage,\n      beforeEnter (to, from, next) {\n        if (!checkShouldRegisterApplication(to, from)) {\n          next(RoutersInfo.empty.path)\n        }\n\n        next()\n      },\n      meta: {\n        hideHeader: true,\n        hideDrawer: true\n      }\n    },\n\n    {\n      path: RoutersInfo.settings.path,\n      name: RoutersInfo.settings.name,\n      component: Settings,\n      meta: {\n        needOAuth: true\n      }\n    }\n  ]\n} as any);\n\nfunction checkShouldRegisterApplication (to, from): boolean {\n  // should have clientId/clientSecret/code\n  const { clientId, clientSecret } = store.state.OAuthInfo\n\n  let code = store.state.OAuthInfo.code\n  if (from.path === '/' && !code) {\n    if (location.search.substring(0,6) == \"?code=\") {\n      code = (new RegExp(\"[\\\\?&]code=([^&#]*)\")).exec(location.search)\n      code = code == null ? \"\": decodeURIComponent(code[1]);\n      // todo maybe shouldn't put this here?\n      store.commit('updateOAuthCode', code)\n    }\n  }\n\n  return !(clientId && clientSecret && store.state.mastodonServerUri && code)\n}\n\nconst statusInitManager = new class {\n\n  private hasInitFetchNotifications: boolean = false\n\n  private hasInitStreamConnection: boolean = false\n  private hasInitLocalStreamConnection: boolean = false\n  private hasInitPublicStreamConnection: boolean = false\n\n  private hasUpdateOAuthAccessToken: boolean = false\n\n  private hasUpdateCurrentUserAccount: boolean = false\n\n  private hasUpdateCustomEmojis: boolean = false\n\n  private loadingInstance = null\n\n  private loadingProcessList = []\n\n  private startLoading (process: string) {\n    this.loadingProcessList.push(process)\n    this.loadingInstance = Loading() || this.loadingInstance\n  }\n\n  private endLoading () {\n    if (this.loadingProcessList.every(process => this[process])) {\n      try {\n        this.loadingInstance && this.loadingInstance.close()\n      } catch (e) {\n\n      }\n    }\n  }\n\n  public initFetchNotifications () {\n    if (!store.state.notifications.length && !this.hasInitFetchNotifications) {\n      store.dispatch('updateNotifications')\n      this.hasInitFetchNotifications = true\n    }\n  }\n\n  public initStreamConnection () {\n    if (!this.hasInitStreamConnection) {\n      Api.streaming.openUserConnection()\n      this.hasInitStreamConnection = true\n    }\n  }\n\n  public initLocalStreamConnection () {\n    if (!this.hasInitLocalStreamConnection) {\n      Api.streaming.openLocalConnection()\n      this.hasInitLocalStreamConnection = true\n    }\n  }\n\n  public destroyLocalStreamConnection () {\n    if (this.hasInitLocalStreamConnection) {\n      Api.streaming.closeConnection(TimeLineTypes.LOCAL)\n      this.hasInitLocalStreamConnection = false\n    }\n  }\n\n  public initPublicStreamConnection () {\n    if (!this.hasInitPublicStreamConnection) {\n      Api.streaming.openPublicConnection()\n      this.hasInitPublicStreamConnection = true\n    }\n  }\n\n  public destroyPublicStreamConnection () {\n    if (this.hasInitPublicStreamConnection) {\n      Api.streaming.closeConnection(TimeLineTypes.PUBLIC)\n      this.hasInitPublicStreamConnection = false\n    }\n  }\n\n\n  public async updateCurrentUserAccount () {\n    if (!this.hasUpdateCurrentUserAccount) {\n\n      if (!store.state.currentUserAccount) {\n        this.startLoading('hasUpdateCurrentUserAccount')\n        await store.dispatch('updateCurrentUserAccount')\n      } else {\n        store.dispatch('updateCurrentUserAccount')\n      }\n\n      this.hasUpdateCurrentUserAccount = true\n      this.endLoading()\n    }\n  }\n\n  public async updateOAuthAccessToken () {\n    if (!store.state.OAuthInfo.accessToken && !this.hasUpdateOAuthAccessToken) {\n      this.startLoading('updateOAuthAccessToken')\n      const result = await Api.oauth.fetchOAuthToken()\n      store.commit('updateOAuthAccessToken', result.data.access_token)\n      this.hasUpdateOAuthAccessToken = true\n      this.endLoading()\n    }\n  }\n\n  public async updateCustomEmojis () {\n    if (!this.hasUpdateCustomEmojis) {\n\n      if (!store.state.customEmojis.length) {\n        this.startLoading('hasUpdateCustomEmojis')\n        await store.dispatch('updateCustomEmojis')\n      } else {\n        store.dispatch('updateCustomEmojis')\n      }\n\n      this.hasUpdateCustomEmojis = true\n      this.endLoading()\n    }\n  }\n\n\n}\n\nlet hasUpdateCurrentUserAccount = false\n\nconst beforeEachHooks = {\n  async beforeEachRoute (to, from, next) {\n\n    await statusInitManager.updateCustomEmojis()\n\n    next()\n  },\n\n  // children routes can't use in-router guide...\n  beforeDefaultTimeLines (to: Route, from, next) {\n    if (to.name === RoutersInfo.defaulttimelines.name) {\n      if (!isBaseTimeLine(to.params.timeLineType)) {\n        return next(homePath)\n      }\n    }\n\n    next()\n  },\n\n  async beforeNeedOAuthRoutes (to, from, next) {\n    if (to.meta.needOAuth) {\n\n      // check if need to register\n      if (checkShouldRegisterApplication(to, from)) {\n        store.commit('clearAllOAuthInfo')\n        return next(RoutersInfo.oauth.path)\n      }\n\n      // check if need to get token\n\n      // check if should to be blocked by user fetch\n      try {\n        await statusInitManager.updateOAuthAccessToken()\n        await statusInitManager.updateCurrentUserAccount()\n      } catch (e) {\n        store.commit('clearAllOAuthInfo')\n        return next(RoutersInfo.oauth.path)\n      }\n\n      // should fetch notifications\n      statusInitManager.initFetchNotifications()\n    }\n\n    next()\n  },\n\n  beforeHomeTimeLine (to, from, next) {\n    if (to.path === homePath) {\n      statusInitManager.initStreamConnection()\n    }\n\n    next()\n  },\n\n  beforeLocalTimeLine (to, from, next) {\n    if (to.path === localPath) {\n      statusInitManager.initLocalStreamConnection()\n    }\n\n    next()\n  },\n\n  afterLocalTimeLine (to, from, next) {\n    if (from.path === localPath) {\n      statusInitManager.destroyLocalStreamConnection()\n    }\n\n    next()\n  },\n\n  beforePublicTimeLine (to, from, next) {\n    if (to.path === publicPath) {\n      statusInitManager.initPublicStreamConnection()\n    }\n\n    next()\n  },\n\n  afterPublicTimeLine (to, from, next) {\n    if (from.path === publicPath) {\n      statusInitManager.destroyPublicStreamConnection()\n    }\n\n    next()\n  }\n}\n\nObject.keys(beforeEachHooks).forEach(key => {\n  router.beforeEach(beforeEachHooks[key])\n})\n\nexport default router\n"
  },
  {
    "path": "src/store/actions/accounts.ts",
    "content": "import * as Api from '@/api'\nimport { mastodonentities } from \"@/interface\"\n\nconst accounts = {\n  async followAccountById ({ commit }, id: string) {\n    try {\n      const result = await Api.accounts.followAccountById(id)\n      commit('updateRelationships', { [result.data.id]: result.data })\n    } catch (e) {\n\n    }\n  },\n\n  async unFollowAccountById ({ commit }, id: string) {\n    try {\n      const result = await Api.accounts.unFollowAccountById(id)\n      commit('updateRelationships', { [result.data.id]: result.data })\n    } catch (e) {\n\n    }\n  }\n}\n\nexport default accounts\n"
  },
  {
    "path": "src/store/actions/appstatus.ts",
    "content": "import { getTargetStatusesList } from '@/util'\nimport * as Api from '@/api'\nimport { mastodonentities } from \"@/interface\"\n\nconst appStatus = {\n  loadStreamStatusesPool ({ commit, state }, { timeLineType, hashName }) {\n    const targetStreamPool = getTargetStatusesList(state.appStatus.streamStatusesPool, timeLineType, hashName)\n\n    commit('unShiftTimeLineStatuses', {\n      newStatusIdList: targetStreamPool.filter(id => state.statusMap[id]),\n      timeLineType, hashName\n    })\n\n    commit('clearStreamStatusesPool', { timeLineType, hashName })\n  },\n\n  async updatePostPrivacy ({ commit }, newPrivacy: string) {\n    try {\n      await Api.accounts.updateUserAccountInfo({ source: { privacy: newPrivacy } })\n\n      commit('updatePostPrivacy', newPrivacy)\n    } catch (e) {\n      // todo log error\n      commit('updatePostPrivacy', newPrivacy)\n    }\n  },\n\n  async updatePostMediaAsSensitiveMode ({ commit }, newSensitiveMode: boolean) {\n    try {\n      await Api.accounts.updateUserAccountInfo({ source: { sensitive: newSensitiveMode } })\n\n      commit('updatePostMediaAsSensitiveMode', newSensitiveMode)\n    } catch (e) {\n      // todo log error\n      commit('updatePostMediaAsSensitiveMode', newSensitiveMode)\n    }\n  }\n}\n\nexport default appStatus"
  },
  {
    "path": "src/store/actions/index.ts",
    "content": "import * as Api from '@/api'\nimport statuses from './statuses'\nimport timelines from './timelines'\nimport notifications from './notifications'\nimport appstatus from './appstatus'\nimport relationships from './relationships'\nimport accounts from './accounts'\nimport { mastodonentities } from \"@/interface\"\n\nconst actions = {\n  ...timelines,\n  ...statuses,\n  ...notifications,\n  ...appstatus,\n  ...relationships,\n  ...accounts,\n\n  async updateCurrentUserAccount ({ commit }) {\n    try {\n      const result = await Api.accounts.fetchCurrentUserAccountInfo()\n\n      const accountInfo: mastodonentities.AuthenticatedAccount = result.data\n\n      commit('updateCurrentUserAccount', accountInfo)\n      // sync settings\n      commit('updatePostPrivacy', accountInfo.source.privacy)\n      commit('updatePostMediaAsSensitiveMode', accountInfo.source.sensitive)\n    } catch (e) {\n\n    }\n  },\n\n  async updateCustomEmojis ({ commit }) {\n    try {\n      const result = await Api.instances.getCustomEmojis()\n      commit('updateCustomEmojis', result.data)\n    } catch (e) {\n\n    }\n  }\n}\n\nexport default actions\n"
  },
  {
    "path": "src/store/actions/notifications.ts",
    "content": "import * as api from '@/api'\nimport { NotificationTypes } from '@/constant'\nimport { mastodonentities } from \"@/interface\"\n\nconst notifications = {\n\n  async updateNotifications ({ commit, state, dispatch }, { isLoadMore, isFetchMore } = {\n    isLoadMore: false,\n    isFetchMore: false\n  }) {\n\n    const notifications: Array<mastodonentities.Notification> = state.notifications\n\n    let maxId, sinceId\n    if (isLoadMore) {\n      maxId = notifications[notifications.length - 1].id\n    } else if (isFetchMore) {\n      sinceId = notifications[0] ? notifications[0].id : null\n    }\n\n    let mutationName = ''\n    if (!isLoadMore && !isFetchMore) mutationName = 'pushNotifications'\n    if (isLoadMore && !isFetchMore) mutationName = 'pushNotifications'\n    if (!isLoadMore && isFetchMore) mutationName = 'unShiftNotification'\n\n    try {\n      const result = await api.notifications.getNotifications({ max_id: maxId, since_id: sinceId })\n\n      commit(mutationName, result.data)\n\n      const followNotifications: Array<mastodonentities.Notification> = result.data.filter(notification => notification.type === NotificationTypes.FOLLOW)\n\n      if (followNotifications.length) {\n        dispatch('updateRelationships', { idList: followNotifications.map(notification => notification.account.id) })\n      }\n\n    } catch (e) {\n\n    }\n  }\n}\n\nexport default notifications\n"
  },
  {
    "path": "src/store/actions/relationships.ts",
    "content": "import * as Api from '@/api'\nimport { mastodonentities } from \"@/interface\"\n\nconst relationships = {\n  async updateRelationships ({ commit }, { idList }: { idList: Array<string> }) {\n    try {\n      const result = await Api.accounts.fetchRelationships(idList || [])\n      const relationshipList: Array<mastodonentities.Relationship> = result.data\n\n      const relationshipMap = {}\n      relationshipList.forEach(relationship => {\n        relationshipMap[relationship.id] = relationship\n      })\n\n      commit('updateRelationships', relationshipMap)\n    } catch (e) {\n\n    }\n  }\n}\n\nexport default relationships\n"
  },
  {
    "path": "src/store/actions/statuses.ts",
    "content": "import * as api from '@/api'\nimport { TimeLineTypes } from '@/constant'\n\ninterface postStatusFormData {\n  // The text of the status\n  status: string\n  // local ID of the status you want to reply to\n  inReplyToId?: string\n  // Array of media IDs to attach to the status (maximum 4)\n  mediaIds?: Array<string>\n  // Set this to mark the media of the status as NSFW\n  sensitive?: boolean\n  // Text to be shown as a warning before the actual content\n  spoilerText?: string\n  // Either \"direct\", \"private\", \"unlisted\" or \"public\"\n  visibility?: string\n  // ISO 639-2 language code of the toot, to skip automatic detection\n  language?: string\n}\n\nconst statuses = {\n  async fetchStatusById ({ commit, dispatch }, statusId: string) {\n    try {\n      const result = await api.statuses.getStatusById(statusId)\n      commit('updateStatusMap', { [statusId]: result.data })\n      dispatch('updateContextMap', statusId)\n      dispatch('updateCardMap', statusId)\n    } catch (e) {\n      throw new Error(e)\n    }\n  },\n\n  async updateFavouriteStatusById ({ commit }, { favourited, mainStatusId, targetStatusId }) {\n    try {\n\n      if (favourited) {\n        api.statuses.favouriteStatusById(targetStatusId)\n      } else {\n        api.statuses.unFavouriteStatusById(targetStatusId)\n      }\n\n      commit('updateFavouriteStatusById', { favourited, mainStatusId, targetStatusId })\n    } catch (e) {\n      throw new Error(e)\n    }\n  },\n\n  async updateReblogStatusById ({ commit }, { reblogged, mainStatusId, targetStatusId }) {\n    try {\n      if (reblogged) {\n        api.statuses.reblogStatusById(targetStatusId)\n      } else {\n        api.statuses.unReblogStatusById(targetStatusId)\n      }\n\n      commit('updateReblogStatusById', { reblogged, mainStatusId, targetStatusId })\n    } catch (e) {\n      throw new Error(e)\n    }\n  },\n\n  async updateContextMap ({ commit, dispatch }, statusId: string) {\n    if (!statusId) throw new Error('unknown status id!')\n\n    try {\n      const result = await api.statuses.getStatusContextById(statusId)\n      const ancestors = result.data.ancestors\n      const descendants = result.data.descendants\n\n      commit('updateContextMap', { [statusId]: {\n        ancestors: ancestors.map(status => status.id),\n        descendants: descendants.map(status => status.id) }\n      })\n\n      const newStatusMap = {}\n      ancestors.forEach(status => newStatusMap[status.id] = status)\n      descendants.forEach(status => newStatusMap[status.id] = status)\n      commit('updateStatusMap', newStatusMap)\n    } catch (e) {\n\n    }\n  },\n\n  async updateCardMap (store, statusId: string) {\n    const targetStatus = store.state.statusMap[statusId]\n\n    if (targetStatus.pixiv_cards && targetStatus.pixiv_cards.length > 0) return\n\n    if (store.state.cardMap[statusId]) return\n\n    try {\n      const result = await api.statuses.getStatusCardInfoById(statusId)\n      store.commit('updateCardMap', { [statusId]: result.data })\n    } catch (e) {\n\n    }\n  },\n\n  async postStatus ({ commit, dispatch }, { formData, mainStatusId }: {\n    formData: postStatusFormData\n    mainStatusId: string\n  }) {\n    try {\n      const result = await api.statuses.postStatus(formData)\n\n      // meaning this is a new root post\n      if (!formData.inReplyToId) {\n        // todo 默认只有home信息流，真的好吗？\n        commit('unShiftTimeLineStatuses', {\n          newStatusIdList: [result.data.id],\n          timeLineType: TimeLineTypes.HOME\n        })\n      } else {\n        // update the reply status's context\n        await dispatch('updateContextMap', mainStatusId)\n      }\n\n      // update status map\n      commit('updateStatusMap', { [result.data.id]: result.data })\n      dispatch('updateCardMap', result.data.id)\n    } catch (e) {\n      throw new Error(e)\n    }\n  },\n\n  async deleteStatus ({ commit }, { statusId }) {\n    // remove from time line\n    commit('deleteStatusFromTimeLine', statusId)\n\n    // remove from status map\n    commit('removeStatusFromStatusMapById', statusId)\n\n    try {\n      await api.statuses.deleteStatusById(statusId)\n\n    } catch (e) {\n\n    }\n  }\n}\n\nexport default statuses\n"
  },
  {
    "path": "src/store/actions/timelines.ts",
    "content": "import * as api from '@/api'\nimport { mastodonentities } from '@/interface'\nimport { isBaseTimeLine } from '@/util'\nimport { TimeLineTypes } from '@/constant'\n\nexport default {\n  async updateTimeLineStatuses ({ commit, dispatch, state }, { timeLineType, hashName, isLoadMore, isFetchMore }: {\n    timeLineType: string\n    hashName?: string\n    isLoadMore?: boolean\n    isFetchMore?: boolean\n  }) {\n    if (!timeLineType) throw new Error('set time line type!')\n\n    let targetStatusIdList: Array<string>\n\n    if (isBaseTimeLine(timeLineType)) {\n      targetStatusIdList = state.timelines[timeLineType]\n    } else {\n      targetStatusIdList = state.timelines[timeLineType][hashName] || []\n    }\n\n    let maxId, sinceId\n    if (isLoadMore) {\n      maxId = targetStatusIdList[targetStatusIdList.length - 1]\n    } else if (isFetchMore) {\n      sinceId = targetStatusIdList[0]\n    }\n\n    let mutationName = ''\n    if (!isLoadMore && !isFetchMore) mutationName = 'setTimeLineStatuses'\n    if (isLoadMore && !isFetchMore) mutationName = 'pushTimeLineStatuses'\n    if (!isLoadMore && isFetchMore) mutationName = 'unShiftTimeLineStatuses'\n\n    try {\n      const result = await api.timelines.getTimeLineStatuses({ timeLineType, hashName, maxId, sinceId })\n\n      const resultToFetchContext = result.data.filter((status: mastodonentities.Status) => {\n        // remove for some instance's replies_count has bug\n        return !status.in_reply_to_id\n      })\n\n      // update context map\n      // optimize only home time line's result should check context\n      if (timeLineType === TimeLineTypes.HOME) {\n        Promise.all(resultToFetchContext.map((status: mastodonentities.Status) => {\n          return api.statuses.getStatusContextById(status.id)\n        })).then(results => {\n          const newContextMap = {}\n          const newStatusMap = {}\n          results.forEach((contextResult, index) => {\n            const descendantIdList = contextResult.data.descendants.map(status => status.id)\n\n            // only record descendant here\n            if (descendantIdList.length) {\n              newContextMap[resultToFetchContext[index].id] = {\n                ancestors: contextResult.data.ancestors.map(status => status.id),\n                descendants: descendantIdList\n              }\n            }\n\n            contextResult.data.ancestors.forEach(status => newStatusMap[status.id] = status)\n            contextResult.data.descendants.forEach(status => newStatusMap[status.id] = status)\n          })\n\n          Object.keys(newContextMap).length && commit('updateContextMap', newContextMap)\n          // also update status map\n          Object.keys(newStatusMap).length && commit('updateStatusMap', newStatusMap)\n        })\n      }\n\n      // update status map\n      const newStatusMap = {}\n      result.data.forEach(status => newStatusMap[status.id] = status)\n      commit('updateStatusMap', newStatusMap)\n      Object.keys(newStatusMap).forEach(statusId => {\n        dispatch('updateCardMap', statusId)\n      })\n      commit(mutationName, { newStatusIdList: result.data.map(status => status.id), timeLineType, hashName })\n\n      return result\n    } catch (e) {\n      throw e\n    }\n  }\n}\n"
  },
  {
    "path": "src/store/getters/index.ts",
    "content": "import { cuckoostore, mastodonentities } from '@/interface'\nimport { isBaseTimeLine } from '@/util'\nimport { UiWidthCheckConstants } from '@/constant'\n\nconst accounts = {\n  getAccountDisplayName () {\n    return (account: mastodonentities.Account) => account.display_name || account.username || account.acct\n  },\n\n  getAccountAtName () {\n    return (account: mastodonentities.Account) => account.username || account.acct\n  }\n}\n\nconst timelines = {\n  getRootStatuses (state: cuckoostore.stateInfo) {\n    return (timeLineType: string, hashName?: string): Array<mastodonentities.Status> => {\n      const targetStatusIdList = isBaseTimeLine(timeLineType) ? state.timelines[timeLineType] :\n        state.timelines[timeLineType][hashName]\n\n      // todo avoid this situation\n      if (!targetStatusIdList) return []\n\n      return targetStatusIdList\n        .map(statusId => state.statusMap[statusId]).filter(status => status)\n        .filter((status: mastodonentities.Status) => !status.in_reply_to_id)\n        .filter(status => {\n          const muteStatusList = state.appStatus.settings.muteMap.statusList\n          return muteStatusList.indexOf(status.id) === -1\n        }).filter(status => {\n          const muteUserList = state.appStatus.settings.muteMap.userList\n          return muteUserList.indexOf(status.account.id) === -1\n        })\n    }\n  }\n}\n\nconst getters = {\n  ...accounts,\n  ...timelines,\n\n  isOAuthUser (state: cuckoostore.stateInfo) {\n    return state.OAuthInfo.accessToken\n  },\n\n  isMobileMode (state: cuckoostore.stateInfo) {\n    return state.appStatus.documentWidth < UiWidthCheckConstants.DRAWER_DOCKING_BOUNDARY\n  },\n\n  shouldDialogFullScreen (state: cuckoostore.stateInfo) {\n    return state.appStatus.documentWidth <= UiWidthCheckConstants.POST_STATUS_DIALOG_TOGGLE_WIDTH\n  }\n}\n\nexport default getters\n"
  },
  {
    "path": "src/store/index.ts",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport mutations from './mutations'\nimport actions from './actions'\nimport getters from './getters'\nimport { cuckoostore } from '@/interface'\nimport { UiWidthCheckConstants, ThemeNames, I18nLocales, VisibilityTypes } from '@/constant'\n\nVue.use(Vuex)\n\nfunction getLocalSetting (tag, defaultValue) {\n  const stringData = localStorage.getItem(tag)\n\n  if (!stringData) return defaultValue\n\n  const parsedData = JSON.parse(stringData)\n\n  if (Object.keys(parsedData).length >= 500) return defaultValue\n\n  return parsedData\n}\n\nconst state: cuckoostore.stateInfo = {\n\n  OAuthInfo: {\n    // todo encode\n    clientId: localStorage.getItem('clientId') || '',\n    clientSecret: localStorage.getItem('clientSecret') || '',\n    accessToken: localStorage.getItem('accessToken') || '',\n    code: localStorage.getItem('code') || ''\n  },\n\n  mastodonServerUri: localStorage.getItem('mastodonServerUri') || '',\n\n  currentUserAccount: getLocalSetting('currentUserAccount', null),\n\n  timelines: {\n    home: getLocalSetting('home', []),\n    public: [],\n    direct: [],\n    local: [],\n    tag: {},\n    list: {}\n  },\n\n  contextMap: getLocalSetting('contextMap', {}),\n\n  statusMap: getLocalSetting('statusMap', {}),\n\n  cardMap: getLocalSetting('cardMap', {}),\n\n  customEmojis: getLocalSetting('customEmojis', []),\n\n  notifications: [],\n\n  relationships: {},\n\n  appStatus: {\n    documentWidth: window.innerWidth,\n\n    isDrawerOpened: window.innerWidth > UiWidthCheckConstants.DRAWER_DOCKING_BOUNDARY,\n\n    isNotificationsPanelOpened: false,\n\n    unreadNotificationCount: 0,\n\n    isEditingThemeMode: false,\n\n    shouldShowThemeEditPanel: false,\n\n    streamStatusesPool: {\n      home: [],\n      public: [],\n      direct: [],\n      local: [],\n      tag: {},\n      list: {}\n    },\n\n    settings: {\n      multiLineMode: getLocalSetting('multiLineMode', true),\n      maximumNumberOfColumnsInMultiLineMode: getLocalSetting('maximumNumberOfColumnsInMultiLineMode', 3),\n      showSensitiveContentMode: getLocalSetting('showSensitiveContentMode', false),\n      realTimeLoadStatusMode: getLocalSetting('realTimeLoadStatusMode', false),\n      autoExpandSpoilerTextMode: getLocalSetting('autoExpandSpoilerTextMode', false),\n      postMediaAsSensitiveMode: getLocalSetting('postMediaAsSensitiveMode', false),\n      theme: localStorage.getItem('theme') || ThemeNames.GOOGLE_PLUS,\n      tags: getLocalSetting('tags', ['hello']),\n      locale: localStorage.getItem('locale') || I18nLocales.EN,\n      postPrivacy: localStorage.getItem('postPrivacy') || VisibilityTypes.PUBLIC,\n      onlyMentionTargetUserMode: getLocalSetting('onlyMentionTargetUserMode', false),\n      muteMap: {\n        statusList: getLocalSetting('statusMuteList', []),\n        userList: getLocalSetting('userMuteList', [])\n      },\n    },\n\n  }\n}\n\nexport default new Vuex.Store({\n  state,\n  mutations,\n  actions,\n  getters\n})\n"
  },
  {
    "path": "src/store/mutations/appstatus.ts",
    "content": "import Vue from 'vue'\nimport { getTargetStatusesList } from '@/util'\nimport { ThemeNames } from '@/constant'\nimport { cuckoostore } from '@/interface'\nimport ThemeManager from '@/themes'\n\nexport default {\n  updateDrawerOpenStatus (state: cuckoostore.stateInfo, isDrawerOpened: boolean) {\n    state.appStatus.isDrawerOpened = isDrawerOpened\n  },\n\n  updateNotificationsPanelStatus (state: cuckoostore.stateInfo, isNotificationsPanelOpened: boolean) {\n    state.appStatus.isNotificationsPanelOpened = isNotificationsPanelOpened\n  },\n\n  updateUnreadNotificationCount (state: cuckoostore.stateInfo, count: number) {\n    state.appStatus.unreadNotificationCount = count\n  },\n\n  updateDocumentWidth (state: cuckoostore.stateInfo) {\n    state.appStatus.documentWidth = window.innerWidth\n  },\n\n  updateTheme (state: cuckoostore.stateInfo, newThemeName: string) {\n    state.appStatus.settings.theme = newThemeName\n    localStorage.setItem('theme', newThemeName)\n  },\n\n  updateTags (state: cuckoostore.stateInfo, newTags: Array<string>) {\n    Vue.set(state.appStatus.settings, 'tags', newTags)\n    localStorage.setItem('tags', JSON.stringify(newTags))\n  },\n\n  updateMultiLineMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.multiLineMode = newMode\n    localStorage.setItem('multiLineMode', JSON.stringify(newMode))\n  },\n\n  updateShowSensitiveContentMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.showSensitiveContentMode = newMode\n    localStorage.setItem('showSensitiveContentMode', JSON.stringify(newMode))\n  },\n\n  updateRealTimeLoadStatusMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.realTimeLoadStatusMode = newMode\n    localStorage.setItem('realTimeLoadStatusMode', JSON.stringify(newMode))\n  },\n\n  updateLocale (state: cuckoostore.stateInfo, newLocale: string) {\n    state.appStatus.settings.locale = newLocale\n    localStorage.setItem('locale', newLocale)\n  },\n\n  updateMuteStatusList (state: cuckoostore.stateInfo, statusId: string) {\n    const statusList: Array<string> = state.appStatus.settings.muteMap.statusList\n    if (statusList.indexOf(statusId) === -1) statusList.push(statusId)\n    localStorage.setItem('statusMuteList', JSON.stringify(statusList))\n  },\n\n  updateMuteUserList (state: cuckoostore.stateInfo, userId: string) {\n    const userList: Array<string> = state.appStatus.settings.muteMap.userList\n    if (userList.indexOf(userId) === -1) userList.push(userId)\n    localStorage.setItem('userMuteList', JSON.stringify(userList))\n  },\n\n  unShiftStreamStatusesPool (state: cuckoostore.stateInfo, { newStatusIdList, timeLineType, hashName }) {\n    const targetStatusesPool = getTargetStatusesList(state.appStatus.streamStatusesPool, timeLineType, hashName)\n    newStatusIdList = newStatusIdList.filter(id => {\n      return targetStatusesPool.indexOf(id) === -1\n    })\n\n    targetStatusesPool.unshift(...newStatusIdList)\n  },\n\n  clearStreamStatusesPool (state: cuckoostore.stateInfo, { timeLineType, hashName }) {\n    const targetStatusesPool = getTargetStatusesList(state.appStatus.streamStatusesPool, timeLineType, hashName)\n    targetStatusesPool.splice(0, targetStatusesPool.length)\n  },\n\n  updatePostPrivacy (state: cuckoostore.stateInfo, newPostPrivacy: string) {\n    state.appStatus.settings.postPrivacy = newPostPrivacy\n    localStorage.setItem('postPrivacy', newPostPrivacy)\n  },\n\n  updatePostMediaAsSensitiveMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.postMediaAsSensitiveMode = newMode\n    localStorage.setItem('postMediaAsSensitiveMode', JSON.stringify(newMode))\n  },\n\n  updateOnlyMentionTargetUserMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.onlyMentionTargetUserMode = newMode\n    localStorage.setItem('onlyMentionTargetUserMode', JSON.stringify(newMode))\n  },\n\n  updateMaximumNumberOfColumnsInMultiLineMode (state: cuckoostore.stateInfo, newNumber: number) {\n    state.appStatus.settings.maximumNumberOfColumnsInMultiLineMode = newNumber\n    localStorage.setItem('maximumNumberOfColumnsInMultiLineMode', JSON.stringify(newNumber))\n  },\n\n  updateAutoExpandSpoilerTextMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.settings.autoExpandSpoilerTextMode = newMode\n    localStorage.setItem('autoExpandSpoilerTextMode', JSON.stringify(newMode))\n  },\n\n  updateIsEditingThemeMode (state: cuckoostore.stateInfo, newMode: boolean) {\n    state.appStatus.isEditingThemeMode = newMode\n    state.appStatus.shouldShowThemeEditPanel = newMode\n  },\n\n  updateShouldShowThemeEditPanel (state: cuckoostore.stateInfo, show: boolean) {\n    state.appStatus.shouldShowThemeEditPanel = show\n  }\n}\n"
  },
  {
    "path": "src/store/mutations/index.ts",
    "content": "import Vue from 'vue'\nimport timelinesMutations from './timelines'\nimport notificationsMutations from './notifications'\nimport appStatusMutations from './appstatus'\nimport { cuckoostore, mastodonentities } from '@/interface'\nimport { formatHtml, formatAccountDisplayName } from '@/util'\n\nfunction formatStatusContent (status: mastodonentities.Status) {\n  return formatHtml(status.content, { externalEmojis: status.emojis })\n}\n\nconst oAuthInfoMutations = {\n\n  clearAllOAuthInfo (state: cuckoostore.stateInfo) {\n    state.OAuthInfo.clientId = ''\n    state.OAuthInfo.clientSecret = ''\n    state.OAuthInfo.code = ''\n    state.OAuthInfo.accessToken = ''\n\n    localStorage.clear()\n  },\n\n  updateClientInfo (state: cuckoostore.stateInfo, { clientId, clientSecret }) {\n    state.OAuthInfo.clientId = clientId\n    state.OAuthInfo.clientSecret = clientSecret\n\n    localStorage.setItem('clientId', clientId)\n    localStorage.setItem('clientSecret', clientSecret)\n  },\n\n  updateOAuthCode (state: cuckoostore.stateInfo, code: string) {\n    state.OAuthInfo.code = code\n\n    localStorage.setItem('code', code)\n  },\n\n  updateOAuthAccessToken (state: cuckoostore.stateInfo, accessToken: string) {\n    state.OAuthInfo.accessToken = accessToken\n\n    localStorage.setItem('accessToken', accessToken)\n  }\n}\n\nconst statusesMutations = {\n  updateStatusMap (state: cuckoostore.stateInfo, newStatusMap) {\n    Object.keys(newStatusMap).forEach(statusId => {\n      // format content\n      const newStatus: mastodonentities.Status = newStatusMap[statusId]\n      newStatus.content = formatStatusContent(newStatus)\n\n      // format reblog content\n      if (newStatus.reblog) newStatus.reblog.content = formatStatusContent(newStatus.reblog)\n\n      // format spoiler text\n      if (newStatus.spoiler_text) newStatus.spoiler_text = formatHtml(newStatus.spoiler_text, { externalEmojis: newStatus.emojis })\n\n      // format account display name\n      newStatus.account.display_name = formatAccountDisplayName(newStatus.account)\n\n      // fix favourited and reblogged count sync bug\n      const checkTarget = newStatus.reblog || newStatus\n      if (checkTarget.favourited && checkTarget.favourites_count === 0) checkTarget.favourites_count = 1\n      if (checkTarget.reblogged && checkTarget.reblogs_count === 0) checkTarget.reblogs_count = 1\n\n      Vue.set(state.statusMap, statusId, newStatusMap[statusId])\n    })\n  },\n\n  removeStatusFromStatusMapById (state: cuckoostore.stateInfo, statusId: string) {\n    Vue.set(state.statusMap, statusId, undefined)\n  },\n\n  updateFavouriteStatusById (state: cuckoostore.stateInfo, { favourited, mainStatusId, targetStatusId, notSelfOperate }) {\n    let targetStatus\n    if (mainStatusId === targetStatusId) {\n      targetStatus = state.statusMap[targetStatusId]\n    } else {\n      targetStatus = state.statusMap[mainStatusId].reblog\n    }\n\n    if (!targetStatus) return\n\n    if (!notSelfOperate) {\n      Vue.set(targetStatus, 'favourited', favourited)\n    }\n\n    Vue.set(targetStatus, 'favourites_count', favourited ?\n      targetStatus.favourites_count + 1 : targetStatus.favourites_count - 1)\n  },\n\n  updateReblogStatusById (state: cuckoostore.stateInfo, { reblogged, mainStatusId, targetStatusId, notSelfOperate }) {\n    let targetStatus\n    if (mainStatusId === targetStatusId) {\n      targetStatus = state.statusMap[targetStatusId]\n    } else {\n      targetStatus = state.statusMap[mainStatusId].reblog\n    }\n\n    if (!targetStatus) return\n\n    if (!notSelfOperate) {\n      Vue.set(targetStatus, 'reblogged', reblogged)\n    }\n\n    Vue.set(targetStatus, 'reblogs_count', reblogged ?\n      targetStatus.reblogs_count + 1 : targetStatus.reblogs_count - 1)\n  }\n}\n\nconst mutations = {\n  updateMastodonServerUri (state: cuckoostore.stateInfo, mastodonServerUri: string) {\n    state.mastodonServerUri = mastodonServerUri\n\n    localStorage.setItem('mastodonServerUri', mastodonServerUri)\n  },\n\n  updateCurrentUserAccount (state: cuckoostore.stateInfo, currentUserAccount: mastodonentities.Account) {\n    currentUserAccount.display_name = formatAccountDisplayName(currentUserAccount)\n\n    state.currentUserAccount = currentUserAccount\n\n    localStorage.setItem('currentUserAccount', JSON.stringify(currentUserAccount))\n  },\n\n  updateCustomEmojis (state: cuckoostore.stateInfo, customEmojis: Array<mastodonentities.Emoji>) {\n    state.customEmojis = customEmojis\n\n    localStorage.setItem('customEmojis', JSON.stringify(customEmojis))\n  },\n\n  updateContextMap (state: cuckoostore.stateInfo, newContextMap) {\n    Object.keys(newContextMap).forEach(statusId => {\n      Vue.set(state.contextMap, statusId, newContextMap[statusId])\n    })\n  },\n\n  updateCardMap (state: cuckoostore.stateInfo, newCardMap) {\n    Object.keys(newCardMap).forEach(statusId => {\n      Vue.set(state.cardMap, statusId, newCardMap[statusId])\n    })\n  },\n\n  ...oAuthInfoMutations,\n  ...timelinesMutations,\n  ...statusesMutations,\n  ...appStatusMutations,\n  ...notificationsMutations\n}\n\nexport default mutations\n"
  },
  {
    "path": "src/store/mutations/notifications.ts",
    "content": "import Vue from 'vue'\nimport { cuckoostore, mastodonentities } from '@/interface'\nimport { formatAccountDisplayName, formatHtml } from '@/util'\n\nexport default {\n  unShiftNotification (state: cuckoostore.stateInfo, newNotifications: Array<mastodonentities.Notification>) {\n    newNotifications.forEach(notification => {\n      if (notification.account) {\n        notification.account.display_name = formatAccountDisplayName(notification.account)\n      }\n      if (notification.status) {\n        notification.status.content = formatHtml(notification.status.content, { externalEmojis: notification.status.emojis })\n      }\n    })\n    state.notifications = newNotifications.concat(state.notifications)\n  },\n\n  pushNotifications (state: cuckoostore.stateInfo, newNotifications: Array<mastodonentities.Notification>) {\n    state.notifications = state.notifications.concat(newNotifications)\n  },\n\n  updateRelationships (state: cuckoostore.stateInfo, newRelationships: { [id: string]: mastodonentities.Relationship }) {\n    Object.keys(newRelationships).forEach(id => {\n      Vue.set(state.relationships, id, newRelationships[id])\n    })\n  }\n}\n"
  },
  {
    "path": "src/store/mutations/timelines.ts",
    "content": "import Vue from 'vue'\nimport { cuckoostore } from '@/interface'\nimport { TimeLineTypes } from '@/constant'\nimport { isBaseTimeLine } from '@/util'\n\nexport default {\n  setTimeLineStatuses (state: cuckoostore.stateInfo, { newStatusIdList, timeLineType, hashName }) {\n    if (isBaseTimeLine(timeLineType)) {\n      Vue.set(state.timelines, timeLineType, newStatusIdList)\n    } else {\n      if (!hashName) throw new Error('need a hash name!')\n\n      Vue.set(state.timelines[timeLineType], hashName, newStatusIdList)\n    }\n  },\n\n  pushTimeLineStatuses (state: cuckoostore.stateInfo, { newStatusIdList, timeLineType, hashName }) {\n    let targetTimeLines\n    if (isBaseTimeLine(timeLineType)) {\n      targetTimeLines = state.timelines[timeLineType]\n    } else {\n      if (!hashName) throw new Error('need a hash name!')\n      targetTimeLines = state.timelines[timeLineType][hashName]\n    }\n\n    newStatusIdList = newStatusIdList.filter(id => {\n      return targetTimeLines.indexOf(id) === -1\n    })\n\n    targetTimeLines.push(...newStatusIdList)\n  },\n\n  unShiftTimeLineStatuses (state: cuckoostore.stateInfo, { newStatusIdList, timeLineType, hashName }) {\n    let targetTimeLines\n    if (isBaseTimeLine(timeLineType)) {\n      targetTimeLines = state.timelines[timeLineType]\n    } else {\n      if (!hashName) throw new Error('need a hash name!')\n      targetTimeLines = state.timelines[timeLineType][hashName]\n    }\n\n    newStatusIdList = newStatusIdList.filter(id => {\n      return targetTimeLines.indexOf(id) === -1\n    })\n\n    targetTimeLines.unshift(...newStatusIdList)\n  },\n\n  deleteStatusFromTimeLine (state: cuckoostore.stateInfo, statusId: string) {\n    Object.keys(state.timelines).forEach(timeLineType => {\n      if (isBaseTimeLine(timeLineType)) {\n        const currentTimeLineList = state.timelines[timeLineType]\n\n        if (currentTimeLineList) {\n          currentTimeLineList.splice(currentTimeLineList.indexOf(statusId), 1)\n        }\n\n      } else {\n        const currentTimeLineMap = state.timelines[timeLineType]\n\n        Object.keys(currentTimeLineMap).forEach(hashName => {\n          const currentTimeLineList = currentTimeLineMap[hashName]\n\n          if (currentTimeLineList) {\n            currentTimeLineList.splice(currentTimeLineList.indexOf(statusId), 1)\n          }\n        })\n\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/themes/basecolor.ts",
    "content": "export default {\n  '@primaryColor': '#2196f3',\n  '@secondaryColor': '#ff4081',\n  '@successColor': '#4caf50',\n  '@warningColor': '#fdd835',\n  '@infoColor': '#2196f3',\n  '@errorColor': '#f44336',\n  '@alternateTextColor': '#ffffff',\n\n  '@trackColor': '#9e9e9e',\n\n  '@textColor': 'rgba(0,0,0,.87)',\n  '@secondaryTextColor': 'rgba(0,0,0,.54)',\n  '@disabledColor': 'rgba(0,0,0,.38)',\n\n  '@backgroundColor': '#f1f1f1',\n  '@dialogBackgroundColor': '#fff'\n}"
  },
  {
    "path": "src/themes/index.ts",
    "content": "// @ts-ignore\nimport cuckooHubTheme from './presets/cuckoohub'\nimport greenLightTheme from './presets/greenlight'\nimport darkTheme from './presets/dark'\nimport googlePlusTheme from './presets/googleplus'\nimport * as less from 'less'\nimport stylePattern from './stylepattern'\nimport { ThemeNames } from '@/constant'\nimport * as fileSaver from 'file-saver'\nimport baseColor from './basecolor'\n\nconst presetThemeInfo = {\n  [ThemeNames.GOOGLE_PLUS]: {\n    theme: googlePlusTheme,\n    less: stylePattern(Object.assign({}, baseColor, googlePlusTheme.colorSet)),\n    css: null,\n  },\n  [ThemeNames.DARK]: {\n    theme: darkTheme,\n    less: stylePattern(Object.assign({}, baseColor, darkTheme.colorSet)),\n    css: null,\n  },\n  [ThemeNames.GREEN_LIGHT]: {\n    theme: greenLightTheme,\n    less: stylePattern(Object.assign({}, baseColor, greenLightTheme.colorSet)),\n    css: null\n  },\n  [ThemeNames.CUCKOO_HUB]: {\n    theme: cuckooHubTheme,\n    less: stylePattern(Object.assign({}, baseColor, cuckooHubTheme.colorSet)),\n    css: null\n  }\n}\n\nclass ThemeManager {\n\n  public get themeInfo () {\n    return Object.assign({}, presetThemeInfo, this.customThemeInfo)\n  }\n\n  private customThemeInfo = localStorage.getItem('customThemeInfo') ? JSON.parse(localStorage.getItem('customThemeInfo')) : {}\n\n  private getThemeStyleElem (): HTMLStyleElement {\n    const themeElemId = 'cuckoo-plus-theme'\n    let styleElem = document.getElementById(themeElemId)\n\n    if (styleElem) return styleElem as HTMLStyleElement\n\n    styleElem = document.createElement('style')\n    styleElem.id = themeElemId\n    document.body.appendChild(styleElem)\n\n    return styleElem as HTMLStyleElement\n  }\n\n  private setFavIconByThemeName (themeName: string) {\n    Array.from(document.head.querySelectorAll('link')).forEach(el => {\n      if (el.getAttribute('rel') === 'icon') {\n        const size = el.getAttribute('sizes')\n        if (size) {\n          el.setAttribute('href', `favicon/${this.themeInfo[themeName].theme.toFavIconPath}/${size}.png`)\n        }\n      }\n    })\n  }\n\n  private setThemeColorByThemeName (themeName: string) {\n    Array.from(document.head.querySelectorAll('meta')).find(el => {\n      return el.getAttribute('name') === 'theme-color'\n    }).setAttribute('content', this.themeInfo[themeName].theme.colorSet['@primaryColor'])\n  }\n\n  private setThemeCssByThemeName (themeName: string) {\n    // todo fix custom localStorage data error\n    if (!this.themeInfo[themeName].less || this.customThemeInfo[themeName]) {\n      this.themeInfo[themeName].less = stylePattern(Object.assign({}, baseColor, this.themeInfo[themeName].theme.colorSet))\n    }\n\n    if (this.themeInfo[themeName].css) {\n      this.getThemeStyleElem().innerHTML = this.themeInfo[themeName].css\n    } else {\n      less.render(this.themeInfo[themeName].less).then(output => {\n        this.getThemeStyleElem().innerHTML = output.css\n        this.themeInfo[themeName].css = output.css\n      })\n    }\n  }\n\n  private addCustomThemeInfo (themeColorSet, themeName) {\n    this.customThemeInfo[themeName] = {\n      theme: { colorSet: themeColorSet, toFavIconPath: 'google_plus' },\n      less: stylePattern(Object.assign({}, baseColor, themeColorSet)),\n      css: null\n    }\n\n    this.updateLocalStorageData()\n  }\n\n  private deleteCustomThemeInfo (themeName) {\n    delete this.customThemeInfo[themeName]\n    this.updateLocalStorageData()\n  }\n\n  private updateLocalStorageData () {\n    const customThemeInfo = {}\n    Object.keys(this.customThemeInfo).forEach(themeName => {\n      customThemeInfo[themeName] = {\n        theme: { colorSet: this.customThemeInfo[themeName].theme.colorSet, toFavIconPath: 'google_plus' }\n      }\n    })\n    localStorage.setItem('customThemeInfo', JSON.stringify(customThemeInfo))\n  }\n\n  public getThemeInfoByThemeName (themeName: string) {\n    if (!this.themeInfo[themeName]) return this.themeInfo[ThemeNames.GOOGLE_PLUS]\n\n    return this.themeInfo[themeName]\n  }\n\n  public getThemeOptionsList () {\n    return Object.keys(this.themeInfo)\n      .filter(themeName => typeof this.themeInfo[themeName] === 'object')\n      .map(themeName => { return { 'value': themeName } })\n  }\n\n  public getCustomThemeOptionsList () {\n    return Object.keys(this.customThemeInfo)\n      .filter(themeName => typeof this.themeInfo[themeName] === 'object')\n      .map(themeName => { return { 'value': themeName } })\n  }\n\n  public setTheme (themeName: string) {\n    if (!this.themeInfo[themeName]) {\n      themeName = ThemeNames.GOOGLE_PLUS\n    }\n    this.setThemeCssByThemeName(themeName)\n    this.setFavIconByThemeName(themeName)\n    this.setThemeColorByThemeName(themeName)\n  }\n\n  public exportTheme (themeName: string) {\n    const blob = new Blob([JSON.stringify(this.themeInfo[themeName].theme.colorSet)], {type: \"text/plain;charset=utf-8\"});\n    fileSaver.saveAs(blob, `${themeName}.json`);\n  }\n\n  public importTheme (themeColorSet, themeName: string) {\n    this.addCustomThemeInfo(themeColorSet, themeName)\n  }\n\n  public deleteTheme (themeName: string) {\n    this.deleteCustomThemeInfo(themeName)\n  }\n\n  public setTempThemeByColorSet (colorSet) {\n    const finalColorSet = Object.assign({}, baseColor, colorSet)\n    less.render(stylePattern(finalColorSet)).then(output => {\n      this.getThemeStyleElem().innerHTML = output.css\n    })\n  }\n}\n\nexport default new ThemeManager()\n"
  },
  {
    "path": "src/themes/presets/cuckoohub.ts",
    "content": "import darkTheme from './dark'\n\nconst colorSet = Object.assign({}, darkTheme.colorSet, {\n  '@primaryColor': '#FF9900',\n  '@secondaryColor': '#FF9900',\n\n  '@textColor': '#fff',\n  '@secondaryTextColor': '#666',\n\n  '@backgroundColor': '#000',\n  '@dialogBackgroundColor': '#1b1b1b'\n})\n\nexport default {\n  colorSet,\n  toFavIconPath: 'cuckoo_hub'\n}\n"
  },
  {
    "path": "src/themes/presets/dark.ts",
    "content": "const colorSet = {\n  '@primaryColor': '#1976d2',\n  '@secondaryColor': '#ff4081',\n  '@trackColor': '#444b5d',\n\n  '@textColor': 'rgba(255, 255, 255, 0.85)',\n  '@secondaryTextColor': '#606984',\n  '@disabledColor': 'rgba(255, 255, 255, 0.3)',\n\n  '@backgroundColor': '#191b22',\n  '@dialogBackgroundColor': '#282c37'\n}\n\nexport default {\n  colorSet,\n  toFavIconPath: 'dark'\n}\n"
  },
  {
    "path": "src/themes/presets/googleplus.ts",
    "content": "const colorSet = {\n  '@primaryColor': '#db4437',\n  '@secondaryColor': '#2b90d9',\n  '@trackColor': '#bdbdbd',\n\n  '@textColor': 'rgba(0,0,0,.87)',\n  '@secondaryTextColor': 'rgba(0,0,0,.54)',\n  '@disabledColor': 'rgba(0,0,0,.38)',\n\n  '@backgroundColor': '#f1f1f1',\n  '@dialogBackgroundColor': '#fff'\n}\n\nexport default {\n  colorSet,\n  toFavIconPath: 'google_plus'\n}\n"
  },
  {
    "path": "src/themes/presets/greenlight.ts",
    "content": "import googlePlusTheme from './googleplus'\n\nconst colorSet = Object.assign({}, googlePlusTheme.colorSet, {\n  '@primaryColor': '#0f9d58',\n  '@secondaryColor': '#0f9d58'\n})\n\nexport default {\n  colorSet,\n  toFavIconPath: 'green_light'\n}\n"
  },
  {
    "path": "src/themes/stylepattern.ts",
    "content": "const themeColorLessText = `\n.mu-primary-color {\n  background-color: @primaryColor;\n}\n\n.mu-secondary-color {\n  background-color: @secondaryColor;\n}\n\n.mu-success-color {\n  background-color: @successColor;\n}\n\n.mu-warning-color {\n  background-color: @warningColor;\n}\n\n.mu-info-color {\n  background-color: @infoColor;\n}\n\n.mu-error-color {\n  background-color: @errorColor;\n}\n\n.mu-inverse {\n  color: @alternateTextColor;\n}\n\n.mu-primary-text-color {\n  color: @primaryColor;\n}\n\n.mu-secondary-text-color {\n  color: @secondaryColor;\n}\n\n.mu-success-text-color {\n  color: @successColor;\n}\n\n.mu-warning-text-color {\n  color: @warningColor;\n}\n\n.mu-info-text-color {\n  color: @infoColor;\n}\n\n.mu-error-text-color {\n  color: @errorColor;\n}\n`\n\nconst appColorLessText = `\nbody {\n  background-color: @backgroundColor;\n}\n\na {\n  color: @secondaryColor;\n}\n\n// class for certain component\n.status-card {\n  .operate-btn-group {\n    .count {\n      color: @textColor;\n    }\n  }\n}\n\n.circle-btn {\n  width: 36px;\n  height: 36px;\n  border-radius: 50%;\n  cursor: pointer;\n  -webkit-transition: background .3s;\n  -moz-transition: background .3s;\n  -ms-transition: background .3s;\n  -o-transition: background .3s;\n  transition: background .3s;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  line-height: 1;\n  margin: 0 8px;\n  font-size: 15px;\n\n  background-color: @backgroundColor;\n  color: @textColor;\n\n  &.disabled {\n    cursor: not-allowed !important;\n\n    &:hover {\n      box-shadow: none;\n    }\n  }\n\n  &:hover {\n    box-shadow: 0 1px 3px 0 rgba(0,0,0,0.26);\n  }\n\n  &.primary-theme-bg-color {\n    background-color: @primaryColor;\n  }\n\n  &.unset-display {\n    display: unset;\n  }\n\n  &.hover:before {\n    background-color: unset;\n  }\n}\n\n.header-svg-fill {\n  fill: @secondaryTextColor;\n}\n\n.auto-size-text-area {\n  width: 100%;\n  font-size: 15px;\n  line-height: 18px;\n  outline: none;\n  border: none;\n  padding: 0;\n  resize: none;\n  background-color: @dialogBackgroundColor;\n  color: @textColor;\n}\n\n.cuckoo-header-container {\n  .mu-text-field-input {\n    color: #fff;\n  }\n}\n\n.delete-hash-btn {\n  color: @textColor !important;\n}\n\n.media-area {\n  height: 212px;\n  overflow-x: auto;\n  overflow-y: hidden;\n  -webkit-overflow-scrolling: touch;\n  white-space: nowrap;\n  \n  .media-item {\n    margin-right: 8px;\n    position: relative;\n    display: inline-block;\n    height: 100%;\n\n    img {\n      width: auto;\n      height: 100%;\n      display: block;\n    }\n  }\n\n  &.single-media-area {\n    .media-item {\n      margin: 0;\n      width: 100%;\n      display: flex;\n      justify-content: center;\n    }\n    \n    img {\n      width: 100%;\n    }\n  }\n}\n\n.media-area {\n  .media-item {\n\n  }\n}\n\n// for overwrite muse-ui style\n.mu-dialog {\n  background-color: @dialogBackgroundColor;\n  \n  .mu-dialog-title {\n    color: @textColor;\n  }\n\n  .mu-dialog-body {\n    height: 100%;\n    color: @textColor;\n  }\n}\n\n.mu-card {\n  color: @textColor;\n  background-color: @dialogBackgroundColor;\n  -webkit-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);\n  -moz-box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);\n  box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.14);\n\n  .mu-card-text {\n    color: @textColor;\n  }\n\n  .mu-card-header-title {\n\n    .mu-card-title {\n      color: @textColor;\n    }\n\n    .mu-card-sub-title {\n      color: @secondaryTextColor;\n    }\n\n  }\n}\n\n.mu-input {\n  color: @secondaryTextColor;\n\n  .mu-select-content {\n    color: @textColor;\n\n    .mu-select-input {\n      color: @textColor;\n    }\n  }\n}\n\n.mu-popover {\n  background-color: @dialogBackgroundColor;\n\n  .mu-list {\n    padding: 0;\n  }\n}\n\n.mu-option {\n  color: @textColor;\n\n  &.is-selected .mu-item {\n    color: @primaryColor;\n  }\n}\n\n.mu-flat-button.disabled {\n  color: @disabledColor;\n}\n\n.mu-item {\n  color: @textColor;\n\n  .mu-item-action {\n    color: @textColor;\n  }\n\n  &.is-selected {\n    color: @primaryColor;\n\n    .mu-item-action {\n      color: @primaryColor;\n    }\n  }\n\n}\n\n.mu-loading-wrap {\n  background-color: fade(@dialogBackgroundColor, 80%) !important;\n  color: @textColor;\n}\n\n.mu-linear-progress {\n  .mu-linear-progress-background, .mu-linear-progress-determinate {\n    background-color: @secondaryColor !important;\n  }\n}\n\n.mu-text-field-input {\n  color: @textColor;\n\n  &::placeholder {\n    color: @trackColor;\n    font-weight: 400;\n  }\n}\n\n.mu-switch-checked {\n  color: @primaryColor;\n}\n\n.mu-form-item__focus {\n  color: @secondaryColor;\n}\n\n.mu-chip.mu-primary-color {\n  background-color: @primaryColor;\n}\n\n.mu-circle-spinner {\n  border-color: @primaryColor;\n}\n\n.notification-content {\n  a {\n    color: @textColor;\n  }\n}\n\n// class for mixin\n.primary-theme-bg-color {\n  background-color: @primaryColor !important;\n\n  > * {\n    color: @alternateTextColor;\n  }\n}\n\n.secondary-theme-bg-color {\n  background-color: @secondaryColor;\n}\n\n\n.default-theme-bg-color {\n  background-color: @backgroundColor;\n}\n\n.primary-theme-text-color {\n  color: @primaryColor;\n}\n\n.secondary-theme-text-color {\n  color: @secondaryColor;\n}\n\n.primary-read-text-color {\n  color: @textColor;\n}\n\n.secondary-read-text-color {\n  color: @secondaryTextColor;\n}\n\n.placeholder-read-text-color {\n  color: @trackColor;\n}\n\n.base-theme-bg-color {\n  background-color: @backgroundColor !important;\n}\n\n.dialog-theme-bg-color {\n  background-color: @dialogBackgroundColor;\n}\n`\n\nimport baseColor from './basecolor'\n\nexport default function (colorSet: Object) {\n  return Object.keys(baseColor).reduce((acc, cur) => {\n    return acc.replace(new RegExp(cur, 'g'), colorSet[cur])\n  }, themeColorLessText + appColorLessText)\n}\n"
  },
  {
    "path": "src/util.ts",
    "content": "import store from '@/store'\nimport { TimeLineTypes, RoutersInfo, I18nTags, VisibilityTypes } from '@/constant'\nimport { Route } from \"vue-router\"\nimport Formatter from \"./formatter\"\nimport { mastodonentities } from \"@/interface\"\nimport * as _ from 'underscore'\nimport * as $ from 'jquery'\n\nexport function patchApiUri (uri: string): string {\n  const targetServerUri = store.state.mastodonServerUri || 'https://pawoo.net'\n  return `${targetServerUri}${uri}`\n}\n\nexport function generateUniqueKey () {\n  const toReplacedString = 'xxxxxxxy-yyxx-xxyx-yyxx-xxyyxxxxxyyy'\n\n  return toReplacedString.replace(/[xy]/g, (c) => {\n    const r = Math.random() * 16 | 0,\n      v = c === 'x' ? r : (r & 0x3 | 0x8)\n\n    return v.toString(16)\n  })\n}\n\nexport function isBaseTimeLine (timeLineType: string): boolean {\n  return [TimeLineTypes.HOME, TimeLineTypes.PUBLIC, TimeLineTypes.DIRECT, TimeLineTypes.LOCAL].indexOf(timeLineType) !== -1\n}\n\nexport function getTimeLineTypeAndHashName (route: Route) {\n  let timeLineType = '', hashName = ''\n  if (route.name === RoutersInfo.defaulttimelines.name) {\n    timeLineType = route.params.timeLineType\n  }\n  else if (route.name === RoutersInfo.tagtimelines.name) {\n    timeLineType = TimeLineTypes.TAG\n    hashName = route.params.tagName\n  }\n  else if (route.name === RoutersInfo.listtimelines.name) {\n    timeLineType = TimeLineTypes.LIST\n    hashName = route.params.listName\n  }\n\n  return { timeLineType, hashName }\n}\n\nexport function getTargetStatusesList (listMap, timeLineType, hashName) {\n  let targetStatusesList\n  if (isBaseTimeLine(timeLineType)) {\n    targetStatusesList = listMap[timeLineType]\n  } else {\n    targetStatusesList = listMap[timeLineType][hashName]\n  }\n\n  return targetStatusesList || []\n}\n\nconst visibilityTypeToDescMap = {\n  [VisibilityTypes.PUBLIC]: {\n    descTag: I18nTags.common.status_visibility_public_desc,\n    icon: 'public'\n  },\n  [VisibilityTypes.UNLISTED]: {\n    descTag: I18nTags.common.status_visibility_unlisted_desc,\n    icon: 'lock_open'\n  },\n  [VisibilityTypes.PRIVATE]: {\n    descTag: I18nTags.common.status_visibility_private_desc,\n    icon: 'lock'\n  },\n  [VisibilityTypes.DIRECT]: {\n    descTag: I18nTags.common.status_visibility_direct_desc,\n    icon: 'email'\n  }\n}\nexport function getVisibilityDescInfo (visibilityType: string) {\n  return visibilityTypeToDescMap[visibilityType]\n}\n\nexport async function prepareRootStatus (status: mastodonentities.Status) {\n  const contextMap = store.state.contextMap\n  const statusMap = store.state.statusMap\n\n  if (!contextMap[status.id]) {\n    await store.dispatch('updateContextMap', status.id)\n  }\n\n  let targetStatus = status\n\n  const targetStatusContext = contextMap[status.id]\n\n  if (!targetStatusContext) return\n\n  if (targetStatusContext.ancestors.length) {\n    targetStatus = statusMap[targetStatusContext.ancestors[0]]\n  }\n\n  store.dispatch('updateContextMap', targetStatus.id)\n\n  return targetStatus\n}\n\n\nlet formatter\nexport function formatHtml(html: string, options: { externalEmojis } = { externalEmojis: [] }): string {\n  if (!formatter) {\n    formatter = new Formatter(store.state.customEmojis)\n  }\n\n  formatter.updateCustomEmojiMap(options.externalEmojis)\n\n  // create a parent node to contain the input html\n  const parentNode = document.createElement('template')\n  parentNode.innerHTML = html\n\n  walkTextNodes(parentNode.content, (parentNode, textNode) => {\n    const spanNode = document.createElement('span')\n    spanNode.innerHTML = formatter.format(_.escape(textNode.textContent))\n    parentNode.replaceChild(spanNode, textNode)\n  })\n\n  return parentNode.innerHTML\n}\n\nexport function formatAccountDisplayName (account: mastodonentities.Account) {\n  return formatHtml(store.getters['getAccountDisplayName'](account), { externalEmojis: account.emojis })\n}\n\nexport function extractText(html: string): string {\n  let text = \"\"\n\n  // create a parent node to contain the input html\n  const parentNode = document.createElement('template')\n  parentNode.innerHTML = html\n\n  walkTextNodes(parentNode.content, (parentNode, textNode) => {\n    text += (textNode.textContent + \" \")\n  })\n\n  return text\n}\n\nconst maxImageSize = 7.8 * 1024 * 1024\nexport async function resetImageFileSizeForUpload (file: File) {\n  if (file.size < maxImageSize) return new Promise(r => r(file))\n\n  const oldImage = new Image()\n  oldImage.src = window.URL.createObjectURL(file)\n\n  // todo set to 1280 width for now\n  const newWidth = 1280\n\n  return new Promise(resolve => {\n    oldImage.onload = () => {\n      const newHeight = oldImage.height * newWidth / oldImage.width\n      const canvas = document.createElement('canvas')\n      const canvasContext = canvas.getContext('2d')\n\n      canvas.width = newWidth\n      canvas.height = newHeight\n\n      canvasContext.drawImage(oldImage, 0, 0, newWidth, newHeight)\n\n      canvas.toBlob((blob) => {\n        resolve(blob)\n      })\n    }\n  })\n}\n\nfunction walkTextNodes(node, textNodeHandler) {\n  if (node) {\n    for (let i = 0; i < node.childNodes.length; ++i) {\n      const childNode = node.childNodes[i]\n      if (childNode.nodeType === 3) {\n        textNodeHandler(node, childNode)\n      } else if (childNode.nodeType === 1 || childNode.nodeType === 9 || childNode.nodeType === 11) {\n        walkTextNodes(childNode, textNodeHandler)\n      }\n    }\n  }\n}\n\nfunction easeInOutQuad (t, b, c, d) {\n  t /= d/2\n  if (t < 1) return c/2*t*t + b\n  t--\n  return -c/2 * (t*(t-2) - 1) + b\n}\n\nconst requestAnimFrame = (function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||function(callback){window.setTimeout(callback,1000/60);};})()\n\nexport function animatedScrollTo (element: HTMLElement, to: number, duration: number, callback?) {\n  const start = element.scrollTop,\n    change = to - start,\n    animationStart = +new Date()\n  let animating = true\n  let lastpos = null\n\n  const animateScroll = function() {\n    if (!animating) return\n\n    requestAnimFrame(animateScroll)\n\n    const now = +new Date()\n    const val = Math.floor(easeInOutQuad(now - animationStart, start, change, duration))\n\n    lastpos = val\n    element.scrollTop = val\n\n    if (now > animationStart + duration) {\n      element.scrollTop = to\n\n      animating = false\n      if (callback) { callback() }\n    }\n  }\n\n  requestAnimFrame(animateScroll)\n}\n\nexport function getNetEaseMusicFrameLinkFromContentLink (link: string): string | void {\n  const url = new URL(link)\n\n  const isNetEaseMusic = url.host === 'music.163.com'\n\n  if (!isNetEaseMusic) return\n\n  let songId\n\n  const isUseSongPath = url.pathname.startsWith('/song')\n  if (isUseSongPath) {\n    // use param song id\n    if (url.searchParams.get('id')) {\n      songId = url.searchParams.get('id')\n    }\n\n    // use path song id\n    if (url.pathname.replace('/song', '').match(/\\d+/)) {\n      songId = url.pathname.replace('/song', '').match(/\\d+/)[0]\n    }\n  }\n\n  const isUseSongHash = url.hash.startsWith('#/song?')\n  if (isUseSongHash) {\n    const paramsList = url.hash.replace('#/song?', '').split('&').filter(anchor => anchor.startsWith('id='))\n    if (paramsList[0]) songId = paramsList[0].split('=')[1]\n  }\n\n  if (!songId) return\n\n  return `//music.163.com/outchain/player?type=2&id=${songId}&auto=0&height=66`\n}\n\nexport function getYoutubeVideoFrameLinkFromContentLink (link: string): string | void {\n  const url = new URL(link)\n\n  let v\n\n  const isShareLink = url.host === 'youtu.be'\n  if (isShareLink) {\n    v = url.pathname.slice(1)\n  }\n\n  const isBrowserLink = url.host === 'www.youtube.com'\n  if (isBrowserLink) {\n    v = url.searchParams.get('v')\n  }\n\n  if (!v) return\n\n  return `https://www.youtube.com/embed/${v}`\n\n  // if (!link.startsWith('https://www.youtube.com/watch')) return\n  //\n  // const url = new URL(link)\n  //\n  // if (!url.searchParams.has('v')) return\n  //\n  // const v = url.searchParams.get('v')\n  // return `https://www.youtube.com/embed/${v}`\n}\n\nexport const documentGlobalEventBus = new class {\n\n  private eventMap: {\n    [key: string]: Array<{\n      listener: Function,\n      skip?: boolean\n    }>\n  } = {}\n\n  on (eventName: string, eventListener: Function, coexistWithOtherListener: boolean = false) {\n    if (!this.eventMap[eventName]) {\n      this.eventMap[eventName] = []\n      this.initDocumentGlobalEvent(eventName)\n    }\n\n    if (!coexistWithOtherListener) {\n      this.eventMap[eventName].forEach(listenerInfo => {\n        listenerInfo.skip = true\n      })\n    }\n\n    this.eventMap[eventName].push({\n      listener: eventListener\n    })\n  }\n\n  off (eventName: string, eventListener: Function) {\n    if (!eventListener) {\n      this.eventMap[eventName] = []\n    }\n\n    if (!this.eventMap[eventName]) return\n\n    const targetIndex = this.eventMap[eventName].findIndex(listenerInfo => listenerInfo.listener === eventListener)\n    this.eventMap[eventName].splice(targetIndex, 1)\n    this.eventMap[eventName].forEach(listenerInfo => {\n      listenerInfo.skip = false\n    })\n  }\n\n  private initDocumentGlobalEvent (eventName: string) {\n    document.addEventListener(eventName, (e) => {\n      this.eventMap[eventName].forEach(listenerInfo => {\n        if (listenerInfo.skip) return\n        listenerInfo.listener(e)\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"outDir\": \"./public/dist/\",\n    \"sourceMap\": true,\n    \"module\": \"commonjs\",\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"emitDecoratorMetadata\": true,\n    \"target\": \"es2015\",\n    \"allowJs\": true,\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"lib\": [\"es2015\", \"dom\"]\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "webpack.config.js",
    "content": "const webpack = require('webpack')\nconst path = require('path')\nconst yargs = require('yargs')\nconst fs = require('fs')\nconst BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin\nconst MinifyPlugin = require(\"babel-minify-webpack-plugin\");\n\nlet { env } = yargs.argv\nif (!env) env = 'develop'\nconst isEnvProduction = env === 'production'\n\nconst plugins = [\n  new webpack.DefinePlugin({\n  'process.env.NODE_ENV': JSON.stringify(env)\n  }),\n]\n\nif (isEnvProduction) {\n  // remove source map data\n  fs.unlink(path.join(__dirname, './public/dist/bundle.js.map'), (err) => {})\n  // plugins.push(new MinifyPlugin())\n} else {\n  // plugins.push(new BundleAnalyzerPlugin())\n\n}\n\nmodule.exports = {\n\n  devServer: {\n    contentBase: path.join(__dirname, '/public'),\n    publicPath: '/dist/',\n    compress: true,\n    port: 3000,\n    host: \"0.0.0.0\",\n    watchContentBase: true,\n    disableHostCheck: true\n  },\n\n  entry: './src/index.ts',\n\n  devtool: isEnvProduction ? '' : '#source-map',\n\n  output: {\n    libraryExport: 'default',\n    path: path.resolve(__dirname, 'public/dist/'),\n    filename: 'bundle.js'\n  },\n\n  module: {\n    rules: [\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: {\n          loaders: {\n            // Since sass-loader (weirdly) has SCSS as its default parse mode, we map\n            // the \"scss\" and \"sass\" values for the lang attribute to the right configs here.\n            // other preprocessors should work out of the box, no loader config like this necessary.\n            'scss': 'vue-style-loader!css-loader!sass-loader',\n            'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',\n            'i18n': '@kazupon/vue-i18n-loader'\n          },\n          esModule: true\n        }\n      },\n\n\n      {\n        test: /\\.ts$/,\n        exclude: /node_modules/,\n        use: {\n          loader: 'ts-loader',\n          options: {\n            appendTsSuffixTo: [/\\.vue$/]\n          }\n        }\n      },\n\n      {\n        test: /\\.tsx$/,\n        exclude: /node_modules/,\n        use: [\n          'babel-loader',\n          'ts-loader'\n        ]\n      },\n\n      {\n        test: /\\.(png|jpg|gif|svg)$/,\n        loader: 'file-loader',\n        options: {\n          name: '../assets/images/[name].[ext]'\n        }\n      },\n\n      {\n        test: /\\.(woff2?|eot|ttf|otf)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 1024,\n          name: '../assets/fonts/[name].[ext]'\n        }\n      },\n\n      {\n        test: /\\.css$/,\n        use: [{\n          loader: \"style-loader\"\n        }, {\n          loader: \"css-loader\"\n        }]\n      },\n\n      {\n        test: /\\.less$/,\n        use: [{\n          loader: \"style-loader\"\n        }, {\n          loader: \"css-loader\"\n        }, {\n          loader: \"less-loader\"\n        }]\n      }\n    ]\n  },\n\n  resolve: {\n    extensions: ['.ts', '.js', '.vue'],\n\n    alias: {\n      '@': path.join(__dirname, '/src')\n    }\n  },\n\n  externals: {\n    'moment': 'moment',\n    'underscore': '_'\n    // todo muse ui has bug\n  },\n\n  plugins: plugins\n};\n"
  }
]