From 902c439fede709985663181bda748cc595a867d5 Mon Sep 17 00:00:00 2001 From: Phan An Date: Fri, 5 Apr 2024 00:20:42 +0200 Subject: [PATCH] feat(ui): use Tailwind CSS --- app/Repositories/SongRepository.php | 1 + app/Services/SongStorages/CloudStorage.php | 5 +- app/Services/SongStorages/DropboxStorage.php | 8 +- app/Services/SongStorages/LocalStorage.php | 3 +- .../SongStorages/S3CompatibleStorage.php | 8 +- app/Services/SongStorages/S3LambdaStorage.php | 4 +- app/Services/SongStorages/SongStorage.php | 14 +- app/Services/YouTubeService.php | 15 +- app/Values/PlaylistCollaborator.php | 2 +- package.json | 2 + postcss.config.cjs | 2 + resources/assets/css/app.pcss | 210 +------- .../assets/css/partials/context-menu.pcss | 26 + resources/assets/css/partials/hack.pcss | 28 +- resources/assets/css/partials/mixins.pcss | 7 + resources/assets/css/partials/shared.pcss | 424 ++-------------- resources/assets/css/partials/skeleton.pcss | 6 +- resources/assets/css/partials/tooltip.pcss | 32 +- resources/assets/css/partials/vars.pcss | 26 +- resources/assets/css/remote.pcss | 11 + resources/assets/css/vendor/alertify.pcss | 21 - resources/assets/css/vendor/nprogress.pcss | 32 +- resources/assets/js/App.vue | 65 +-- resources/assets/js/__tests__/stubs.ts | 2 +- .../assets/js/components/album/AlbumCard.vue | 12 +- .../assets/js/components/album/AlbumInfo.vue | 73 ++- .../js/components/album/AlbumTrackList.vue | 46 +- .../components/album/AlbumTrackListItem.vue | 27 +- .../__snapshots__/AlbumCard.spec.ts.snap | 8 +- .../AlbumContextMenu.spec.ts.snap | 2 +- .../AlbumTrackListItem.spec.ts.snap | 4 +- .../js/components/artist/ArtistCard.vue | 17 +- .../js/components/artist/ArtistInfo.vue | 69 +-- .../__snapshots__/ArtistCard.spec.ts.snap | 8 +- .../ArtistContextMenu.spec.ts.snap | 2 +- .../js/components/auth/ForgotPasswordForm.vue | 71 +-- .../assets/js/components/auth/LoginForm.vue | 86 +--- .../js/components/auth/ResetPasswordForm.vue | 56 +-- .../auth/__snapshots__/LoginForm.spec.ts.snap | 42 +- .../components/auth/sso/GoogleLoginButton.vue | 17 +- .../invitation/AcceptInvitation.vue | 110 ++-- .../koel-plus/ActivateLicenseForm.vue | 35 +- .../components/koel-plus/BtnUpgradeToPlus.vue | 26 +- .../js/components/koel-plus/KoelPlusModal.vue | 93 +--- .../js/components/layout/ModalWrapper.vue | 59 +-- .../layout/app-footer/AudioPlayer.vue | 43 +- .../layout/app-footer/FooterButton.vue | 9 + .../layout/app-footer/FooterExtraControls.vue | 62 +-- .../app-footer/FooterPlaybackControls.vue | 60 +-- .../layout/app-footer/FooterSongInfo.vue | 77 +-- .../FooterExtraControls.spec.ts.snap | 6 +- .../FooterPlaybackControls.spec.ts.snap | 8 +- .../__snapshots__/FooterSongInfo.spec.ts.snap | 8 +- .../js/components/layout/app-footer/index.vue | 70 +-- .../layout/main-wrapper/ExtraDrawer.vue | 253 ---------- .../layout/main-wrapper/MainContent.vue | 62 +-- .../extra-drawer/AboutKoelButton.vue | 25 + .../{ => extra-drawer}/ExtraDrawer.spec.ts | 0 .../main-wrapper/extra-drawer/ExtraDrawer.vue | 150 ++++++ .../extra-drawer/ExtraDrawerButton.vue | 24 + .../ExtraDrawerTabHeader.spec.ts | 0 .../extra-drawer}/ExtraDrawerTabHeader.vue | 25 +- .../extra-drawer/LogoutButton.vue | 14 + .../__snapshots__/ExtraDrawer.spec.ts.snap | 15 + .../components/layout/main-wrapper/index.vue | 14 +- .../sidebar/PlaylistFolderSidebarItem.vue | 75 ++- .../sidebar/PlaylistSidebarItem.vue | 48 +- .../sidebar/PlaylistSidebarList.vue | 82 --- .../main-wrapper/sidebar/QueueSidebarItem.vue | 4 +- .../main-wrapper/sidebar/Sidebar.spec.ts | 2 +- .../layout/main-wrapper/sidebar/Sidebar.vue | 247 ++------- .../main-wrapper/sidebar/SidebarItem.vue | 47 +- .../sidebar/SidebarManageSection.vue | 38 ++ ...pec.ts => SidebarPlaylistsSection.spec.ts} | 4 +- .../sidebar/SidebarPlaylistsSection.vue | 38 ++ .../main-wrapper/sidebar/SidebarSection.vue | 6 + .../sidebar/SidebarSectionHeader.vue | 5 + .../sidebar/SidebarToggleButton.vue | 24 + .../sidebar/SidebarYourMusicSection.vue | 62 +++ .../sidebar/YouTubeSidebarItem.vue | 10 +- .../__snapshots__/SidebarItem.spec.ts.snap | 2 +- .../YouTubeSidebarItem.spec.ts.snap | 2 +- .../js/components/meta/AboutKoelModal.vue | 123 +---- .../js/components/meta/CreditsBlock.vue | 45 ++ .../assets/js/components/meta/SponsorList.vue | 36 +- .../assets/js/components/meta/SupportKoel.vue | 55 +- .../__snapshots__/AboutKoelModal.spec.ts.snap | 2 +- ...c.ts => CreatePlaylistContextMenu.spec.ts} | 3 +- ...Menu.vue => CreatePlaylistContextMenu.vue} | 0 .../CreatePlaylistContextMenuButton.vue | 24 + .../playlist/CreatePlaylistFolderForm.vue | 13 +- .../playlist/CreatePlaylistForm.vue | 52 +- .../playlist/EditPlaylistFolderForm.vue | 18 +- .../components/playlist/EditPlaylistForm.vue | 30 +- .../playlist/InvitePlaylistCollaborators.vue | 20 +- .../playlist/PlaylistCollaborationModal.vue | 36 +- .../playlist/PlaylistCollaboratorList.vue | 18 +- .../playlist/PlaylistCollaboratorListItem.vue | 75 +-- .../playlist/PlaylistCollaboratorsBadge.vue | 35 +- .../playlist/PlaylistContextMenu.vue | 14 +- .../PlaylistCollaborationModal.spec.ts.snap | 2 +- .../CreateSmartPlaylistForm.vue | 44 +- .../smart-playlist/EditSmartPlaylistForm.vue | 39 +- .../smart-playlist/SmartPlaylistFormBase.vue | 30 +- .../smart-playlist/SmartPlaylistRule.vue | 83 ++- .../smart-playlist/SmartPlaylistRuleGroup.vue | 68 ++- .../smart-playlist/SmartPlaylistRuleInput.vue | 9 +- .../EditableProfileAvatar.vue | 100 +--- .../profile-preferences/Integrations.vue | 16 +- .../profile-preferences/LastfmIntegration.vue | 38 +- .../profile-preferences/PreferencesForm.vue | 55 +- .../profile-preferences/ProfileForm.vue | 146 ++---- .../profile-preferences/QRLogin.vue | 14 +- .../SpotifyIntegration.vue | 21 +- .../profile-preferences/ThemeCard.vue | 52 +- .../profile-preferences/ThemeList.vue | 19 +- .../SpotifyIntegration.spec.ts.snap | 32 +- .../__snapshots__/ThemeCard.spec.ts.snap | 6 +- .../js/components/screens/AlbumListScreen.vue | 49 +- .../js/components/screens/AlbumScreen.vue | 94 ++-- .../js/components/screens/AllSongsScreen.vue | 95 ++-- .../components/screens/ArtistListScreen.vue | 53 +- .../js/components/screens/ArtistScreen.vue | 91 ++-- .../js/components/screens/FavoritesScreen.vue | 68 +-- .../js/components/screens/GenreListScreen.vue | 105 ++-- .../js/components/screens/GenreScreen.vue | 50 +- .../js/components/screens/HomeScreen.vue | 106 ++-- .../js/components/screens/NotFoundScreen.vue | 25 +- .../js/components/screens/PlaylistScreen.vue | 76 +-- .../js/components/screens/ProfileScreen.vue | 171 +++---- .../js/components/screens/QueueScreen.vue | 52 +- .../screens/RecentlyPlayedScreen.vue | 57 ++- .../js/components/screens/ScreenBase.vue | 27 + .../js/components/screens/SettingsScreen.vue | 69 ++- .../js/components/screens/UploadScreen.vue | 136 +++-- .../components/screens/UserListScreen.spec.ts | 5 +- .../js/components/screens/UserListScreen.vue | 129 ++--- .../components/screens/VisualizerScreen.vue | 103 ++-- .../js/components/screens/YouTubeScreen.vue | 44 +- .../__snapshots__/AllSongsScreen.spec.ts.snap | 72 +-- .../__snapshots__/SettingsScreen.spec.ts.snap | 25 +- .../screens/home/HomeScreenBlock.vue | 10 + .../screens/home/MostPlayedAlbums.vue | 14 +- .../screens/home/MostPlayedArtists.vue | 13 +- .../screens/home/MostPlayedSongs.vue | 15 +- .../screens/home/RecentlyAddedAlbums.vue | 14 +- .../screens/home/RecentlyAddedSongs.vue | 15 +- .../screens/home/RecentlyPlayedSongs.vue | 32 +- .../screens/home/ViewAllRecentSongsButton.vue | 18 + .../search/AlbumExcerptResultsBlock.vue | 40 ++ .../search/ArtistExcerptResultsBlock.vue | 40 ++ .../screens/search/ExcerptResultBlock.vue | 8 + .../screens/search/SearchExcerptsScreen.vue | 142 +----- .../search/SearchSongResultsScreen.vue | 58 ++- .../search/SongExcerptResultsBlock.vue | 59 +++ .../js/components/song/AddToMenu.spec.ts | 2 +- .../assets/js/components/song/AddToMenu.vue | 70 +-- .../js/components/song/EditSongForm.vue | 211 ++++---- .../assets/js/components/song/SongCard.vue | 120 +---- .../components/song/SongContextMenu.spec.ts | 2 +- .../js/components/song/SongContextMenu.vue | 33 +- .../js/components/song/SongLikeButton.vue | 5 +- .../assets/js/components/song/SongList.vue | 129 ++--- .../js/components/song/SongListControls.vue | 45 +- .../js/components/song/SongListFilter.vue | 50 +- .../js/components/song/SongListItem.vue | 80 +-- .../js/components/song/SongListSorter.vue | 39 +- .../js/components/song/SongThumbnail.vue | 79 +-- .../song/__snapshots__/AddToMenu.spec.ts.snap | 8 +- .../__snapshots__/EditSongForm.spec.ts.snap | 80 ++- .../song/__snapshots__/SongList.spec.ts.snap | 4 +- .../__snapshots__/SongListItem.spec.ts.snap | 6 +- .../js/components/ui/AlbumArtOverlay.vue | 17 +- .../assets/js/components/ui/AlertBox.vue | 33 +- .../js/components/ui/AppleMusicButton.vue | 27 +- .../js/components/ui/ArtistAlbumCard.vue | 89 +--- .../components/ui/ArtistAlbumScreenTabs.vue | 53 +- .../js/components/ui/ArtistAlbumThumbnail.vue | 140 ++---- resources/assets/js/components/ui/Btn.vue | 79 --- .../js/components/ui/BtnCloseModal.spec.ts | 18 - .../assets/js/components/ui/BtnCloseModal.vue | 33 -- .../assets/js/components/ui/BtnGroup.vue | 37 -- .../js/components/ui/BtnScrollToTop.vue | 30 +- .../assets/js/components/ui/CheckBox.vue | 65 --- .../js/components/ui/ContextMenuBase.vue | 12 +- .../assets/js/components/ui/DialogBox.vue | 109 ++-- .../assets/js/components/ui/Equalizer.vue | 276 ---------- .../assets/js/components/ui/ExternalMark.vue | 9 + .../js/components/ui/FooterPlayButton.vue | 29 +- .../assets/js/components/ui/LyricsPane.vue | 81 ++- .../assets/js/components/ui/Magnifier.vue | 36 +- .../assets/js/components/ui/MessageToast.vue | 110 ---- .../js/components/ui/OfflineNotification.vue | 46 +- resources/assets/js/components/ui/Overlay.vue | 36 +- .../js/components/ui/PlaylistThumbnail.vue | 30 +- .../assets/js/components/ui/ProfileAvatar.vue | 22 +- .../js/components/ui/RepeatModeSwitch.vue | 17 +- .../js/components/ui/ScreenControlsToggle.vue | 18 +- .../js/components/ui/ScreenEmptyState.vue | 59 +-- .../assets/js/components/ui/ScreenHeader.vue | 110 +--- .../assets/js/components/ui/SearchForm.vue | 78 +-- .../components/ui/SidebarMenuToggleButton.vue | 6 +- .../assets/js/components/ui/SoundBars.vue | 20 +- .../js/components/ui/ThumbnailStack.vue | 31 +- .../assets/js/components/ui/TooltipIcon.vue | 2 +- .../js/components/ui/ViewModeSwitch.vue | 29 +- .../js/components/ui/VirtualScroller.vue | 23 +- .../js/components/ui/VolumeSlider.spec.ts | 2 +- .../assets/js/components/ui/VolumeSlider.vue | 58 +-- .../js/components/ui/YouTubeVideoItem.vue | 55 -- .../AlbumArtOverlay.spec.ts.snap | 4 +- .../AppleMusicButton.spec.ts.snap | 2 +- .../ArtistAlbumThumbnail.spec.ts.snap | 4 +- .../__snapshots__/BtnScrollToTop.spec.ts.snap | 2 +- .../ui/__snapshots__/LyricsPane.spec.ts.snap | 8 +- .../ui/__snapshots__/Magnifier.spec.ts.snap | 2 +- .../ScreenEmptyState.spec.ts.snap | 8 +- .../__snapshots__/ScreenHeader.spec.ts.snap | 10 +- .../__snapshots__/ViewModeSwitch.spec.ts.snap | 4 +- .../__snapshots__/VolumeSlider.spec.ts.snap | 6 +- .../ui/album-artist/AlbumOrArtistGrid.vue | 26 + .../ui/album-artist/AlbumOrArtistInfo.vue | 27 + .../album-artist/ExpandableContentBlock.vue | 19 + .../js/components/ui/equalizer/Equalizer.vue | 105 ++++ .../components/ui/equalizer/EqualizerBand.vue | 140 ++++++ .../js/components/ui/{ => form}/Btn.spec.ts | 0 .../assets/js/components/ui/form/Btn.vue | 75 +++ .../components/ui/{ => form}/BtnGroup.spec.ts | 0 .../assets/js/components/ui/form/BtnGroup.vue | 31 ++ .../components/ui/{ => form}/CheckBox.spec.ts | 0 .../assets/js/components/ui/form/CheckBox.vue | 22 + .../assets/js/components/ui/form/FormRow.vue | 39 ++ .../ui/{ => form}/PasswordField.vue | 26 +- .../js/components/ui/form/SelectBox.vue | 41 ++ .../assets/js/components/ui/form/TextArea.vue | 25 + .../js/components/ui/form/TextInput.vue | 27 + .../ui/form/__snapshots__/Btn.spec.ts.snap | 3 + .../form/__snapshots__/BtnGroup.spec.ts.snap | 3 + .../form/__snapshots__/CheckBox.spec.ts.snap | 5 + .../MessageToast.spec.ts | 0 .../ui/message-toaster/MessageToast.vue | 76 +++ .../{ => message-toaster}/MessageToaster.vue | 23 +- .../__snapshots__/MessageToast.spec.ts.snap | 8 + .../ui/skeletons/ArtistAlbumCardSkeleton.vue | 58 +-- .../ui/skeletons/GenreItemSkeleton.vue | 30 +- .../PlaylistCollaboratorListSkeleton.vue | 21 +- .../ui/skeletons/ScreenHeaderSkeleton.vue | 54 +- .../ui/skeletons/SongCardSkeleton.vue | 52 +- .../ui/skeletons/SongListSkeleton.vue | 132 +++-- .../js/components/ui/tabs/TabButton.vue | 21 + .../assets/js/components/ui/tabs/TabList.vue | 5 + .../assets/js/components/ui/tabs/TabPanel.vue | 5 + .../components/ui/tabs/TabPanelContainer.vue | 5 + .../assets/js/components/ui/tabs/Tabs.vue | 5 + .../js/components/ui/upload/DropZone.vue | 35 +- .../components/ui/upload/UploadItem.spec.ts | 2 +- .../js/components/ui/upload/UploadItem.vue | 90 +--- .../__snapshots__/UploadItem.spec.ts.snap | 2 +- .../ui/{ => youtube}/YouTubeVideoItem.spec.ts | 0 .../ui/youtube/YouTubeVideoItem.vue | 40 ++ .../ui/{ => youtube}/YouTubeVideoList.spec.ts | 4 +- .../ui/{ => youtube}/YouTubeVideoList.vue | 23 +- .../YouTubeVideoItem.spec.ts.snap | 10 + .../assets/js/components/user/AddUserForm.vue | 71 ++- .../js/components/user/EditUserForm.vue | 84 ++-- .../js/components/user/InviteUserForm.vue | 28 +- .../assets/js/components/user/UserAvatar.vue | 16 +- .../assets/js/components/user/UserCard.vue | 75 +-- .../js/components/utils/ImageCropper.vue | 46 +- resources/assets/js/composables/index.ts | 1 + .../assets/js/composables/useContextMenu.ts | 2 +- .../js/composables/useInfiniteScroll.ts | 3 +- .../assets/js/composables/useKoelPlus.ts | 2 +- .../js/composables/useMessageToaster.ts | 2 +- resources/assets/js/composables/useOverlay.ts | 4 +- .../assets/js/composables/usePolicies.ts | 27 + .../js/composables/useSmartPlaylistForm.ts | 2 +- resources/assets/js/composables/useUpload.ts | 7 +- resources/assets/js/directives/tooltip.ts | 2 +- resources/assets/js/remote/App.vue | 423 ++-------------- .../js/remote/components/RemoteFooter.vue | 66 +++ .../assets/js/remote/components/Scanner.vue | 49 ++ .../js/remote/components/SongDetails.vue | 31 ++ .../js/remote/components/VolumeControl.vue | 113 +++++ resources/assets/js/remote/types.ts | 4 + resources/assets/js/services/audioService.ts | 7 +- resources/assets/js/services/http.ts | 8 +- .../assets/js/services/volumeManager.spec.ts | 3 +- resources/assets/js/symbols.ts | 2 +- resources/assets/js/themes.ts | 16 + resources/assets/js/utils/fileReader.ts | 11 - resources/assets/js/utils/formatters.ts | 9 + resources/assets/js/utils/index.ts | 1 - .../asteroid/scripts/AudioAnalyzer.ts | 1 + tailwind.config.js | 37 ++ yarn.lock | 476 +++++++++++++++++- 296 files changed, 5204 insertions(+), 7669 deletions(-) create mode 100644 resources/assets/css/partials/context-menu.pcss create mode 100644 resources/assets/css/partials/mixins.pcss delete mode 100644 resources/assets/css/vendor/alertify.pcss create mode 100644 resources/assets/js/components/layout/app-footer/FooterButton.vue delete mode 100644 resources/assets/js/components/layout/main-wrapper/ExtraDrawer.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/extra-drawer/AboutKoelButton.vue rename resources/assets/js/components/layout/main-wrapper/{ => extra-drawer}/ExtraDrawer.spec.ts (100%) create mode 100644 resources/assets/js/components/layout/main-wrapper/extra-drawer/ExtraDrawer.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/extra-drawer/ExtraDrawerButton.vue rename resources/assets/js/components/{ui => layout/main-wrapper/extra-drawer}/ExtraDrawerTabHeader.spec.ts (100%) rename resources/assets/js/components/{ui => layout/main-wrapper/extra-drawer}/ExtraDrawerTabHeader.vue (85%) create mode 100644 resources/assets/js/components/layout/main-wrapper/extra-drawer/LogoutButton.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/extra-drawer/__snapshots__/ExtraDrawer.spec.ts.snap delete mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/PlaylistSidebarList.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarManageSection.vue rename resources/assets/js/components/layout/main-wrapper/sidebar/{PlaylistSidebarList.spec.ts => SidebarPlaylistsSection.spec.ts} (93%) create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarPlaylistsSection.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarSection.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarSectionHeader.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarToggleButton.vue create mode 100644 resources/assets/js/components/layout/main-wrapper/sidebar/SidebarYourMusicSection.vue create mode 100644 resources/assets/js/components/meta/CreditsBlock.vue rename resources/assets/js/components/playlist/{CreateNewPlaylistContextMenu.spec.ts => CreatePlaylistContextMenu.spec.ts} (93%) rename resources/assets/js/components/playlist/{CreateNewPlaylistContextMenu.vue => CreatePlaylistContextMenu.vue} (100%) create mode 100644 resources/assets/js/components/playlist/CreatePlaylistContextMenuButton.vue create mode 100644 resources/assets/js/components/screens/ScreenBase.vue create mode 100644 resources/assets/js/components/screens/home/HomeScreenBlock.vue create mode 100644 resources/assets/js/components/screens/home/ViewAllRecentSongsButton.vue create mode 100644 resources/assets/js/components/screens/search/AlbumExcerptResultsBlock.vue create mode 100644 resources/assets/js/components/screens/search/ArtistExcerptResultsBlock.vue create mode 100644 resources/assets/js/components/screens/search/ExcerptResultBlock.vue create mode 100644 resources/assets/js/components/screens/search/SongExcerptResultsBlock.vue delete mode 100644 resources/assets/js/components/ui/Btn.vue delete mode 100644 resources/assets/js/components/ui/BtnCloseModal.spec.ts delete mode 100644 resources/assets/js/components/ui/BtnCloseModal.vue delete mode 100644 resources/assets/js/components/ui/BtnGroup.vue delete mode 100644 resources/assets/js/components/ui/CheckBox.vue delete mode 100644 resources/assets/js/components/ui/Equalizer.vue create mode 100644 resources/assets/js/components/ui/ExternalMark.vue delete mode 100644 resources/assets/js/components/ui/MessageToast.vue delete mode 100644 resources/assets/js/components/ui/YouTubeVideoItem.vue create mode 100644 resources/assets/js/components/ui/album-artist/AlbumOrArtistGrid.vue create mode 100644 resources/assets/js/components/ui/album-artist/AlbumOrArtistInfo.vue create mode 100644 resources/assets/js/components/ui/album-artist/ExpandableContentBlock.vue create mode 100644 resources/assets/js/components/ui/equalizer/Equalizer.vue create mode 100644 resources/assets/js/components/ui/equalizer/EqualizerBand.vue rename resources/assets/js/components/ui/{ => form}/Btn.spec.ts (100%) create mode 100644 resources/assets/js/components/ui/form/Btn.vue rename resources/assets/js/components/ui/{ => form}/BtnGroup.spec.ts (100%) create mode 100644 resources/assets/js/components/ui/form/BtnGroup.vue rename resources/assets/js/components/ui/{ => form}/CheckBox.spec.ts (100%) create mode 100644 resources/assets/js/components/ui/form/CheckBox.vue create mode 100644 resources/assets/js/components/ui/form/FormRow.vue rename resources/assets/js/components/ui/{ => form}/PasswordField.vue (71%) create mode 100644 resources/assets/js/components/ui/form/SelectBox.vue create mode 100644 resources/assets/js/components/ui/form/TextArea.vue create mode 100644 resources/assets/js/components/ui/form/TextInput.vue create mode 100644 resources/assets/js/components/ui/form/__snapshots__/Btn.spec.ts.snap create mode 100644 resources/assets/js/components/ui/form/__snapshots__/BtnGroup.spec.ts.snap create mode 100644 resources/assets/js/components/ui/form/__snapshots__/CheckBox.spec.ts.snap rename resources/assets/js/components/ui/{ => message-toaster}/MessageToast.spec.ts (100%) create mode 100644 resources/assets/js/components/ui/message-toaster/MessageToast.vue rename resources/assets/js/components/ui/{ => message-toaster}/MessageToaster.vue (77%) create mode 100644 resources/assets/js/components/ui/message-toaster/__snapshots__/MessageToast.spec.ts.snap create mode 100644 resources/assets/js/components/ui/tabs/TabButton.vue create mode 100644 resources/assets/js/components/ui/tabs/TabList.vue create mode 100644 resources/assets/js/components/ui/tabs/TabPanel.vue create mode 100644 resources/assets/js/components/ui/tabs/TabPanelContainer.vue create mode 100644 resources/assets/js/components/ui/tabs/Tabs.vue rename resources/assets/js/components/ui/{ => youtube}/YouTubeVideoItem.spec.ts (100%) create mode 100644 resources/assets/js/components/ui/youtube/YouTubeVideoItem.vue rename resources/assets/js/components/ui/{ => youtube}/YouTubeVideoList.spec.ts (91%) rename resources/assets/js/components/ui/{ => youtube}/YouTubeVideoList.vue (72%) create mode 100644 resources/assets/js/components/ui/youtube/__snapshots__/YouTubeVideoItem.spec.ts.snap create mode 100644 resources/assets/js/composables/usePolicies.ts create mode 100644 resources/assets/js/remote/components/RemoteFooter.vue create mode 100644 resources/assets/js/remote/components/Scanner.vue create mode 100644 resources/assets/js/remote/components/SongDetails.vue create mode 100644 resources/assets/js/remote/components/VolumeControl.vue create mode 100644 resources/assets/js/remote/types.ts delete mode 100644 resources/assets/js/utils/fileReader.ts create mode 100644 tailwind.config.js diff --git a/app/Repositories/SongRepository.php b/app/Repositories/SongRepository.php index 47fbbe08..0f8201c5 100644 --- a/app/Repositories/SongRepository.php +++ b/app/Repositories/SongRepository.php @@ -190,6 +190,7 @@ class SongRepository extends Repository 'collaborators.id as collaborator_id', 'collaborators.name as collaborator_name', 'collaborators.email as collaborator_email', + 'collaborators.avatar as collaborator_avatar', 'playlist_song.created_at as added_at' ); }) diff --git a/app/Services/SongStorages/CloudStorage.php b/app/Services/SongStorages/CloudStorage.php index f544f14e..e38a3c7d 100644 --- a/app/Services/SongStorages/CloudStorage.php +++ b/app/Services/SongStorages/CloudStorage.php @@ -17,11 +17,12 @@ abstract class CloudStorage extends SongStorage { public function __construct(protected FileScanner $scanner) { - parent::__construct(); } protected function scanUploadedFile(UploadedFile $file, User $uploader): ScanResult { + self::assertSupported(); + // Can't scan the uploaded file directly, as it apparently causes some misbehavior during idv3 tag reading. // Instead, we copy the file to the tmp directory and scan it from there. $tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'koel_tmp'; @@ -42,6 +43,8 @@ abstract class CloudStorage extends SongStorage public function copyToLocal(Song $song): string { + self::assertSupported(); + $tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'koel_tmp'; File::ensureDirectoryExists($tmpDir); diff --git a/app/Services/SongStorages/DropboxStorage.php b/app/Services/SongStorages/DropboxStorage.php index 175e08ad..5f5124e2 100644 --- a/app/Services/SongStorages/DropboxStorage.php +++ b/app/Services/SongStorages/DropboxStorage.php @@ -27,6 +27,8 @@ final class DropboxStorage extends CloudStorage public function storeUploadedFile(UploadedFile $file, User $uploader): Song { + self::assertSupported(); + return DB::transaction(function () use ($file, $uploader): Song { $result = $this->scanUploadedFile($file, $uploader); $song = $this->scanner->getSong(); @@ -71,16 +73,20 @@ final class DropboxStorage extends CloudStorage public function getSongPresignedUrl(Song $song): string { + self::assertSupported(); + return $this->filesystem->temporaryUrl($song->storage_metadata->getPath()); } - public function supported(): bool + protected function supported(): bool { return SongStorageTypes::supported(SongStorageTypes::DROPBOX); } public function delete(Song $song, bool $backup = false): void { + self::assertSupported(); + $path = $song->storage_metadata->getPath(); if ($backup) { diff --git a/app/Services/SongStorages/LocalStorage.php b/app/Services/SongStorages/LocalStorage.php index 8ff5aef1..1bc04b56 100644 --- a/app/Services/SongStorages/LocalStorage.php +++ b/app/Services/SongStorages/LocalStorage.php @@ -21,11 +21,12 @@ final class LocalStorage extends SongStorage { public function __construct(private FileScanner $scanner) { - parent::__construct(); } public function storeUploadedFile(UploadedFile $file, User $uploader): Song { + self::assertSupported(); + $uploadDirectory = $this->getUploadDirectory($uploader); $targetFileName = $this->getTargetFileName($file, $uploader); diff --git a/app/Services/SongStorages/S3CompatibleStorage.php b/app/Services/SongStorages/S3CompatibleStorage.php index 85a8274d..27783c97 100644 --- a/app/Services/SongStorages/S3CompatibleStorage.php +++ b/app/Services/SongStorages/S3CompatibleStorage.php @@ -20,6 +20,8 @@ class S3CompatibleStorage extends CloudStorage public function storeUploadedFile(UploadedFile $file, User $uploader): Song { + self::assertSupported(); + return DB::transaction(function () use ($file, $uploader): Song { $result = $this->scanUploadedFile($file, $uploader); $song = $this->scanner->getSong(); @@ -40,11 +42,15 @@ class S3CompatibleStorage extends CloudStorage public function getSongPresignedUrl(Song $song): string { + self::assertSupported(); + return Storage::disk('s3')->temporaryUrl($song->storage_metadata->getPath(), now()->addHour()); } public function delete(Song $song, bool $backup = false): void { + self::assertSupported(); + $disk = Storage::disk('s3'); $path = $song->storage_metadata->getPath(); @@ -61,7 +67,7 @@ class S3CompatibleStorage extends CloudStorage Storage::disk('s3')->delete('test.txt'); } - public function supported(): bool + protected function supported(): bool { return SongStorageTypes::supported(SongStorageTypes::S3); } diff --git a/app/Services/SongStorages/S3LambdaStorage.php b/app/Services/SongStorages/S3LambdaStorage.php index a6714daf..1de94934 100644 --- a/app/Services/SongStorages/S3LambdaStorage.php +++ b/app/Services/SongStorages/S3LambdaStorage.php @@ -29,6 +29,8 @@ final class S3LambdaStorage extends S3CompatibleStorage public function storeUploadedFile(UploadedFile $file, User $uploader): Song { + self::assertSupported(); + throw new MethodNotImplementedException('Lambda storage does not support uploading.'); } @@ -85,7 +87,7 @@ final class S3LambdaStorage extends S3CompatibleStorage $song->delete(); } - public function supported(): bool + protected function supported(): bool { return SongStorageTypes::supported(SongStorageTypes::S3_LAMBDA); } diff --git a/app/Services/SongStorages/SongStorage.php b/app/Services/SongStorages/SongStorage.php index 65e75297..de99873f 100644 --- a/app/Services/SongStorages/SongStorage.php +++ b/app/Services/SongStorages/SongStorage.php @@ -9,17 +9,17 @@ use Illuminate\Http\UploadedFile; abstract class SongStorage { - public function __construct() + abstract public function storeUploadedFile(UploadedFile $file, User $uploader): Song; + + abstract public function delete(Song $song, bool $backup = false): void; + + abstract protected function supported(): bool; + + protected function assertSupported(): void { throw_unless( $this->supported(), new KoelPlusRequiredException('The storage driver is only supported in Koel Plus.') ); } - - abstract public function storeUploadedFile(UploadedFile $file, User $uploader): Song; - - abstract public function delete(Song $song, bool $backup = false): void; - - abstract protected function supported(): bool; } diff --git a/app/Services/YouTubeService.php b/app/Services/YouTubeService.php index 54650e2b..63552d12 100644 --- a/app/Services/YouTubeService.php +++ b/app/Services/YouTubeService.php @@ -6,6 +6,7 @@ use App\Http\Integrations\YouTube\Requests\SearchVideosRequest; use App\Http\Integrations\YouTube\YouTubeConnector; use App\Models\Song; use Illuminate\Cache\Repository as Cache; +use Throwable; class YouTubeService { @@ -27,10 +28,14 @@ class YouTubeService $request = new SearchVideosRequest($song, $pageToken); $hash = md5(serialize($request->query()->all())); - return $this->cache->remember( - "youtube.$hash", - now()->addWeek(), - fn () => $this->connector->send($request)->object() - ); + try { + return $this->cache->remember( + "youtube.$hash", + now()->addWeek(), + fn () => $this->connector->send($request)->object() + ); + } catch (Throwable) { + return null; + } } } diff --git a/app/Values/PlaylistCollaborator.php b/app/Values/PlaylistCollaborator.php index ad4b0cb7..f74603e5 100644 --- a/app/Values/PlaylistCollaborator.php +++ b/app/Values/PlaylistCollaborator.php @@ -18,7 +18,7 @@ final class PlaylistCollaborator implements Arrayable public static function fromUser(User $user): self { - return new self($user->id, $user->name, gravatar($user->email)); + return new self($user->id, $user->name, $user->avatar); } /** @return array */ diff --git a/package.json b/package.json index 868c23ab..37a75cc7 100644 --- a/package.json +++ b/package.json @@ -74,9 +74,11 @@ "lint-staged": "^10.3.0", "lucide-vue-next": "^0.363.0", "postcss": "^8.4.38", + "postcss-mixins": "^10.0.0", "postcss-nested": "^6.0.1", "qrcode": "^1", "start-server-and-test": "^2.0.3", + "tailwindcss": "^3.4.3", "typescript": "^4.8.4", "vite": "^5.1.6", "vitepress": "^1.0.0-rc.45", diff --git a/postcss.config.cjs b/postcss.config.cjs index 7ccf6606..bdebaef3 100644 --- a/postcss.config.cjs +++ b/postcss.config.cjs @@ -1,5 +1,7 @@ module.exports = { plugins: [ + require('tailwindcss'), + require('postcss-mixins'), require('postcss-nested'), require('autoprefixer') ] diff --git a/resources/assets/css/app.pcss b/resources/assets/css/app.pcss index 0c3041b1..fa80c2da 100644 --- a/resources/assets/css/app.pcss +++ b/resources/assets/css/app.pcss @@ -1,203 +1,41 @@ -@import './vendor/reset.pcss'; @import './partials/vars.pcss'; @import './partials/hack.pcss'; +@import './partials/mixins.pcss'; + @import '@modules/nouislider/distribute/nouislider.min.css'; @import './vendor/plyr.pcss'; @import './vendor/nprogress.pcss'; @import './partials/skeleton.pcss'; @import './partials/tooltip.pcss'; +@import './partials/context-menu.pcss'; @import './partials/shared.pcss'; -.vertical-center { - display: flex; - align-items: center; - justify-content: center; -} +@tailwind base; +@tailwind components; +@tailwind utilities; -.artist-album-wrapper { - display: grid !important; - gap: 16px; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); +@layer utilities { + .fade-top { + -webkit-mask-image: linear-gradient(to bottom, transparent, black var(--fade-size)); + mask-image: linear-gradient(to bottom, transparent, black var(--fade-size)); + } - &.as-list { - gap: 0.7em 1em; - align-content: start; - grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); + .fade-bottom { + -webkit-mask-image: linear-gradient(to top, transparent, black var(--fade-size)); + mask-image: linear-gradient(to top, transparent, black var(--fade-size)); + } - @media only screen and (max-width: 667px) { - display: block; + .fade-top.fade-bottom { + -webkit-mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top, + linear-gradient(to top, transparent, black var(--fade-size)) bottom; + -webkit-mask-size: 100% 51%; + -webkit-mask-repeat: no-repeat; - >*+* { - margin-top: .7rem; - } - } + mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top, + linear-gradient(to top, transparent, black var(--fade-size)) bottom; + mask-size: 100% 51%; + mask-repeat: no-repeat; } } - -.artist-album-info-wrapper { - .loading { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - } - - .info-wrapper { - color: var(--color-text-secondary); - position: absolute; - top: 0; - left: 0; - background: var(--color-bg-primary); - width: 100%; - height: 100%; - z-index: 2; - - .inner { - overflow: auto; - height: 100%; - padding: 24px 24px 48px; - - @media only screen and (max-width: 768px) { - padding: 16px; - } - } - - .close-modal { - display: none; - } - - &:hover { - .close-modal { - display: block; - } - } - } -} - -.artist-album-info { - color: var(--color-text-secondary); - - h1 { - display: flex; - align-items: center; - justify-content: center; - font-weight: var(--font-weight-thin); - line-height: 2.8rem; - margin-bottom: 16px; - - &.name { - font-size: 2rem; - margin-bottom: 2rem; - } - - span { - flex: 1; - margin-right: 12px; - } - - a { - font-size: 14px; - - &:hover { - color: var(--color-highlight); - } - } - } - - .bio, - .wiki { - margin: 16px 0; - } - - .more { - margin-top: .75rem; - border-radius: .23rem; - background: var(--color-blue); - color: var(--color-text-primary); - padding: .3rem .6rem; - display: inline-block; - text-transform: uppercase; - font-size: .8rem; - } - - .cover, - .cool-guys-posing { - width: 100%; - height: auto; - display: block; - border-radius: 8px; - overflow: hidden; - } - - footer { - margin-top: 24px; - font-size: .9rem; - text-align: right; - - a { - color: var(--color-text-primary); - font-weight: var(--font-weight-normal); - - &:hover { - color: var(--color-text-secondary); - } - } - } - - &.full { - .cover { - width: 300px; - max-width: 100%; - float: left; - margin: 0 16px 16px 0; - } - - .bio, - .wiki { - margin-top: 0; - } - - h1.name { - font-size: 2.4rem; - - a.shuffle { - display: none; - } - } - } -} - -.inset-when-pressed { - &:not([disabled]):active { - box-shadow: inset 0 10px 10px -10px rgba(0, 0, 0, .6); - } -} - -.context-menu { - padding: .4rem 0; - width: max-content; - min-width: 144px; - background-color: var(--color-bg-context-menu); - position: fixed; - border-radius: 4px; - z-index: 1001; - filter: drop-shadow(0 5px 15px rgba(0, 0, 0, .5)); - - :deep(.arrow) { - background-color: var(--color-bg-context-menu); - position: absolute; - width: 8px; - height: 8px; - transform: rotate(45deg); - } -} - -.themed-background, body, html { - background-color: var(--color-bg-primary); - background-image: var(--bg-image); - background-attachment: var(--bg-attachment); - background-size: var(--bg-size); - background-position: var(--bg-position); -} diff --git a/resources/assets/css/partials/context-menu.pcss b/resources/assets/css/partials/context-menu.pcss new file mode 100644 index 00000000..49e90d28 --- /dev/null +++ b/resources/assets/css/partials/context-menu.pcss @@ -0,0 +1,26 @@ +.context-menu { + @apply py-1 px-0 w-max min-w-[144px] bg-k-bg-context-menu fixed rounded-md z-[1001] shadow-md; + + .arrow { + @apply bg-k-bg-context-menu absolute w-[8px] aspect-square rotate-45; + } + + li { + @apply relative px-4 py-1.5 whitespace-nowrap hover:bg-k-highlight hover:text-k-text-primary; + + &.separator { + @apply pointer-events-none p-0 border-b border-b-white/10; + } + + &.has-sub { + @apply pr-[30px] bg-no-repeat; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 30 30' shape-rendering='geometricPrecision' text-rendering='geometricPrecision'%3E%3Cpolygon points='-2.303673 -19.980561 12.696327 5.999439 -17.303673 5.999439 -2.303673 -19.980561' transform='matrix(0 1-1 0 5.999439 17.303673)' fill='%23fff' stroke-width='0'/%3E%3C/svg%3E"); + background-position: right 8px center; + background-size: 10px; + } + } + + .submenu { + @apply absolute top-0 left-full hidden; + } +} diff --git a/resources/assets/css/partials/hack.pcss b/resources/assets/css/partials/hack.pcss index 64e493d9..e2e5f4a4 100644 --- a/resources/assets/css/partials/hack.pcss +++ b/resources/assets/css/partials/hack.pcss @@ -6,9 +6,7 @@ * Make elements draggable in old WebKit */ [draggable] { - user-select: none; - -khtml-user-drag: element; - -webkit-user-drag: element; + @apply select-none; } /** @@ -16,44 +14,38 @@ */ html.non-mac { ::-webkit-scrollbar { - width: 10px; - height: 10px; + @apply w-[10px] h-[10px]; } ::-webkit-scrollbar-button { - width: 0px; - height: 0px; + @apply w-0 h-0; } ::-webkit-scrollbar-thumb { - background: var(--color-bg-primary); - border: 1px solid rgba(255, 255, 255, .2); - border-radius: 50px; + @apply bg-k-bg-primary border border-white/20 rounded-[50px]; } ::-webkit-scrollbar-thumb:hover { - background: #303030; + @apply bg-[#303030]; } ::-webkit-scrollbar-thumb:active { - background: var(--color-bg-primary); + @apply bg-k-bg-primary; } ::-webkit-scrollbar-track { - background: var(--color-bg-primary); - border: 0px none var(--color-text-primary); - border-radius: 50px; + @apply bg-k-bg-primary border-0 rounded-[50px]; } ::-webkit-scrollbar-track:hover { - background: var(--color-bg-primary); + @apply bg-k-bg-primary; } ::-webkit-scrollbar-track:active { - background: #333333; + @apply bg-[#333]; } ::-webkit-scrollbar-corner { - background: transparent; + @apply bg-transparent; } } diff --git a/resources/assets/css/partials/mixins.pcss b/resources/assets/css/partials/mixins.pcss new file mode 100644 index 00000000..e1d4638e --- /dev/null +++ b/resources/assets/css/partials/mixins.pcss @@ -0,0 +1,7 @@ +@define-mixin themed-background { + background-color: var(--color-bg-primary); + background-image: var(--bg-image); + background-attachment: var(--bg-attachment); + background-size: var(--bg-size); + background-position: var(--bg-position); +} diff --git a/resources/assets/css/partials/shared.pcss b/resources/assets/css/partials/shared.pcss index 18ee7a3a..d089f698 100644 --- a/resources/assets/css/partials/shared.pcss +++ b/resources/assets/css/partials/shared.pcss @@ -1,416 +1,54 @@ -*, -*::before, -*::after { - box-sizing: border-box; - outline: none; -} +@tailwind base; -h1, h2, h3, h4, h5, h6, blockquote { - text-wrap: balance; -} - -*::marker { - display: none !important; -} - -:root { - color-scheme: dark; -} - -::placeholder { - color: rgba(0, 0, 0, .5); -} - -body, -html { - color: var(--color-text-primary); - font-family: var(--font-family); - font-size: 13px; - line-height: 1.5rem; - font-weight: var(--font-weight-light); - overflow: hidden; -} - -input { - height: 32px; -} - -input, -select, -button, -textarea, -.btn { - appearance: none; - border: 0; - outline: 0; - font-family: var(--font-family); - font-size: 1rem; - font-weight: var(--font-weight-light); - padding: .5rem .6rem; - border-radius: var(--border-radius-input); - margin: 0; - background: var(--color-bg-input); - color: var(--color-input); - - &:required, - &:invalid { - box-shadow: none; +@layer base { + h1, h2, h3, h4, h5, h6, blockquote { + @apply text-balance; } - &[type="search"] { - border-radius: 12px; - height: 24px; - padding: 0 .5rem; + *::marker { + @apply hidden !important; } - &[type="text"] { - display: block; + :root { + color-scheme: dark; } - &[disabled], &[readonly] { - background: rgba(255, 255, 255, .7); - cursor: not-allowed; + ::placeholder { + @apply text-black/5; } - &:focus { - outline: none !important; + body, html { + @mixin themed-background; + + font-family: var(--font-family); + font-size: 13px; + + @apply text-k-text-primary font-light leading-6 overflow-hidden; } - &::-moz-focus-inner { - border: 0 !important; - } -} + input, select, button, textarea, .btn { + font-family: var(--font-family); -button, -[role=button] { - cursor: pointer; - background: transparent; - padding: 0; - border: 0; - color: currentColor; -} + @apply appearance-none border-0 outline-0 text-base font-light; -select { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAA4UlEQVRYhe3WMQ6CMABA0Y9xcHDVlBM5OXsC4xXcvImzg/EQTNykHZx0JcHFxkKQtpQGhv5OtCG8pElLVlMzdYupAZAQvxJClxC6WSCW5kMu8jNwAVYRvlUBV6nkqb2QmSdmLvI3sI4AMNtIJZ/mRHs77pEBRRvQhTgCj0iAEth3LTQQUskKOESAlMBOKvmyIiJBegGdiJEhVsBfxEgQJ0AvIhDiDLAiBkK8AE4IT4g3wBnhCBkE8EJYIIMB3ogW5PadKkIAANQBQwixDXlfj8YtOlWz+KlJCF1C6BJCNwvEB8RnttABpb3tAAAAAElFTkSuQmCC); - background-size: 12px; - background-position: calc(100% - 8px) 50%; - padding-right: 26px; - background-repeat: no-repeat; -} + &:required, &:invalid { + @apply shadow-none; + } -.hidden { - display: none !important; -} - -input[type="checkbox"] { - border-radius: 2px; - cursor: pointer; - height: 15px; - margin: -4px 4px 0 0; - padding: 0 !important; - vertical-align: middle; - width: 15px; - background: var(--color-text-primary); -} - -a { - text-decoration: none; - cursor: pointer; - - &:link, - &:visited { - color: var(--color-text-secondary); - } - - &:hover, - &:focus { - color: var(--color-accent); - } -} - -.control { - cursor: pointer; - color: var(--color-text-secondary); - - &:hover { - color: var(--color-text-primary); - } -} - -p { - line-height: 1.5rem; -} - -em { - font-style: italic; -} - -.help { - opacity: .7; - font-size: .9rem; - line-height: 1.3rem; -} - -label { - font-size: 1.1rem; - display: block; - cursor: pointer; - - &.small { - font-size: 1rem; - } -} - -.pointer-events-none { - pointer-events: none; -} - -.tabs { - min-height: 100%; - display: flex; - flex-direction: column; - position: relative; - - [role=tablist] { - overflow: auto; - border-bottom: 2px solid rgba(255, 255, 255, .1); - padding: 0 1.25rem; - display: flex; - gap: 0.3rem; - min-height: 36px; - - [role=tab] { - position: relative; - padding: .7rem 1.3rem; - border-radius: 4px 4px 0 0; - opacity: .7; - background: rgba(255, 255, 255, .05); - text-transform: uppercase; - color: var(--color-text-secondary); - - &:hover { - transition: .3s; - background: rgba(255, 255, 255, .1); - } - - &[aria-selected=true] { - transition: none; - color: var(--color-text-primary); - background: rgba(255, 255, 255, .1); - opacity: 1; - } + &::-moz-focus-inner { + @apply border-0 !important; } } - .panes { - padding: 1.25rem; - } -} + a { + @apply no-underline text-k-text-primary cursor-pointer hover:text-k-accent focus:text-k-accent; -.form-row + .form-row { - margin-top: 1.125rem; - position: relative; -} - -.form-row.cols { - display: flex; - place-content: space-between; - gap: 1rem; - - > * { - flex: 1; - } -} - -.form-row input:not([type="checkbox"]), -.form-row select { - margin-top: .7rem; - display: block; - height: 32px; -} - -.font-size- { - &0 { - font-size: 0; - } - - &1 { - font-size: 1rem; - } - - &1\.5 { - font-size: 1.5rem; - } - - &2 { - font-size: 2rem; - } -} - -.text- { - &primary { - color: var(--color-text-primary) !important; - } - - &secondary { - color: var(--color-text-secondary) !important; - } - - &highlight { - color: var(--color-highlight) !important; - } - - &maroon { - color: var(--color-maroon) !important; - } - - &red { - color: var(--color-red) !important; - } - - &blue { - color: var(--color-blue) !important; - } - - &green { - color: var(--color-green) !important; - } - - &uppercase { - text-transform: uppercase !important; - } - - &thin { - font-weight: var(--font-weight-thin) !important; - } - - &normal { - font-weight: var(--font-weight-normal) !important; - } - - &light { - font-weight: var(--font-weight-light) !important; - } - - &bold { - font-weight: var(--font-weight-bold) !important; - } -} - -.bg- { - &primary { - background-color: var(--color-bg-primary) !important; - } - - &secondary { - background-color: var(--color-bg-secondary) !important; - } -} - -.d- { - &block { - display: block; - } - - &inline { - display: block; - } - - &inline-block { - display: inline-block; - } - - &flex { - display: flex; - } - - &inline-flex { - display: inline-block; - } - - &grid { - display: grid; - } - - &inline-grid { - display: inline-grid; - } -} - -.context-menu, -.submenu, -menu { - position: fixed; - - @keyframes subMenuHoverHelp { - 0% { - height: 500%; - } - 99% { - height: 500%; - } - 100% { - height: 0; + &:link, &:visited { + @apply text-k-accent; } } - li { - list-style: none; - position: relative; - padding: 4px 12px; - cursor: default; - white-space: nowrap; - - &:hover { - background: var(--color-highlight); - color: var(--color-text-primary); - } - - &.separator { - pointer-events: none; - padding: 0; - border-bottom: 1px solid rgba(255, 255, 255, .1); - } - - &.has-sub { - padding-right: 30px; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 30 30' shape-rendering='geometricPrecision' text-rendering='geometricPrecision'%3E%3Cpolygon points='-2.303673 -19.980561 12.696327 5.999439 -17.303673 5.999439 -2.303673 -19.980561' transform='matrix(0 1-1 0 5.999439 17.303673)' fill='%23fff' stroke-width='0'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 10px; - } - } - - .submenu { - position: absolute; - display: none; - left: 100%; - top: 0; + :root { + --fade-size: 3rem; } } - -:root { - --fade-size: 3rem; -} - -.fade-top { - -webkit-mask-image: linear-gradient(to bottom, transparent, black var(--fade-size)); - mask-image: linear-gradient(to bottom, transparent, black var(--fade-size)); -} - -.fade-bottom { - -webkit-mask-image: linear-gradient(to top, transparent, black var(--fade-size)); - mask-image: linear-gradient(to top, transparent, black var(--fade-size)); -} - -.fade-top.fade-bottom { - -webkit-mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top, - linear-gradient(to top, transparent, black var(--fade-size)) bottom; - -webkit-mask-size: 100% 51%; - -webkit-mask-repeat: no-repeat; - - mask: linear-gradient(to bottom, transparent, black var(--fade-size)) top, - linear-gradient(to top, transparent, black var(--fade-size)) bottom; - mask-size: 100% 51%; - mask-repeat: no-repeat; -} diff --git a/resources/assets/css/partials/skeleton.pcss b/resources/assets/css/partials/skeleton.pcss index f56d9410..108c4b0b 100644 --- a/resources/assets/css/partials/skeleton.pcss +++ b/resources/assets/css/partials/skeleton.pcss @@ -1,15 +1,15 @@ .skeleton { .pulse, &.pulse { + @apply bg-white/5; animation: skeleton-pulse 2s infinite; - background-color: rgba(255, 255, 255, .05); } @keyframes skeleton-pulse { 0%, 100% { - opacity: 0; + @apply opacity-0; } 50% { - opacity: 1; + @apply opacity-100; } } } diff --git a/resources/assets/css/partials/tooltip.pcss b/resources/assets/css/partials/tooltip.pcss index 85287e69..c7342a41 100644 --- a/resources/assets/css/partials/tooltip.pcss +++ b/resources/assets/css/partials/tooltip.pcss @@ -1,36 +1,12 @@ .tooltip { - opacity: 0; - color: rgba(255, 255, 255, .8); - width: max-content; - position: absolute; - top: 0; - left: 0; - background: #111; - padding: 5px 12px; - border-radius: 4px; - pointer-events: none; - filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, .3)); - z-index: 9999; + @apply opacity-0 text-white/80 w-max absolute top-0 left-0 bg-black px-3 py-2 rounded-md pointer-events-none + drop-shadow z-[9999] no-hover:hidden; &.show { - opacity: 1; - transition: opacity .5s ease-in-out; - transition-delay: .3s; + @apply opacity-100 transition-opacity duration-500 ease-in-out; } &-arrow { - position: absolute; - background: #111; - width: 8px; - height: 8px; - transform: rotate(45deg); - } - - @media (hover: none) { - display: none !important; - } - - @media screen and (max-width: 768px) { - display: none !important; + @apply absolute bg-black w-[8px] aspect-square rotate-45; } } diff --git a/resources/assets/css/partials/vars.pcss b/resources/assets/css/partials/vars.pcss index dc41a013..b2e782d4 100644 --- a/resources/assets/css/partials/vars.pcss +++ b/resources/assets/css/partials/vars.pcss @@ -2,13 +2,13 @@ --color-text-primary: #fff; --color-text-secondary: rgba(255, 255, 255, .7); --color-bg-primary: #181818; - --color-bg-secondary: rgba(255, 255, 255, .025); + --color-bg-secondary: #1d1d1d; --color-border: var(--color-bg-secondary); --color-highlight: #ff7d2e; --color-accent: var(--color-highlight); --color-bg-context-menu: var(--color-bg-primary); - --color-input: #333; + --color-text-input: #333; --color-bg-input: #fff; --bg-image: none; @@ -18,23 +18,15 @@ --font-family: system, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; - --font-weight-thin: 100; - --font-weight-light: 300; - --font-weight-normal: 500; - --font-weight-bold: 700; - --header-height: auto; --footer-height: 84px; - --extra-drawer-width: 320px; - --sidebar-width: 256px; + --extra-drawer-width: 25rem; + --sidebar-width: 20rem; - --color-black: #181818; - --color-maroon: #bf2043; - --color-green: #56a052; - --color-blue: #0191f7; - --color-red: #c34848; - - --border-radius-input: .3rem; + --color-love: #bf2043; + --color-success: #56a052; + --color-primary: #0191f7; + --color-danger: #c34848; @media screen and (max-width: 768px) { --header-height: 56px; @@ -42,5 +34,3 @@ --extra-drawer-width: 100%; } } - -$plyr-blue: var(--color-highlight); diff --git a/resources/assets/css/remote.pcss b/resources/assets/css/remote.pcss index 0d71ec86..90dab0c7 100644 --- a/resources/assets/css/remote.pcss +++ b/resources/assets/css/remote.pcss @@ -1,4 +1,15 @@ @import './vendor/reset.pcss'; @import '@modules/nouislider/distribute/nouislider.min.css'; @import './partials/vars.pcss'; +@import './partials/mixins.pcss'; @import './partials/shared.pcss'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body, html { + @apply h-screen relative; + } +} diff --git a/resources/assets/css/vendor/alertify.pcss b/resources/assets/css/vendor/alertify.pcss deleted file mode 100644 index 5949e089..00000000 --- a/resources/assets/css/vendor/alertify.pcss +++ /dev/null @@ -1,21 +0,0 @@ -.alertify { - font-family: var(--font-family); - font-weight: var(--font-weight-light); - background-color: rgba(0, 0, 0, .7); - z-index: 9999; - color: rgba(0,0,0,.87); - - .dialog > div { - border-radius: 3px; - } -} - -.alertify-logs { - font-family: var(--font-family); - font-weight: var(--font-weight-thin); - z-index: 9999; - - .show { - border-radius: 3px; - } -} diff --git a/resources/assets/css/vendor/nprogress.pcss b/resources/assets/css/vendor/nprogress.pcss index ab1a6e9d..56f9c832 100644 --- a/resources/assets/css/vendor/nprogress.pcss +++ b/resources/assets/css/vendor/nprogress.pcss @@ -4,44 +4,22 @@ * > The included CSS file is pretty minimal... in fact, feel free to scrap it and make your own! */ #nprogress { - pointer-events: none; + @apply pointer-events-none; .bar { - display: none; + @apply hidden; } /* Fancy blur effect */ .peg { - display: none; + @apply hidden; } .spinner { - display: block; - position: fixed; - z-index: 9999; - top: 15px; - right: 13px; + @apply block fixed z-[9999] top-[15px] right-[13px]; } .spinner-icon { - width: 18px; - height: 18px; - box-sizing: border-box; - - border: solid 2px transparent; - border-top-color: var(--color-highlight); - border-left-color: var(--color-highlight); - border-radius: 50%; - - animation: nprogress-spinner 400ms linear infinite; - } -} - -@keyframes nprogress-spinner { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); + @apply w-[18px] aspect-square border-2 border-transparent border-t-k-highlight border-l-k-highlight rounded-full animate-spin; } } diff --git a/resources/assets/js/App.vue b/resources/assets/js/App.vue index 88bfcc7a..a0ef52f0 100644 --- a/resources/assets/js/App.vue +++ b/resources/assets/js/App.vue @@ -5,7 +5,13 @@ -
+
@@ -17,7 +23,7 @@ -
+ @@ -31,10 +37,10 @@ import { useOnline } from '@vueuse/core' import { commonStore, preferenceStore as preferences, queueStore } from '@/stores' import { authService, socketListener, socketService, uploadService } from '@/services' import { CurrentSongKey, DialogBoxKey, MessageToasterKey, OverlayKey } from '@/symbols' -import { useRouter } from '@/composables' +import { useRouter, useOverlay } from '@/composables' import DialogBox from '@/components/ui/DialogBox.vue' -import MessageToaster from '@/components/ui/MessageToaster.vue' +import MessageToaster from '@/components/ui/message-toaster/MessageToaster.vue' import Overlay from '@/components/ui/Overlay.vue' import OfflineNotification from '@/components/ui/OfflineNotification.vue' @@ -53,7 +59,7 @@ const ArtistContextMenu = defineAsyncComponent(() => import('@/components/artist const PlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/PlaylistContextMenu.vue')) const PlaylistFolderContextMenu = defineAsyncComponent(() => import('@/components/playlist/PlaylistFolderContextMenu.vue')) const SongContextMenu = defineAsyncComponent(() => import('@/components/song/SongContextMenu.vue')) -const CreateNewPlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/CreateNewPlaylistContextMenu.vue')) +const CreateNewPlaylistContextMenu = defineAsyncComponent(() => import('@/components/playlist/CreatePlaylistContextMenu.vue')) const SupportKoel = defineAsyncComponent(() => import('@/components/meta/SupportKoel.vue')) const DropZone = defineAsyncComponent(() => import('@/components/ui/upload/DropZone.vue')) const AcceptInvitation = defineAsyncComponent(() => import('@/components/invitation/AcceptInvitation.vue')) @@ -121,7 +127,7 @@ onMounted(async () => { const initialized = ref(false) const init = async () => { - overlay.value!.show({ message: 'Just a little patience…' }) + useOverlay(overlay).showOverlay({ message: 'Just a little patience…' }) try { await commonStore.init() @@ -141,7 +147,7 @@ const init = async () => { layout.value = 'auth' throw err } finally { - overlay.value!.hide() + useOverlay(overlay).hideOverlay() } } @@ -162,50 +168,11 @@ provide(CurrentSongKey, currentSong) diff --git a/resources/assets/js/__tests__/stubs.ts b/resources/assets/js/__tests__/stubs.ts index 3e931f81..a2490e78 100644 --- a/resources/assets/js/__tests__/stubs.ts +++ b/resources/assets/js/__tests__/stubs.ts @@ -1,7 +1,7 @@ import { Ref, ref } from 'vue' import { noop } from '@/utils' -import MessageToaster from '@/components/ui/MessageToaster.vue' +import MessageToaster from '@/components/ui/message-toaster/MessageToaster.vue' import DialogBox from '@/components/ui/DialogBox.vue' import Overlay from '@/components/ui/Overlay.vue' diff --git a/resources/assets/js/components/album/AlbumCard.vue b/resources/assets/js/components/album/AlbumCard.vue index d249cc56..e948a867 100644 --- a/resources/assets/js/components/album/AlbumCard.vue +++ b/resources/assets/js/components/album/AlbumCard.vue @@ -9,24 +9,18 @@ @dragstart="onDragStart" >