index.vue 18 KB

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