MainHeader.vue 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <script setup lang="ts">
  2. import { defineAsyncComponent, ref, onMounted, watch, nextTick } from "vue";
  3. import Toast from "toasters";
  4. import { storeToRefs } from "pinia";
  5. import { useWebsocketsStore } from "@/stores/websockets";
  6. import { useConfigStore } from "@/stores/config";
  7. import { useUserAuthStore } from "@/stores/userAuth";
  8. import { useUserPreferencesStore } from "@/stores/userPreferences";
  9. import { useModalsStore } from "@/stores/modals";
  10. const ChristmasLights = defineAsyncComponent(
  11. () => import("@/components/ChristmasLights.vue")
  12. );
  13. defineProps({
  14. hideLogo: { type: Boolean, default: false },
  15. transparent: { type: Boolean, default: false },
  16. hideLoggedOut: { type: Boolean, default: false }
  17. });
  18. const userAuthStore = useUserAuthStore();
  19. const localNightmode = ref(false);
  20. const isMobile = ref(false);
  21. const windowWidth = ref(0);
  22. const broadcastChannel = ref();
  23. const { socket } = useWebsocketsStore();
  24. const configStore = useConfigStore();
  25. const { cookie, sitename, registrationDisabled, christmas } =
  26. storeToRefs(configStore);
  27. const { loggedIn, username } = storeToRefs(userAuthStore);
  28. const { logout, hasPermission } = userAuthStore;
  29. const userPreferencesStore = useUserPreferencesStore();
  30. const { nightmode } = storeToRefs(userPreferencesStore);
  31. const { openModal } = useModalsStore();
  32. const toggleNightmode = toggle => {
  33. localNightmode.value =
  34. toggle === undefined ? !localNightmode.value : toggle;
  35. if (loggedIn.value) {
  36. socket.dispatch(
  37. "users.updatePreferences",
  38. { nightmode: localNightmode.value },
  39. res => {
  40. if (res.status !== "success") new Toast(res.message);
  41. }
  42. );
  43. } else {
  44. broadcastChannel.value.postMessage(localNightmode.value);
  45. }
  46. };
  47. const onResize = () => {
  48. windowWidth.value = window.innerWidth;
  49. };
  50. watch(nightmode, value => {
  51. localNightmode.value = value;
  52. });
  53. onMounted(async () => {
  54. localNightmode.value = nightmode.value;
  55. await nextTick();
  56. onResize();
  57. window.addEventListener("resize", onResize);
  58. broadcastChannel.value = new BroadcastChannel(`${cookie.value}.nightmode`);
  59. });
  60. </script>
  61. <template>
  62. <nav
  63. class="nav is-info"
  64. :class="{ transparent, 'hide-logged-out': !loggedIn && hideLoggedOut }"
  65. >
  66. <div class="nav-left">
  67. <router-link v-if="!hideLogo" class="nav-item is-brand" to="/">
  68. <img
  69. v-if="sitename === 'Musare'"
  70. src="/assets/white_wordmark.png"
  71. :alt="sitename"
  72. />
  73. <span v-else>{{ sitename }}</span>
  74. </router-link>
  75. </div>
  76. <span
  77. v-if="loggedIn || !hideLoggedOut"
  78. class="nav-toggle"
  79. :class="{ 'is-active': isMobile }"
  80. tabindex="0"
  81. @click="isMobile = !isMobile"
  82. @keyup.enter="isMobile = !isMobile"
  83. >
  84. <span />
  85. <span />
  86. <span />
  87. </span>
  88. <div class="nav-right nav-menu" :class="{ 'is-active': isMobile }">
  89. <div
  90. class="nav-item"
  91. id="nightmode-toggle"
  92. @click="toggleNightmode(!localNightmode)"
  93. >
  94. <span
  95. :class="{
  96. 'material-icons': true,
  97. 'night-mode-toggle': true,
  98. 'night-mode-on': localNightmode
  99. }"
  100. :content="`${
  101. localNightmode ? 'Disable' : 'Enable'
  102. } Nightmode`"
  103. v-tippy
  104. >
  105. {{ localNightmode ? "dark_mode" : "light_mode" }}
  106. </span>
  107. <span class="night-mode-label">Toggle Nightmode</span>
  108. </div>
  109. <span v-if="loggedIn" class="grouped">
  110. <router-link
  111. v-if="hasPermission('admin.view')"
  112. class="nav-item admin"
  113. to="/admin"
  114. >
  115. <strong>Admin</strong>
  116. </router-link>
  117. <router-link
  118. class="nav-item"
  119. :to="{
  120. name: 'profile',
  121. params: { username },
  122. query: { tab: 'playlists' }
  123. }"
  124. >
  125. Playlists
  126. </router-link>
  127. <router-link
  128. class="nav-item"
  129. :to="{
  130. name: 'profile',
  131. params: { username }
  132. }"
  133. >
  134. Profile
  135. </router-link>
  136. <router-link class="nav-item" to="/settings"
  137. >Settings</router-link
  138. >
  139. <a class="nav-item" @click="logout()">Logout</a>
  140. </span>
  141. <span v-if="!loggedIn && !hideLoggedOut" class="grouped">
  142. <a class="nav-item" @click="openModal('login')">Login</a>
  143. <a
  144. v-if="!registrationDisabled"
  145. class="nav-item"
  146. @click="openModal('register')"
  147. >Register</a
  148. >
  149. </span>
  150. </div>
  151. <christmas-lights
  152. v-if="christmas"
  153. :lights="Math.min(Math.max(Math.floor(windowWidth / 175), 5), 15)"
  154. />
  155. </nav>
  156. </template>
  157. <style lang="less" scoped>
  158. .night-mode {
  159. .nav {
  160. background-color: var(--dark-grey-3) !important;
  161. }
  162. @media screen and (max-width: 768px) {
  163. .nav:not(.hide-logged-out) .nav-menu {
  164. background-color: var(--dark-grey-3) !important;
  165. }
  166. }
  167. .nav-item {
  168. color: var(--light-grey-2) !important;
  169. }
  170. }
  171. .nav {
  172. flex-shrink: 0;
  173. display: flex;
  174. position: relative;
  175. background-color: var(--primary-color);
  176. height: 64px;
  177. z-index: 100;
  178. &.transparent {
  179. background-color: transparent !important;
  180. }
  181. .nav-left,
  182. .nav-right {
  183. flex: 1;
  184. display: flex;
  185. }
  186. .nav-right {
  187. justify-content: flex-end;
  188. }
  189. a.nav-item.is-tab:hover {
  190. border-bottom: none;
  191. border-top: solid 1px var(--white);
  192. padding-top: 9px;
  193. }
  194. .nav-toggle {
  195. height: 64px;
  196. width: 50px;
  197. position: relative;
  198. background-color: transparent;
  199. display: none;
  200. position: relative;
  201. cursor: pointer;
  202. &.is-active {
  203. span:nth-child(1) {
  204. margin-left: -5px;
  205. transform: rotate(45deg);
  206. transform-origin: left top;
  207. }
  208. span:nth-child(2) {
  209. opacity: 0;
  210. }
  211. span:nth-child(3) {
  212. margin-left: -5px;
  213. transform: rotate(-45deg);
  214. transform-origin: left bottom;
  215. }
  216. }
  217. span {
  218. background-color: var(--white) !important;
  219. display: block;
  220. height: 1px;
  221. left: 50%;
  222. margin-left: -7px;
  223. position: absolute;
  224. top: 50%;
  225. width: 15px;
  226. transition: none 86ms ease-out;
  227. transition-property: opacity, transform;
  228. &:nth-child(1) {
  229. margin-top: -6px;
  230. }
  231. &:nth-child(2) {
  232. margin-top: -1px;
  233. }
  234. &:nth-child(3) {
  235. margin-top: 4px;
  236. }
  237. }
  238. }
  239. .nav-item {
  240. font-size: 17px;
  241. color: var(--white);
  242. border-top: 0;
  243. display: flex;
  244. align-items: center;
  245. padding: 10px;
  246. cursor: pointer;
  247. &:hover,
  248. &:focus {
  249. color: var(--white);
  250. }
  251. &.is-brand {
  252. font-size: 2.1rem !important;
  253. line-height: 38px !important;
  254. padding: 0 20px;
  255. font-family: Pacifico, cursive;
  256. display: flex;
  257. align-items: center;
  258. img {
  259. max-height: 38px;
  260. color: var(--primary-color);
  261. user-select: none;
  262. -webkit-user-drag: none;
  263. }
  264. }
  265. .night-mode-label {
  266. display: none;
  267. }
  268. }
  269. }
  270. .grouped {
  271. margin: 0;
  272. display: flex;
  273. text-decoration: none;
  274. .nav-item {
  275. &:hover,
  276. &:focus {
  277. border-top: 1px solid var(--white);
  278. height: calc(100% - 1px);
  279. }
  280. }
  281. }
  282. @media screen and (max-width: 768px) {
  283. .nav:not(.hide-logged-out) {
  284. .nav-toggle {
  285. display: block !important;
  286. }
  287. .nav-menu {
  288. display: none !important;
  289. box-shadow: @box-shadow-dropdown;
  290. left: 0;
  291. right: 0;
  292. top: 100%;
  293. position: absolute;
  294. background: var(--white);
  295. z-index: 100;
  296. }
  297. .nav-menu.is-active {
  298. display: flex !important;
  299. flex-direction: column-reverse;
  300. .nav-item {
  301. color: var(--dark-grey-2);
  302. &:hover {
  303. color: var(--dark-grey-2);
  304. }
  305. .night-mode-label {
  306. display: inline;
  307. margin-left: 5px;
  308. }
  309. }
  310. }
  311. .nav-menu {
  312. .grouped {
  313. flex-direction: column;
  314. }
  315. .nav-item {
  316. padding: 10px 20px;
  317. &:hover,
  318. &:focus {
  319. border-top: 0;
  320. height: unset;
  321. }
  322. }
  323. }
  324. }
  325. }
  326. </style>