index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. <template>
  2. <div class="app">
  3. <div class="admin-area">
  4. <main-header :class="{ 'admin-sidebar-active': sidebarActive }" />
  5. <div class="admin-content">
  6. <div
  7. class="admin-sidebar"
  8. :class="{ minimised: !sidebarActive }"
  9. >
  10. <div class="inner">
  11. <div
  12. class="bottom"
  13. :style="`padding-bottom: ${sidebarPadding}px`"
  14. >
  15. <div
  16. class="sidebar-item toggle-sidebar"
  17. @click="toggleSidebar()"
  18. content="Expand"
  19. v-tippy="{ onShow: () => !sidebarActive }"
  20. >
  21. <i class="material-icons">menu_open</i>
  22. <span>Minimise</span>
  23. </div>
  24. <div
  25. v-if="sidebarActive"
  26. class="sidebar-item with-children"
  27. :class="{ 'is-active': childrenActive.songs }"
  28. >
  29. <span>
  30. <router-link to="/admin/songs">
  31. <i class="material-icons">music_note</i>
  32. <span>Songs</span>
  33. </router-link>
  34. <i
  35. class="material-icons toggle-sidebar-children"
  36. @click="
  37. toggleChildren({ child: 'songs' })
  38. "
  39. >
  40. {{
  41. childrenActive.songs
  42. ? "expand_less"
  43. : "expand_more"
  44. }}
  45. </i>
  46. </span>
  47. <div class="sidebar-item-children">
  48. <router-link
  49. class="sidebar-item-child"
  50. to="/admin/songs"
  51. >
  52. Songs
  53. </router-link>
  54. <router-link
  55. class="sidebar-item-child"
  56. to="/admin/songs/import"
  57. >
  58. Import
  59. </router-link>
  60. </div>
  61. </div>
  62. <router-link
  63. v-else
  64. class="sidebar-item songs"
  65. to="/admin/songs"
  66. content="Songs"
  67. v-tippy="{
  68. theme: 'info',
  69. onShow: () => !sidebarActive
  70. }"
  71. >
  72. <i class="material-icons">music_note</i>
  73. <span>Songs</span>
  74. </router-link>
  75. <router-link
  76. class="sidebar-item reports"
  77. to="/admin/reports"
  78. content="Reports"
  79. v-tippy="{
  80. theme: 'info',
  81. onShow: () => !sidebarActive
  82. }"
  83. >
  84. <i class="material-icons">flag</i>
  85. <span>Reports</span>
  86. </router-link>
  87. <router-link
  88. class="sidebar-item stations"
  89. to="/admin/stations"
  90. content="Stations"
  91. v-tippy="{
  92. theme: 'info',
  93. onShow: () => !sidebarActive
  94. }"
  95. >
  96. <i class="material-icons">radio</i>
  97. <span>Stations</span>
  98. </router-link>
  99. <router-link
  100. class="sidebar-item playlists"
  101. to="/admin/playlists"
  102. content="Playlists"
  103. v-tippy="{
  104. theme: 'info',
  105. onShow: () => !sidebarActive
  106. }"
  107. >
  108. <i class="material-icons">library_music</i>
  109. <span>Playlists</span>
  110. </router-link>
  111. <div
  112. v-if="sidebarActive"
  113. class="sidebar-item with-children"
  114. :class="{ 'is-active': childrenActive.users }"
  115. >
  116. <span>
  117. <router-link to="/admin/users">
  118. <i class="material-icons">people</i>
  119. <span>Users</span>
  120. </router-link>
  121. <i
  122. class="material-icons toggle-sidebar-children"
  123. @click="
  124. toggleChildren({ child: 'users' })
  125. "
  126. >
  127. {{
  128. childrenActive.users
  129. ? "expand_less"
  130. : "expand_more"
  131. }}
  132. </i>
  133. </span>
  134. <div class="sidebar-item-children">
  135. <router-link
  136. class="sidebar-item-child"
  137. to="/admin/users"
  138. >
  139. Users
  140. </router-link>
  141. <router-link
  142. class="sidebar-item-child"
  143. to="/admin/users/data-requests"
  144. >
  145. Data Requests
  146. </router-link>
  147. <router-link
  148. class="sidebar-item-child"
  149. to="/admin/users/punishments"
  150. >
  151. Punishments
  152. </router-link>
  153. </div>
  154. </div>
  155. <router-link
  156. v-else
  157. class="sidebar-item users"
  158. to="/admin/users"
  159. content="Users"
  160. v-tippy="{
  161. theme: 'info',
  162. onShow: () => !sidebarActive
  163. }"
  164. >
  165. <i class="material-icons">people</i>
  166. <span>Users</span>
  167. </router-link>
  168. <router-link
  169. class="sidebar-item news"
  170. to="/admin/news"
  171. content="News"
  172. v-tippy="{
  173. theme: 'info',
  174. onShow: () => !sidebarActive
  175. }"
  176. >
  177. <i class="material-icons">chrome_reader_mode</i>
  178. <span>News</span>
  179. </router-link>
  180. <router-link
  181. class="sidebar-item statistics"
  182. to="/admin/statistics"
  183. content="Statistics"
  184. v-tippy="{
  185. theme: 'info',
  186. onShow: () => !sidebarActive
  187. }"
  188. >
  189. <i class="material-icons">show_chart</i>
  190. <span>Statistics</span>
  191. </router-link>
  192. <router-link
  193. class="sidebar-item youtube"
  194. to="/admin/youtube"
  195. content="YouTube"
  196. v-tippy="{
  197. theme: 'info',
  198. onShow: () => !sidebarActive
  199. }"
  200. >
  201. <i class="material-icons">smart_display</i>
  202. <span>YouTube</span>
  203. </router-link>
  204. </div>
  205. </div>
  206. </div>
  207. <div class="admin-container">
  208. <div class="admin-tab-container">
  209. <router-view></router-view>
  210. </div>
  211. <main-footer />
  212. </div>
  213. </div>
  214. </div>
  215. <floating-box
  216. id="keyboardShortcutsHelper"
  217. ref="keyboardShortcutsHelper"
  218. >
  219. <template #body>
  220. <div>
  221. <div>
  222. <span class="biggest"
  223. ><b>Keyboard shortcuts helper</b></span
  224. >
  225. <span
  226. ><b>Ctrl + /</b> - Toggles this keyboard shortcuts
  227. helper</span
  228. >
  229. <span
  230. ><b>Ctrl + Shift + /</b> - Resets the position of
  231. this keyboard shortcuts helper</span
  232. >
  233. <hr />
  234. </div>
  235. <div>
  236. <span class="biggest"><b>Table</b></span>
  237. <span class="bigger"><b>Navigation</b></span>
  238. <span
  239. ><b>Up / Down arrow keys</b> - Move between
  240. rows</span
  241. >
  242. <hr />
  243. </div>
  244. <div>
  245. <span class="bigger"><b>Page navigation</b></span>
  246. <span
  247. ><b>Ctrl + Left/Right arrow keys</b> - Previous/next
  248. page</span
  249. >
  250. <span
  251. ><b>Ctrl + Shift + Left/Right arrow keys</b> -
  252. First/last page</span
  253. >
  254. <hr />
  255. </div>
  256. <div>
  257. <span class="bigger"><b>Reset localStorage</b></span>
  258. <span><b>Ctrl + F5</b> - Resets localStorage</span>
  259. <hr />
  260. </div>
  261. <div>
  262. <span class="bigger"><b>Selecting</b></span>
  263. <span><b>Space</b> - Selects/unselects a row</span>
  264. <span><b>Ctrl + A</b> - Selects all rows</span>
  265. <span
  266. ><b>Shift + Up/Down arrow keys</b> - Selects all
  267. rows in between</span
  268. >
  269. <span
  270. ><b>Ctrl + Up/Down arrow keys</b> - Unselects all
  271. rows in between</span
  272. >
  273. <hr />
  274. </div>
  275. <div>
  276. <span class="bigger"><b>Popup actions</b></span>
  277. <span><b>Ctrl + 1-9</b> - Execute action 1-9</span>
  278. <span><b>Ctrl + 0</b> - Select action 1</span>
  279. <hr />
  280. </div>
  281. </div>
  282. </template>
  283. </floating-box>
  284. </div>
  285. </template>
  286. <script>
  287. import { mapState, mapActions, mapGetters } from "vuex";
  288. import keyboardShortcuts from "@/keyboardShortcuts";
  289. import FloatingBox from "@/components/FloatingBox.vue";
  290. export default {
  291. components: {
  292. FloatingBox
  293. },
  294. data() {
  295. return {
  296. currentTab: "",
  297. siteSettings: {
  298. logo: "",
  299. sitename: ""
  300. },
  301. sidebarActive: true,
  302. sidebarPadding: 0
  303. };
  304. },
  305. computed: {
  306. ...mapGetters({
  307. socket: "websockets/getSocket"
  308. }),
  309. ...mapState("admin", { childrenActive: state => state.childrenActive })
  310. },
  311. watch: {
  312. $route(route) {
  313. if (this.getTabFromPath(route.path)) this.onRouteChange();
  314. }
  315. },
  316. async mounted() {
  317. if (this.getTabFromPath()) {
  318. this.onRouteChange();
  319. } else if (localStorage.getItem("lastAdminPage")) {
  320. this.$router.push(
  321. `/admin/${localStorage.getItem("lastAdminPage")}`
  322. );
  323. } else {
  324. this.$router.push(`/admin/songs`);
  325. }
  326. this.siteSettings = await lofig.get("siteSettings");
  327. this.sidebarActive = JSON.parse(
  328. localStorage.getItem("admin-sidebar-active")
  329. );
  330. if (this.sidebarActive === null)
  331. this.sidebarActive = !(document.body.clientWidth <= 768);
  332. this.calculateSidebarPadding();
  333. window.addEventListener("scroll", this.calculateSidebarPadding);
  334. keyboardShortcuts.registerShortcut(
  335. "admin.toggleKeyboardShortcutsHelper",
  336. {
  337. keyCode: 191, // '/' key
  338. ctrl: true,
  339. preventDefault: true,
  340. handler: () => {
  341. this.toggleKeyboardShortcutsHelper();
  342. }
  343. }
  344. );
  345. keyboardShortcuts.registerShortcut(
  346. "admin.resetKeyboardShortcutsHelper",
  347. {
  348. keyCode: 191, // '/' key
  349. ctrl: true,
  350. shift: true,
  351. preventDefault: true,
  352. handler: () => {
  353. this.resetKeyboardShortcutsHelper();
  354. }
  355. }
  356. );
  357. },
  358. beforeUnmount() {
  359. this.socket.dispatch("apis.leaveRooms");
  360. window.removeEventListener("scroll", this.calculateSidebarPadding);
  361. const shortcutNames = [
  362. "admin.toggleKeyboardShortcutsHelper",
  363. "admin.resetKeyboardShortcutsHelper"
  364. ];
  365. shortcutNames.forEach(shortcutName => {
  366. keyboardShortcuts.unregisterShortcut(shortcutName);
  367. });
  368. },
  369. methods: {
  370. onRouteChange() {
  371. if (this.currentTab.startsWith("songs")) {
  372. this.toggleChildren({ child: "songs", force: false });
  373. } else if (this.currentTab.startsWith("users")) {
  374. this.toggleChildren({ child: "users", force: false });
  375. }
  376. this.currentTab = this.getTabFromPath();
  377. if (this.$refs[`${this.currentTab}-tab`])
  378. this.$refs[`${this.currentTab}-tab`].scrollIntoView({
  379. inline: "center",
  380. block: "nearest"
  381. });
  382. localStorage.setItem("lastAdminPage", this.currentTab);
  383. if (this.currentTab.startsWith("songs"))
  384. this.toggleChildren({ child: "songs", force: true });
  385. else if (this.currentTab.startsWith("users"))
  386. this.toggleChildren({ child: "users", force: true });
  387. },
  388. toggleKeyboardShortcutsHelper() {
  389. this.$refs.keyboardShortcutsHelper.toggleBox();
  390. },
  391. resetKeyboardShortcutsHelper() {
  392. this.$refs.keyboardShortcutsHelper.resetBox();
  393. },
  394. toggleSidebar() {
  395. this.sidebarActive = !this.sidebarActive;
  396. localStorage.setItem("admin-sidebar-active", this.sidebarActive);
  397. },
  398. getTabFromPath(path) {
  399. const localPath = path || this.$route.path;
  400. return localPath.substr(0, 7) === "/admin/"
  401. ? localPath.substr(7, localPath.length)
  402. : null;
  403. },
  404. calculateSidebarPadding() {
  405. const scrollTop =
  406. document.documentElement.scrollTop || document.scrollTop || 0;
  407. if (scrollTop <= 64) this.sidebarPadding = 64 - scrollTop;
  408. else this.sidebarPadding = 0;
  409. },
  410. ...mapActions("admin", ["toggleChildren"])
  411. }
  412. };
  413. </script>
  414. <style lang="less" scoped>
  415. .night-mode {
  416. .main-container .admin-area {
  417. .admin-sidebar .inner {
  418. .top {
  419. background-color: var(--dark-grey-3);
  420. }
  421. .bottom {
  422. background-color: var(--dark-grey-2);
  423. .sidebar-item {
  424. background-color: var(--dark-grey-2);
  425. border-color: var(--dark-grey-3);
  426. &,
  427. &.with-children .sidebar-item-child,
  428. &.with-children > span > a {
  429. color: var(--white);
  430. }
  431. }
  432. }
  433. }
  434. :deep(.admin-content .admin-container .admin-tab-container) {
  435. .admin-tab {
  436. .card {
  437. background-color: var(--dark-grey-3);
  438. p {
  439. color: var(--light-grey-2);
  440. }
  441. }
  442. }
  443. }
  444. }
  445. }
  446. .main-container {
  447. height: auto;
  448. .admin-area {
  449. display: flex;
  450. flex-direction: column;
  451. min-height: 100vh;
  452. :deep(.nav) {
  453. .nav-menu.is-active {
  454. left: 45px;
  455. }
  456. &.admin-sidebar-active .nav-menu.is-active {
  457. left: 200px;
  458. }
  459. }
  460. .admin-sidebar {
  461. display: flex;
  462. min-width: 200px;
  463. width: 200px;
  464. @media screen and (max-width: 768px) {
  465. min-width: 45px;
  466. width: 45px;
  467. }
  468. .inner {
  469. display: flex;
  470. flex-direction: column;
  471. max-height: 100vh;
  472. width: 200px;
  473. position: sticky;
  474. top: 0;
  475. bottom: 0;
  476. left: 0;
  477. z-index: 5;
  478. box-shadow: @box-shadow;
  479. .bottom {
  480. overflow-y: auto;
  481. height: 100%;
  482. max-height: 100%;
  483. display: flex;
  484. flex-direction: column;
  485. flex: 1 0 auto;
  486. background-color: var(--white);
  487. .sidebar-item {
  488. display: flex;
  489. padding: 0 20px;
  490. line-height: 40px;
  491. font-size: 16px;
  492. font-weight: 600;
  493. color: var(--primary-color);
  494. background-color: var(--white);
  495. border-bottom: 1px solid var(--light-grey-2);
  496. transition: filter 0.2s ease-in-out;
  497. & > .material-icons {
  498. line-height: 40px;
  499. margin-right: 5px;
  500. }
  501. &:hover,
  502. &:focus,
  503. &.router-link-active,
  504. &.is-active {
  505. filter: brightness(95%);
  506. }
  507. &.toggle-sidebar {
  508. cursor: pointer;
  509. font-weight: 400;
  510. }
  511. &.with-children {
  512. flex-direction: column;
  513. & > span {
  514. display: flex;
  515. line-height: 40px;
  516. cursor: pointer;
  517. & > a {
  518. display: flex;
  519. }
  520. & > .material-icons,
  521. & > a > .material-icons {
  522. line-height: 40px;
  523. margin-right: 5px;
  524. }
  525. }
  526. .toggle-sidebar-children {
  527. margin-left: auto;
  528. }
  529. .sidebar-item-children {
  530. display: none;
  531. }
  532. &.is-active .sidebar-item-children {
  533. display: flex;
  534. flex-direction: column;
  535. .sidebar-item-child {
  536. display: flex;
  537. flex-direction: column;
  538. margin-left: 30px;
  539. font-size: 14px;
  540. line-height: 30px;
  541. position: relative;
  542. &::before {
  543. content: "";
  544. position: absolute;
  545. width: 1px;
  546. height: 30px;
  547. top: 0;
  548. left: -20px;
  549. background-color: var(--light-grey-3);
  550. }
  551. &:last-child::before {
  552. height: 16px;
  553. }
  554. &::after {
  555. content: "";
  556. position: absolute;
  557. width: 15px;
  558. height: 1px;
  559. top: 15px;
  560. left: -20px;
  561. background-color: var(--light-grey-3);
  562. }
  563. &.router-link-active {
  564. filter: brightness(95%);
  565. }
  566. }
  567. }
  568. }
  569. }
  570. }
  571. }
  572. &.minimised {
  573. min-width: 45px;
  574. width: 45px;
  575. .inner {
  576. max-width: 45px;
  577. .top {
  578. justify-content: center;
  579. .full-logo {
  580. display: none;
  581. }
  582. .minimised-logo {
  583. display: flex;
  584. }
  585. }
  586. .sidebar-item {
  587. justify-content: center;
  588. padding: 0;
  589. & > span {
  590. display: none;
  591. }
  592. }
  593. }
  594. }
  595. }
  596. .admin-content {
  597. display: flex;
  598. flex-direction: row;
  599. flex-grow: 1;
  600. .admin-container {
  601. display: flex;
  602. flex-direction: column;
  603. flex-grow: 1;
  604. overflow: hidden;
  605. :deep(.admin-tab-container) {
  606. display: flex;
  607. flex-direction: column;
  608. flex: 1 0 auto;
  609. padding: 10px 10px 20px 10px;
  610. .admin-tab {
  611. display: flex;
  612. flex-direction: column;
  613. width: 100%;
  614. max-width: 1900px;
  615. margin: 0 auto;
  616. padding: 0 10px;
  617. .card {
  618. display: flex;
  619. flex-grow: 1;
  620. flex-direction: column;
  621. padding: 20px;
  622. margin: 10px 0;
  623. border-radius: @border-radius;
  624. background-color: var(--white);
  625. color: var(--dark-grey);
  626. box-shadow: @box-shadow;
  627. h1 {
  628. font-size: 36px;
  629. margin: 0 0 5px 0;
  630. }
  631. h4 {
  632. font-size: 22px;
  633. margin: 0;
  634. }
  635. h5 {
  636. font-size: 18px;
  637. margin: 0;
  638. }
  639. hr {
  640. margin: 10px 0;
  641. }
  642. &.tab-info {
  643. flex-direction: row;
  644. flex-wrap: wrap;
  645. .info-row {
  646. display: flex;
  647. flex-grow: 1;
  648. flex-direction: column;
  649. }
  650. .button-row {
  651. display: flex;
  652. flex-direction: row;
  653. flex-wrap: wrap;
  654. justify-content: center;
  655. margin: auto 0;
  656. padding: 5px 0;
  657. & > .button,
  658. & > span {
  659. margin: auto 0;
  660. &:not(:first-child) {
  661. margin-left: 5px;
  662. }
  663. }
  664. & > span > .control.has-addons {
  665. margin-bottom: 0 !important;
  666. }
  667. }
  668. }
  669. }
  670. @media screen and (min-width: 980px) {
  671. &.container {
  672. margin: 0 auto;
  673. max-width: 960px;
  674. }
  675. }
  676. @media screen and (min-width: 1180px) {
  677. &.container {
  678. max-width: 1200px;
  679. }
  680. }
  681. }
  682. }
  683. }
  684. }
  685. }
  686. }
  687. :deep(.box) {
  688. box-shadow: @box-shadow;
  689. display: block;
  690. &:not(:last-child) {
  691. margin-bottom: 20px;
  692. }
  693. }
  694. #keyboardShortcutsHelper {
  695. .box-body {
  696. .biggest {
  697. font-size: 18px;
  698. }
  699. .bigger {
  700. font-size: 16px;
  701. }
  702. span {
  703. display: block;
  704. }
  705. }
  706. }
  707. </style>