index.vue 18 KB

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