feat: use FontAwesome as components

This commit is contained in:
Phan An 2022-07-15 09:23:55 +02:00
parent 1861b30f56
commit 67ff46880a
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
60 changed files with 432 additions and 347 deletions

View file

@ -14,11 +14,15 @@
"url": "https://github.com/koel/koel"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/free-brands-svg-icons": "^6.1.1",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^6.1.1",
"@fortawesome/vue-fontawesome": "^3.0.1",
"alertify.js": "^1.0.12",
"axios": "^0.21.1",
"blueimp-md5": "^2.3.0",
"compare-versions": "^3.5.1",
"font-awesome": "^4.7.0",
"ismobilejs": "^0.4.0",
"local-storage": "^2.0.0",
"lodash": "^4.17.19",
@ -64,7 +68,6 @@
"eslint-plugin-vue": "^8.7.1",
"factoria": "^4.0.0",
"file-loader": "^1.1.6",
"font-awesome": "^4.7.0",
"husky": "^4.2.5",
"jest-serializer-vue": "^2.0.2",
"jsdom": "^19.0.0",

View file

@ -2,8 +2,10 @@ import './staticLoader'
import { createApp } from 'vue'
import App from './App.vue'
import { clickaway, droppable, focus } from '@/directives'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
createApp(App)
.component('icon', FontAwesomeIcon)
.directive('koel-focus', focus)
.directive('koel-clickaway', clickaway)
.directive('koel-droppable', droppable)

View file

@ -36,7 +36,7 @@
role="button"
@click.prevent="shuffle"
>
<i class="fa fa-random"/>
<icon :icon="faRandom"/>
</a>
<a
v-if="allowDownload"
@ -47,7 +47,7 @@
role="button"
@click.prevent="download"
>
<i class="fa fa-download"/>
<icon :icon="faDownload"/>
</a>
</span>
</p>
@ -56,6 +56,7 @@
</template>
<script lang="ts" setup>
import { faDownload, faRandom } from '@fortawesome/free-solid-svg-icons'
import { computed, toRef, toRefs } from 'vue'
import { defaultCover, eventBus, pluralize, secondsToHis, startDragging } from '@/utils'
import { albumStore, artistStore, commonStore, songStore } from '@/stores'
@ -82,13 +83,5 @@ const requestContextMenu = (event: MouseEvent) => eventBus.emit('ALBUM_CONTEXT_M
</script>
<style lang="scss">
.sep {
display: none;
.as-list & {
display: inline;
}
}
@include artist-album-card();
</style>

View file

@ -2,8 +2,8 @@
<article :class="mode" class="album-info" data-testid="album-info">
<h1 class="name">
<span>{{ album.name }}</span>
<button :title="`Play all songs in ${album.name}`" class="control play" type="button" @click.prevent="play">
<i class="fa fa-play"/>
<button :title="`Play all songs in ${album.name}`" class="control" type="button" @click.prevent="play">
<icon :icon="faCirclePlay" size="xl"/>
</button>
</h1>
@ -32,6 +32,7 @@
</template>
<script lang="ts" setup>
import { faCirclePlay } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, ref, toRefs, watch } from 'vue'
import { useThirdPartyServices } from '@/composables'
import { songStore } from '@/stores'

View file

@ -36,7 +36,7 @@
data-testid="shuffle-artist"
@click.prevent="shuffle"
>
<i class="fa fa-random"/>
<icon :icon="faRandom"/>
</a>
<a
v-if="allowDownload"
@ -47,7 +47,7 @@
data-testid="download-artist"
@click.prevent="download"
>
<i class="fa fa-download"/>
<icon :icon="faDownload"/>
</a>
</span>
</p>
@ -56,11 +56,11 @@
</template>
<script lang="ts" setup>
import { faDownload, faRandom } from '@fortawesome/free-solid-svg-icons'
import { computed, toRef, toRefs } from 'vue'
import { eventBus, pluralize, startDragging } from '@/utils'
import { artistStore, commonStore, songStore } from '@/stores'
import { downloadService, playbackService } from '@/services'
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
const props = withDefaults(defineProps<{ artist: Artist, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })

View file

@ -2,8 +2,8 @@
<article :class="mode" class="artist-info" data-testid="artist-info">
<h1 class="name">
<span>{{ artist.name }}</span>
<button :title="`Play all songs by ${artist.name}`" class="play control" type="button" @click.prevent="play">
<i class="fa fa-play"/>
<button :title="`Play all songs by ${artist.name}`" class="control" type="button" @click.prevent="play">
<icon :icon="faCirclePlay" size="xl"/>
</button>
</h1>
@ -30,6 +30,7 @@
</template>
<script lang="ts" setup>
import { faCirclePlay } from '@fortawesome/free-solid-svg-icons'
import { computed, ref, toRefs, watch } from 'vue'
import { playbackService } from '@/services'
import { useThirdPartyServices } from '@/composables'

View file

@ -2,10 +2,10 @@
<header id="mainHeader">
<h1 class="brand">Koel</h1>
<span class="hamburger" role="button" title="Show or hide the sidebar" @click="toggleSidebar">
<i class="fa fa-bars"></i>
<icon :icon="faBars"/>
</span>
<span class="magnifier" role="button" title="Show or hide the search form" @click="toggleSearchForm">
<i class="fa fa-search"></i>
<icon :icon="faSearch"/>
</span>
<SearchForm v-if="showSearchForm"/>
<div class="header-right">
@ -20,13 +20,14 @@
<span v-if="shouldNotifyNewVersion" class="new-version" data-testid="new-version">
{{ latestVersion }} available!
</span>
<i v-else class="fa fa-info-circle"></i>
<icon v-else :icon="faInfoCircle"/>
</button>
</div>
</header>
</template>
<script lang="ts" setup>
import { faBars, faInfoCircle, faSearch } from '@fortawesome/free-solid-svg-icons'
import isMobile from 'ismobilejs'
import { ref } from 'vue'
import { eventBus } from '@/utils'

View file

@ -35,11 +35,11 @@
type="button"
@click.prevent="toggleEqualizer"
>
<i class="fa fa-sliders"></i>
<icon :icon="faSliders"/>
</button>
<a v-else :class="{ active: viewingQueue }" class="queue control" href="#!/queue">
<i class="fa fa-list-ol"></i>
<icon :icon="faListOl"/>
</a>
<RepeatModeSwitch/>
@ -49,8 +49,9 @@
</template>
<script lang="ts" setup>
import { ref, toRef, toRefs } from 'vue'
import isMobile from 'ismobilejs'
import { faListOl, faSliders } from '@fortawesome/free-solid-svg-icons'
import { ref, toRef, toRefs } from 'vue'
import { eventBus, isAudioContextSupported as useEqualizer } from '@/utils'
import { preferenceStore } from '@/stores'

View file

@ -1,7 +1,8 @@
<template>
<div class="side player-controls" :class="{ playing }">
<i
class="prev fa fa-step-backward control"
<icon
:icon="faStepBackward"
class="prev control"
data-testid="play-prev-btn"
role="button"
tabindex="0"
@ -20,7 +21,7 @@
title="Play or resume"
@click.prevent="toggle"
>
<i class="fa fa-play"></i>
<icon :icon="faPlay" size="lg"/>
</span>
<span
v-else
@ -31,12 +32,13 @@
title="Pause"
@click.prevent="toggle"
>
<i class="fa fa-pause"></i>
<icon :icon="faPause" size="lg"/>
</span>
</span>
<i
class="next fa fa-step-forward control"
<icon
:icon="faStepForward"
class="next control"
data-testid="play-next-btn"
role="button"
tabindex="0"
@ -47,6 +49,7 @@
</template>
<script lang="ts" setup>
import { faPause, faPlay, faStepBackward, faStepForward } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { playbackService } from '@/services'
import { defaultCover } from '@/utils'
@ -148,8 +151,10 @@ const toggle = async () => await playbackService.toggle()
top: 0;
left: 0;
transition: opacity .4s ease-out;
font-size: 3rem;
display: inline-block;
display: flex;
font-size: 2rem;
place-items: center;
justify-content: center;
width: 100%;
height: 100%;
line-height: calc(var(--footer-height) + 30px);
@ -160,9 +165,8 @@ const toggle = async () => await playbackService.toggle()
text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);
}
.pause {
text-indent: 0;
font-size: 2rem;
.play {
margin-left: .2rem;
}
.enabled {

View file

@ -46,7 +46,7 @@
type="button"
@click.prevent="currentTab = 'YouTube'"
>
<i class="fa fa-youtube-play"></i>
<icon :icon="faYoutube"/>
</button>
</div>
@ -97,6 +97,7 @@
<script lang="ts" setup>
import isMobile from 'ismobilejs'
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
import { ref, toRef, watch } from 'vue'
import { $, eventBus } from '@/utils'
import { albumStore, artistStore, preferenceStore as preferences } from '@/stores'

View file

@ -5,24 +5,40 @@
<ul class="menu">
<li>
<a :class="['home', currentView === 'Home' ? 'active' : '']" href="#!/home">Home</a>
<a :class="['home', currentView === 'Home' ? 'active' : '']" href="#!/home">
<icon :icon="faHome" fixed-width/>
Home
</a>
</li>
<li>
<a v-koel-droppable="handleDrop" :class="['queue', currentView === 'Queue' ? 'active' : '']" href="#!/queue">
<icon :icon="faListOl" fixed-width/>
Current Queue
</a>
</li>
<li>
<a :class="['songs', currentView === 'Songs' ? 'active' : '']" href="#!/songs">All Songs</a>
<a :class="['songs', currentView === 'Songs' ? 'active' : '']" href="#!/songs">
<icon :icon="faMusic" fixed-width/>
All Songs
</a>
</li>
<li>
<a :class="['albums', currentView === 'Albums' ? 'active' : '']" href="#!/albums">Albums</a>
<a :class="['albums', currentView === 'Albums' ? 'active' : '']" href="#!/albums">
<icon :icon="faCompactDisc" fixed-width/>
Albums
</a>
</li>
<li>
<a :class="['artists', currentView === 'Artists' ? 'active' : '']" href="#!/artists">Artists</a>
<a :class="['artists', currentView === 'Artists' ? 'active' : '']" href="#!/artists">
<icon :icon="faMicrophone" fixed-width/>
Artists
</a>
</li>
<li v-if="useYouTube">
<a :class="['youtube', currentView === 'YouTube' ? 'active' : '']" href="#!/youtube">YouTube Video</a>
<a :class="['youtube', currentView === 'YouTube' ? 'active' : '']" href="#!/youtube">
<icon :icon="faYoutube" fixed-width/>
YouTube Video
</a>
</li>
</ul>
</section>
@ -34,13 +50,22 @@
<ul class="menu">
<li>
<a :class="['settings', currentView === 'Settings' ? 'active' : '']" href="#!/settings">Settings</a>
<a :class="['settings', currentView === 'Settings' ? 'active' : '']" href="#!/settings">
<icon :icon="faTools" fixed-width/>
Settings
</a>
</li>
<li>
<a :class="['upload', currentView === 'Upload' ? 'active' : '']" href="#!/upload">Upload</a>
<a :class="['upload', currentView === 'Upload' ? 'active' : '']" href="#!/upload">
<icon :icon="faUpload" fixed-width/>
Upload
</a>
</li>
<li>
<a :class="['users', currentView === 'Users' ? 'active' : '']" href="#!/users">Users</a>
<a :class="['users', currentView === 'Users' ? 'active' : '']" href="#!/users">
<icon :icon="faUsers" fixed-width/>
Users
</a>
</li>
</ul>
</section>
@ -49,6 +74,17 @@
<script lang="ts" setup>
import isMobile from 'ismobilejs'
import {
faCompactDisc,
faHome,
faListOl,
faMicrophone,
faMusic,
faTools,
faUpload,
faUsers
} from '@fortawesome/free-solid-svg-icons'
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
import { ref } from 'vue'
import { eventBus, resolveSongsFromDragEvent } from '@/utils'
import { queueStore } from '@/stores'
@ -122,10 +158,12 @@ eventBus.on('TOGGLE_SIDEBAR', () => (showing.value = !showing.value))
}
a {
display: block;
display: flex;
align-items: center;
gap: .7rem;
height: 36px;
line-height: 36px;
padding: 0 12px 0 16px;
padding: 0 16px 0 12px;
border-left: 4px solid transparent;
&.active, &:hover {
@ -142,50 +180,6 @@ eventBus.on('TOGGLE_SIDEBAR', () => (showing.value = !showing.value))
&:hover {
border-left-color: var(--color-highlight);
}
&::before {
width: 24px;
display: inline-block;
font-family: FontAwesome;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
&.home::before {
content: "\f015";
}
&.queue::before {
content: "\f0cb";
}
&.songs::before {
content: "\f001";
}
&.albums::before {
content: "\f152";
}
&.artists::before {
content: "\f130";
}
&.youtube::before {
content: "\f16a";
}
&.settings::before {
content: "\f013";
}
&.users::before {
content: "\f0c0";
}
&.upload::before {
content: "\f093";
}
}
}

View file

@ -1,7 +1,7 @@
<template>
<div v-koel-focus class="about text-secondary" data-testid="about-modal" tabindex="0" @keydown.esc="close">
<header>
<h1 class="text-white">About Koel</h1>
<h1 class="text-primary">About Koel</h1>
</header>
<main>

View file

@ -11,9 +11,14 @@
:href="url"
@contextmenu.prevent="openContextMenu"
>
<icon v-if="type === 'favorites'" :icon="faHeart" class="text-maroon" fixed-width/>
<icon v-else :icon="faMusic" :mask="faFile" transform="shrink-7 down-2" fixed-width/>
{{ playlist.name }}
</a>
<a v-else :class="{ active }" :href="url" @contextmenu.prevent="openContextMenu">
<icon v-if="type === 'recently-played'" :icon="faClockRotateLeft" class="text-green" fixed-width/>
<icon v-else :icon="faBoltLightning" :mask="faFile" transform="shrink-7 down-2" fixed-width/>
{{ playlist.name }}
</a>
@ -29,6 +34,7 @@
</template>
<script lang="ts" setup>
import { faBoltLightning, faClockRotateLeft, faFile, faHeart, faMusic } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, nextTick, ref, toRefs } from 'vue'
import { alerts, eventBus, pluralize, resolveSongsFromDragEvent } from '@/utils'
import { favoriteStore, playlistStore } from '@/stores'
@ -149,24 +155,6 @@ eventBus.on('LOAD_MAIN_CONTENT', (view: MainViewName, _playlist: Playlist): void
span {
pointer-events: none;
}
&::before {
content: "\f0f6";
}
}
&.favorites a::before {
content: "\f004";
color: var(--color-maroon);
}
&.recently-played a::before {
content: "\f1da";
color: var(--color-green);
}
&.smart a::before {
content: "\f069";
}
input {

View file

@ -1,14 +1,16 @@
<template>
<section id="playlists">
<h1>Playlists
<i
<h1>
<span>Playlists</span>
<icon
:class="{ creating }"
class="fa fa-plus-circle control create"
:icon="faCirclePlus"
class="control create"
data-testid="sidebar-create-playlist-btn"
role="button"
title="Create a new playlist"
@click.stop.prevent="toggleContextMenu"
></i>
/>
</h1>
<form v-if="creating" @submit.prevent="createPlaylist" name="create-simple-playlist-form" class="create">
@ -39,6 +41,7 @@
</template>
<script lang="ts" setup>
import { faCirclePlus } from '@fortawesome/free-solid-svg-icons'
import { nextTick, ref, toRef } from 'vue'
import { favoriteStore, playlistStore } from '@/stores'
import router from '@/router'
@ -79,11 +82,17 @@ const toggleContextMenu = async (event: MouseEvent) => {
<style lang="scss">
#playlists {
h1 {
display: flex;
align-items: center;
span {
flex: 1;
}
}
.control.create {
margin: -8px -10px -10px;
font-size: 16px;
transition: .3s;
padding: 10px;
&.creating {
transform: rotate(135deg);

View file

@ -22,7 +22,8 @@
@input="onGroupChanged"
/>
<Btn class="btn-add-group" green small uppercase @click.prevent="addGroup">
<i class="fa fa-plus"></i> Group
<icon :icon="faPlus"/>
Group
</Btn>
</div>
</div>
@ -37,6 +38,7 @@
</template>
<script lang="ts" setup>
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { nextTick, ref } from 'vue'
import { playlistStore } from '@/stores'
import { alerts } from '@/utils'

View file

@ -22,7 +22,7 @@
@input="onGroupChanged"
/>
<Btn class="btn-add-group" green small uppercase @click.prevent="addGroup">
<i class="fa fa-plus"></i> Group
<icon :icon="faPlus"/>
</Btn>
</div>
</div>
@ -37,6 +37,7 @@
</template>
<script lang="ts" setup>
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { computed, inject, reactive, ref, watch } from 'vue'
import { cloneDeep, isEqual } from 'lodash'
import { playlistStore } from '@/stores'

View file

@ -1,6 +1,8 @@
<template>
<div class="row" data-testid="smart-playlist-rule-row">
<Btn class="remove-rule" red @click.prevent="removeRule"><i class="fa fa-times"></i></Btn>
<Btn class="remove-rule" red @click.prevent="removeRule">
<icon :icon="faTimes"/>
</Btn>
<select v-model="selectedModel" name="model[]">
<option v-for="model in models" :key="model.name" :value="model">{{ model.label }}</option>
@ -26,6 +28,7 @@
</template>
<script lang="ts" setup>
import { faTimes } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, ref, toRefs, watch } from 'vue'
import models from '@/config/smart-playlist/models'
import inputTypes from '@/config/smart-playlist/inputTypes'

View file

@ -18,13 +18,14 @@
/>
<Btn @click.prevent="addRule" class="btn-add-rule" green small uppercase>
<i class="fa fa-plus"></i>
<icon :icon="faPlus"/>
Rule
</Btn>
</div>
</template>
<script lang="ts" setup>
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import { defineAsyncComponent, reactive, toRefs } from 'vue'
import { playlistStore } from '@/stores'

View file

@ -13,7 +13,7 @@
<p>
Connecting Koel and your Last.fm account enables such exciting features as
<a
class="text-orange"
class="text-highlight"
href="https://www.last.fm/about/trackmymusic"
rel="noopener"
target="_blank"
@ -23,7 +23,7 @@
</p>
<div class="buttons">
<Btn class="connect" @click.prevent="connect">
<i class="fa fa-lastfm"></i>
<icon :icon="faLastfm"/>
{{ connected ? 'Reconnect' : 'Connect' }}
</Btn>
@ -36,7 +36,7 @@
This installation of Koel has no Last.fm integration.
<span v-if="isAdmin" data-testid="lastfm-admin-instruction">
Visit
<a href="https://docs.koel.dev/3rd-party.html#last-fm" class="text-orange" target="_blank">Koels Wiki</a>
<a href="https://docs.koel.dev/3rd-party.html#last-fm" class="text-highlight" target="_blank">Koels Wiki</a>
for a quick how-to.
</span>
<span v-else data-testid="lastfm-user-instruction">
@ -48,6 +48,7 @@
</template>
<script lang="ts" setup>
import { faLastfm } from '@fortawesome/free-brands-svg-icons'
import { computed, defineAsyncComponent } from 'vue'
import { authService, httpService } from '@/services'
import { forceReloadWindow } from '@/utils'

View file

@ -2,25 +2,26 @@
<div>
<div class="form-row" v-if="!isPhone">
<label>
<input type="checkbox" name="notify" v-model="preferences.notify">
<CheckBox name="notify" v-model="preferences.notify"/>
Show Now Playing song notification
</label>
</div>
<div class="form-row" v-if="!isPhone">
<label>
<input type="checkbox" name="confirm_closing" v-model="preferences.confirmClosing">
<CheckBox name="confirm_closing" v-model="preferences.confirmClosing"/>
Confirm before closing Koel
</label>
</div>
<div class="form-row" v-if="isPhone">
<label>
<CheckBox name="transcode_on_mobile" v-model="preferences.transcodeOnMobile"/>
<input type="checkbox" name="transcode_on_mobile" v-model="preferences.transcodeOnMobile">
Convert and play media at 128kbps on mobile
</label>
</div>
<div class="form-row">
<label>
<input type="checkbox" name="show_album_art_overlay" v-model="preferences.showAlbumArtOverlay">
<CheckBox name="show_album_art_overlay" v-model="preferences.showAlbumArtOverlay"/>
Show a translucent, blurred overlay of the current albums art
</label>
</div>
@ -30,6 +31,7 @@
<script lang="ts" setup>
import isMobile from 'ismobilejs'
import { preferenceStore as preferences } from '@/stores'
import CheckBox from '@/components/ui/CheckBox.vue'
const isPhone = isMobile.phone
</script>

View file

@ -39,12 +39,12 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-frown-o"></i>
<icon :icon="faHeartBroken"/>
</template>
No favorites yet.
<span class="secondary d-block">
Click the&nbsp;
<i class="fa fa-heart-o"></i>&nbsp;
<icon :icon="faHeart"/>&nbsp;
icon to mark a song as favorite.
</span>
</ScreenEmptyState>
@ -52,6 +52,8 @@
</template>
<script lang="ts" setup>
import { faHeartBroken } from '@fortawesome/free-solid-svg-icons'
import { faHeart } from '@fortawesome/free-regular-svg-icons'
import { eventBus, pluralize } from '@/utils'
import { commonStore, favoriteStore } from '@/stores'
import { downloadService } from '@/services'

View file

@ -5,7 +5,7 @@
<div class="main-scroll-wrap" @scroll="scrolling">
<ScreenEmptyState v-if="libraryEmpty">
<template v-slot:icon>
<i class="fa fa-volume-off"></i>
<icon :icon="faVolumeOff"/>
</template>
No songs found.
<span class="secondary d-block">
@ -34,9 +34,9 @@
</template>
<script lang="ts" setup>
import { faVolumeOff } from '@fortawesome/free-solid-svg-icons'
import { sample } from 'lodash'
import { computed } from 'vue'
import { eventBus, noop } from '@/utils'
import { commonStore, overviewStore, userStore } from '@/stores'
import { useAuthorization, useInfiniteScroll } from '@/composables'

View file

@ -39,7 +39,7 @@
<ScreenEmptyState v-if="!songs.length && !loading">
<template v-slot:icon>
<i class="fa fa-file-o"></i>
<icon :icon="faFile"/>
</template>
<template v-if="playlist?.is_smart">
@ -57,6 +57,7 @@
</template>
<script lang="ts" setup>
import { faFile } from '@fortawesome/free-regular-svg-icons'
import { difference } from 'lodash'
import { ref, toRef } from 'vue'
import { alerts, eventBus, pluralize } from '@/utils'

View file

@ -30,7 +30,7 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-coffee"></i>
<icon :icon="faCoffee"/>
</template>
No songs queued.
@ -43,6 +43,7 @@
</template>
<script lang="ts" setup>
import { faCoffee } from '@fortawesome/free-solid-svg-icons'
import { computed, toRef } from 'vue'
import { pluralize } from '@/utils'
import { commonStore, queueStore } from '@/stores'

View file

@ -22,7 +22,7 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-clock-o"></i>
<icon :icon="faClock"/>
</template>
No songs recently played.
<span class="secondary d-block">Start playing to populate this playlist.</span>
@ -31,6 +31,7 @@
</template>
<script lang="ts" setup>
import { faClock } from '@fortawesome/free-regular-svg-icons'
import { eventBus, pluralize } from '@/utils'
import { recentlyPlayedStore } from '@/stores'
import { useSongList } from '@/composables'

View file

@ -6,11 +6,11 @@
<template v-slot:controls>
<BtnGroup uppercased v-if="hasUploadFailures">
<Btn data-testid="upload-retry-all-btn" green @click="retryAll">
<i class="fa fa-repeat"></i>
<icon :icon="faRotateBack"/>
Retry All
</Btn>
<Btn data-testid="upload-remove-all-btn" orange @click="removeFailedEntries">
<i class="fa fa-times"></i>
<icon :icon="faTimes"/>
Remove Failed
</Btn>
</BtnGroup>
@ -33,10 +33,11 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-upload"></i>
<icon :icon="faUpload"/>
</template>
{{ instructionText }}
{{ canDropFolders ? 'Drop files or folders to upload' : 'Drop files to upload' }}
<span class="secondary d-block">
<a class="or-click d-block" role="button">
or click here to select songs
@ -48,7 +49,7 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-exclamation-triangle"></i>
<icon :icon="faWarning"/>
</template>
No media path set.
</ScreenEmptyState>
@ -57,12 +58,13 @@
</template>
<script lang="ts" setup>
import { faRotateBack, faTimes, faUpload, faWarning } from '@fortawesome/free-solid-svg-icons'
import ismobile from 'ismobilejs'
import md5 from 'blueimp-md5'
import { computed, defineAsyncComponent, ref, toRef } from 'vue'
import { settingStore } from '@/stores'
import { eventBus, getAllFileEntries, isDirectoryReadingSupported } from '@/utils'
import { eventBus, getAllFileEntries, isDirectoryReadingSupported as canDropFolders } from '@/utils'
import { acceptedMediaTypes, UploadFile } from '@/config'
import { uploadService } from '@/services'
import { useAuthorization } from '@/composables'
@ -84,10 +86,6 @@ const hasUploadFailures = ref(false)
const { isAdmin } = useAuthorization()
const allowsUpload = computed(() => isAdmin.value && !ismobile.any)
const instructionText = isDirectoryReadingSupported
? 'Drop files or folders to upload'
: 'Drop files to upload'
const onDragEnter = () => (droppable.value = allowsUpload.value)
const onDragLeave = () => (droppable.value = false)

View file

@ -7,7 +7,7 @@
<template v-slot:controls>
<BtnGroup uppercased v-if="showingControls || !isPhone">
<Btn class="btn-add" data-testid="add-user-btn" green @click="showAddUserForm">
<i class="fa fa-plus"></i>
<icon :icon="faPlus"/>
Add
</Btn>
</BtnGroup>
@ -25,6 +25,7 @@
</template>
<script lang="ts" setup>
import { faPlus } from '@fortawesome/free-solid-svg-icons'
import isMobile from 'ismobilejs'
import { defineAsyncComponent, onMounted, ref, toRef } from 'vue'
import { userStore } from '@/stores'

View file

@ -5,7 +5,7 @@
<div id="player">
<ScreenEmptyState data-testid="youtube-placeholder">
<template v-slot:icon>
<i class="fa fa-youtube-play"></i>
<icon :icon="faYoutube"/>
</template>
YouTube videos will be played here.
<span class="d-block instruction">Start a video playback from the right sidebar.</span>
@ -15,6 +15,7 @@
</template>
<script lang="ts" setup>
import { faYoutube } from '@fortawesome/free-brands-svg-icons'
import createYouTubePlayer from 'youtube-player'
import { ref } from 'vue'
import type { YouTubePlayer } from 'youtube-player/dist/types'

View file

@ -52,7 +52,7 @@
<ScreenEmptyState v-else>
<template v-slot:icon>
<i class="fa fa-search"></i>
<icon :icon="faSearch"/>
</template>
Find songs, artists, and albums,
<span class="secondary d-block">all in one place.</span>
@ -62,6 +62,7 @@
</template>
<script lang="ts" setup>
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { ref, toRef } from 'vue'
import { eventBus } from '@/utils'
import { searchStore } from '@/stores'

View file

@ -10,8 +10,7 @@
>
<span :style="{ backgroundImage: `url(${song.album_cover}), url(${defaultCover})` }" class="cover">
<a class="control" @click.prevent="changeSongState" data-testid="play-control">
<i v-if="song.playback_state !== 'Playing'" class="fa fa-play"/>
<i class="fa fa-pause" v-else/>
<icon :icon="song.playback_state === 'Playing' ? faPause : faPlay" class="text-highlight"/>
</a>
</span>
<span class="main">
@ -31,6 +30,7 @@
</template>
<script lang="ts" setup>
import { faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { defaultCover, eventBus, pluralize, startDragging } from '@/utils'
import { queueStore } from '@/stores'
@ -88,7 +88,7 @@ article {
&:hover .cover, &:focus .cover {
.control {
display: block;
display: flex;
}
&::before {
@ -103,8 +103,9 @@ article {
position: relative;
border-radius: 4px;
overflow: hidden;
@include vertical-center();
display: flex;
align-items: center;
justify-content: center;
&::before {
content: " ";
@ -127,16 +128,16 @@ article {
width: 28px;
height: 28px;
background: rgba(0, 0, 0, .5);
line-height: 2rem;
font-size: 1rem;
text-align: center;
z-index: 1;
display: none;
color: var(--color-text-primary);
transition: .3s;
justify-content: center;
align-items: center;
@media (hover: none) {
display: block;
display: flex;
}
}
}

View file

@ -1,11 +1,13 @@
<template>
<button @click.stop="toggleLike" :title="title" class="text-secondary" data-testid="like-btn">
<i class="fa fa-heart text-maroon" v-if="song.liked" data-testid="btn-like-liked"></i>
<i class="fa fa-heart-o" data-testid="btn-like-unliked" v-else></i>
<icon v-if="song.liked" :icon="faHeart" class="text-maroon" data-testid="btn-like-liked"/>
<icon v-else :icon="faEmptyHeart" data-testid="btn-like-unliked"/>
</button>
</template>
<script lang="ts" setup>
import { faHeart } from '@fortawesome/free-solid-svg-icons'
import { faHeart as faEmptyHeart } from '@fortawesome/free-regular-svg-icons'
import { computed, toRefs } from 'vue'
import { favoriteStore } from '@/stores'
@ -19,7 +21,7 @@ const toggleLike = () => favoriteStore.toggleOne(song.value)
<style lang="scss" scoped>
button {
&:hover .fa-heart-o {
&:hover .fa-heart {
color: var(--color-maroon);
}
}

View file

@ -17,8 +17,8 @@
@click="sort('track')"
>
#
<i v-show="sortField === 'track' && sortOrder === 'asc'" class="fa fa-angle-down"></i>
<i v-show="sortField === 'track' && sortOrder === 'desc'" class="fa fa-angle-up"></i>
<icon v-if="sortField === 'track' && sortOrder === 'asc'" :icon="faAngleDown" class="text-highlight"/>
<icon v-if="sortField === 'track' && sortOrder === 'desc'" :icon="faAngleUp" class="text-highlight"/>
</span>
<span
v-if="config.columns.includes('title')"
@ -27,8 +27,8 @@
@click="sort('title')"
>
Title
<i v-show="sortField === 'title' && sortOrder === 'asc'" class="fa fa-angle-down"></i>
<i v-show="sortField === 'title' && sortOrder === 'desc'" class="fa fa-angle-up"></i>
<icon v-if="sortField === 'title' && sortOrder === 'asc'" :icon="faAngleDown" class="text-highlight"/>
<icon v-if="sortField === 'title' && sortOrder === 'desc'" :icon="faAngleUp" class="text-highlight"/>
</span>
<span
v-if="config.columns.includes('artist')"
@ -37,8 +37,8 @@
@click="sort('artist_name')"
>
Artist
<i v-show="sortField === 'artist_name' && sortOrder === 'asc'" class="fa fa-angle-down"></i>
<i v-show="sortField === 'artist_name' && sortOrder === 'desc'" class="fa fa-angle-up"></i>
<icon v-if="sortField === 'artist_name' && sortOrder === 'asc'" :icon="faAngleDown" class="text-highlight"/>
<icon v-if="sortField === 'artist_name' && sortOrder === 'desc'" :icon="faAngleUp" class="text-highlight"/>
</span>
<span
v-if="config.columns.includes('album')"
@ -47,8 +47,8 @@
@click="sort('album_name')"
>
Album
<i v-show="sortField === 'album_name' && sortOrder === 'asc'" class="fa fa-angle-down"></i>
<i v-show="sortField === 'album_name' && sortOrder === 'desc'" class="fa fa-angle-up"></i>
<icon v-if="sortField === 'album_name' && sortOrder === 'asc'" :icon="faAngleDown" class="text-highlight"/>
<icon v-if="sortField === 'album_name' && sortOrder === 'desc'" :icon="faAngleUp" class="text-highlight"/>
</span>
<span
v-if="config.columns.includes('length')"
@ -56,9 +56,10 @@
data-testid="header-length"
@click="sort('length')"
>
<i v-show="sortField === 'length' && sortOrder === 'asc'" class="fa fa-angle-down"></i>
<i v-show="sortField === 'length' && sortOrder === 'desc'" class="fa fa-angle-up"></i>
&nbsp;<i class="duration-header fa fa-clock-o"></i>
<icon v-if="sortField === 'length' && sortOrder === 'asc'" :icon="faAngleDown" class="text-highlight"/>
<icon v-if="sortField === 'length' && sortOrder === 'desc'" :icon="faAngleUp" class="text-highlight"/>
&nbsp;
<icon :icon="faClock" class="duration-header"/>
</span>
<span class="favorite"></span>
<span class="play"></span>
@ -83,6 +84,8 @@
</template>
<script lang="ts" setup>
import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'
import { faClock } from '@fortawesome/free-regular-svg-icons'
import isMobile from 'ismobilejs'
import { findIndex } from 'lodash'
import { computed, inject, nextTick, onMounted, ref, watch } from 'vue'

View file

@ -10,7 +10,8 @@
title="Play all songs"
@click.prevent="playAll"
>
<i class="fa fa-play"></i> All
<icon :icon="faPlay"/>
All
</Btn>
<Btn
@ -20,7 +21,8 @@
title="Play selected songs"
@click.prevent="playSelected"
>
<i class="fa fa-play"></i> Selected
<icon :icon="faPlay"/>
Selected
</Btn>
</template>
@ -33,7 +35,8 @@
title="Shuffle all songs"
@click.prevent="shuffle"
>
<i class="fa fa-random"></i> All
<icon :icon="faRandom"/>
All
</Btn>
<Btn
@ -44,7 +47,8 @@
title="Shuffle selected songs"
@click.prevent="shuffleSelected"
>
<i class="fa fa-random"></i> Selected
<icon :icon="faRandom"/>
Selected
</Btn>
</template>
</template>
@ -69,7 +73,8 @@
title="Delete this playlist"
@click.prevent="deletePlaylist"
>
<i class="fa fa-times"></i> Playlist
<icon :icon="faTimes"/>
Playlist
</Btn>
</BtnGroup>
@ -85,6 +90,7 @@
</template>
<script lang="ts" setup>
import { faPlay, faRandom, faTimes } from '@fortawesome/free-solid-svg-icons'
import { computed, inject, nextTick, onMounted, onUnmounted, ref, toRefs } from 'vue'
import { SelectedSongsKey, SongsKey } from '@/symbols'

View file

@ -14,13 +14,14 @@
<LikeButton :song="song"/>
</span>
<span class="play" data-testid="song-item-play" role="button" @click.stop="doPlayback">
<i class="fa fa-pause-circle" v-if="song.playback_state === 'Playing'"></i>
<i class="fa fa-play-circle" v-else></i>
<icon v-if="song.playback_state === 'Playing'" :icon="faCirclePause"/>
<icon v-else :icon="faCirclePlay"/>
</span>
</div>
</template>
<script lang="ts" setup>
import { faCirclePause, faCirclePlay } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { playbackService } from '@/services'
import { queueStore } from '@/stores'

View file

@ -6,7 +6,7 @@
data-testid="album-artist-thumbnail"
>
<a
class="control control-play font-size-0"
class="control control-play"
href
role="button"
@click.prevent="playOrQueue"
@ -15,12 +15,16 @@
@drop.stop.prevent="onDrop"
@dragover.prevent
>
{{ buttonLabel }}
<span class="hidden">{{ buttonLabel }}</span>
<span class="icon-wrapper">
<icon :icon="faPlay" size="5x"/>
</span>
</a>
</span>
</template>
<script lang="ts" setup>
import { faPlay } from '@fortawesome/free-solid-svg-icons'
import { orderBy } from 'lodash'
import { computed, ref, toRef, toRefs } from 'vue'
import { albumStore, artistStore, queueStore, songStore, userStore } from '@/stores'
@ -157,20 +161,18 @@ const onDrop = async (event: DragEvent) => {
z-index: 1;
}
&::after {
content: "";
width: 60%;
max-width: 128px;
height: 60%;
max-height: 128px;
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTEzcHgiIGhlaWdodD0iMTMxcHgiIHZpZXdCb3g9IjAgMCAxMTMgMTMxIiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPgogICAgPGcgaWQ9InRyaWFuZ2xlIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj4KICAgICAgICA8cG9seWdvbiBpZD0iUG9seWdvbiIgZmlsbD0iI0ZGRkZGRiIgcG9pbnRzPSIxMTMuMDIzNzI5IDY1LjI1NDI4MDMgLTEuNTg1Njc4MzFlLTE0IDEzMC41MDg1NjEgLTUuNjg0MzQxODllLTE0IDAiPjwvcG9seWdvbj4KICAgIDwvZz4KPC9zdmc+);
background-size: 45%;
background-position: 58% 50%;
background-repeat: no-repeat;
border-radius: 50%;
.icon-wrapper {
background-color: var(--color-bg-primary);
color: var(--color-highlight);
opacity: 0;
z-index: 2;
border-radius: 50%;
width: 50%;
height: 50%;
display: flex;
justify-content: center;
align-items: center;
padding-left: 4%;
z-index: 99;
@media (hover: none) {
opacity: .5;
@ -178,7 +180,7 @@ const onDrop = async (event: DragEvent) => {
}
&:hover, &:focus {
&::before, &::after {
&::before, .icon-wrapper {
transition: .3s opacity;
opacity: 1;
}
@ -189,7 +191,7 @@ const onDrop = async (event: DragEvent) => {
background: rgba(0, 0, 0, .5);
}
&::after {
.icon-wrapper {
transform: scale(.9);
}
}
@ -215,4 +217,8 @@ const onDrop = async (event: DragEvent) => {
}
}
}
.compact .icon-wrapper {
font-size: .3rem; // to control the size of the icon
}
</style>

View file

@ -11,6 +11,9 @@ button {
font-size: 1rem;
padding: .6rem 1rem;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: .3rem;
&:hover {
box-shadow: inset 0 0 0 10rem rgba(0, 0, 0, .05);

View file

@ -1,9 +1,12 @@
<template>
<button data-testid="close-modal-btn" type="button" @click.prevent="$emit('click')">
<i class="fa fa-times"></i>
<icon :icon="faTimes"/>
</button>
</template>
<script lang="ts" setup>
import { faTimes } from '@fortawesome/free-solid-svg-icons'</script>
<style lang="scss" scoped>
button {
display: flex;

View file

@ -1,12 +1,14 @@
<template>
<Transition name="fade">
<button v-show="showing" ref="el" title="Scroll to top" type="button" @click="scrollToTop">
<i class="fa fa-arrow-circle-up"/> Top
<icon :icon="faCircleUp"/>&nbsp;
Top
</button>
</Transition>
</template>
<script lang="ts" setup>
import { faCircleUp } from '@fortawesome/free-solid-svg-icons'
import { onMounted, ref } from 'vue'
import { $ } from '@/utils'
@ -39,10 +41,6 @@ button {
background: rgba(0, 0, 0, .5);
border: 1px solid var(--color-text-primary);
color: var(--color-text-primary);
i {
margin-right: 4px;
}
}
@media screen and (min-width: 415px) {

View file

@ -0,0 +1,37 @@
<template>
<span>
<input :checked="checked" type="checkbox" v-bind="$attrs" @input="onInput">
<icon :icon="faCheck" v-if="checked"/>
</span>
</template>
<script lang="ts" setup>
import { faCheck } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
const props = withDefaults(defineProps<{ modelValue?: any }>(), {
modelValue: false
})
const checked = ref(props.modelValue)
const emit = defineEmits(['update:modelValue'])
const onInput = (event: InputEvent) => {
checked.value = (event.target as HTMLInputElement).checked
emit('update:modelValue', checked.value)
}
</script>
<style scoped>
span {
position: relative;
}
svg {
color: var(--color-highlight);
position: absolute;
top: 1px;
left: 2px;
}
</style>

View file

@ -6,6 +6,7 @@
<option disabled value="-1">Preset</option>
<option v-for="preset in presets" :value="preset.id" :key="preset.id" v-once>{{ preset.name }}</option>
</select>
<icon :icon="faAngleDown" class="arrow text-highlight" size="sm"/>
</label>
</div>
<div class="bands">
@ -30,6 +31,7 @@
<script lang="ts" setup>
import noUiSlider from 'nouislider'
import { faAngleDown } from '@fortawesome/free-solid-svg-icons'
import { nextTick, onMounted, ref, watch } from 'vue'
import { eventBus } from '@/utils'
import { equalizerStore, preferenceStore as preferences } from '@/stores'
@ -204,14 +206,8 @@ onMounted(() => eventBus.on('INIT_EQUALIZER', () => init()))
position: relative;
margin-bottom: 0;
&::after {
content: '\f107';
font-family: FontAwesome;
color: var(--color-highlight);
display: inline-block;
position: absolute;
right: 8px;
top: 6px;
.arrow {
margin-left: -14px;
pointer-events: none;
}
}

View file

@ -9,7 +9,7 @@
<p v-if="song.id && !song.lyrics" class="none text-secondary">
<template v-if="isAdmin">
No lyrics found.
<button class="text-orange" data-testid="add-lyrics-btn" type="button" @click.prevent="showEditSongForm">
<button class="text-highlight" data-testid="add-lyrics-btn" type="button" @click.prevent="showEditSongForm">
Click here
</button>
to add lyrics.

View file

@ -2,10 +2,10 @@
<div v-if="state.showing" id="overlay" :class="state.type" class="overlay" data-testid="overlay">
<div class="display">
<SoundBar v-if="state.type === 'loading'"/>
<i v-if="state.type === 'error'" class="fa fa-exclamation-circle"/>
<i v-if="state.type === 'warning'" class="fa fa-exclamation-triangle"/>
<i v-if="state.type === 'info'" class="fa fa-info-circle"/>
<i v-if="state.type === 'success'" class="fa fa-check-circle"/>
<icon v-if="state.type === 'error'" :icon="faCircleExclamation"/>
<icon v-if="state.type === 'warning'" :icon="faWarning"/>
<icon v-if="state.type === 'info'" :icon="faCircleInfo"/>
<icon v-if="state.type === 'success'" :icon="faCircleCheck"/>
<span class="message" v-html="state.message"/>
</div>
@ -15,6 +15,7 @@
</template>
<script lang="ts" setup>
import { faCircleCheck, faCircleExclamation, faCircleInfo, faWarning } from '@fortawesome/free-solid-svg-icons'
import { eventBus } from '@/utils'
import { defineAsyncComponent, reactive } from 'vue'
@ -48,8 +49,8 @@ eventBus.on({
.display {
@include vertical-center();
i {
margin-right: 6px;
.message {
margin-left: 6px;
}
}

View file

@ -1,17 +1,22 @@
<template>
<button
:class="mode"
:class="{ active: mode !== 'NO_REPEAT' }"
:title="`Change repeat mode (current mode: ${readableMode})`"
class="control"
data-testid="repeat-mode-switch"
type="button"
@click.prevent="changeMode"
>
<i class="fa fa-repeat"></i>
<FontAwesomeLayers>
<icon :icon="faRepeat"/>
<FontAwesomeLayersText v-if="mode === 'REPEAT_ONE'" counter value="1"/>
</FontAwesomeLayers>
</button>
</template>
<script lang="ts" setup>
import { FontAwesomeLayers, FontAwesomeLayersText } from '@fortawesome/vue-fontawesome'
import { faRepeat } from '@fortawesome/free-solid-svg-icons'
import { computed, toRef } from 'vue'
import { playbackService } from '@/services'
import { preferenceStore } from '@/stores'
@ -28,25 +33,17 @@ const changeMode = () => playbackService.changeRepeatMode()
</script>
<style lang="scss" scoped>
button {
position: relative;
.fa-layers-counter {
transform: none;
font-size: .45rem;
font-weight: bold;
right: 2px;
top: 2px;
color: var(--color-highlight);
background: transparent;
}
&.REPEAT_ALL, &.REPEAT_ONE {
color: var(--color-highlight);
}
&.REPEAT_ONE::after {
content: "1";
position: absolute;
display: flex;
place-content: center;
place-items: center;
top: 0;
left: 0;
font-weight: 700;
font-size: .5rem;
width: 100%;
height: 100%;
}
.active {
color: var(--color-highlight);
}
</style>

View file

@ -1,11 +1,11 @@
<template>
<span v-if="isMobile.phone" class="text-orange" data-testid="controls-toggle" @click="$emit('toggleControls')">
<i v-if="showingControls" class="fa fa-angle-up toggle"/>
<i v-else class="fa fa-angle-down toggle"/>
<span v-if="isMobile.phone" class="text-highlight" data-testid="controls-toggle" @click="$emit('toggleControls')">
<icon v-if="showingControls" :icon="showingControls ? faAngleUp : faAngleDown" class="toggle"/>
</span>
</template>
<script lang="ts" setup>
import { faAngleDown, faAngleUp } from '@fortawesome/free-solid-svg-icons'
import isMobile from 'ismobilejs'
import { toRefs } from 'vue'

View file

@ -2,9 +2,7 @@
<div class="screen-empty-state" data-testid="screen-empty-state">
<div>
<span class="jumbo-icon">
<slot name="icon">
<i class="fa fa-upload"></i>
</slot>
<slot name="icon"></slot>
</span>
<div class="text">
<slot>Placeholder text goes here.</slot>

View file

@ -1,15 +1,16 @@
<template>
<span>
<button title="Zoom out" type="button" @click.prevent="zoom(-1)">
<i class="fa fa-search-minus"/>
<icon :icon="faSearchMinus"/>
</button>
<button title="Zoom in" type="button" @click.prevent="zoom(1)">
<i class="fa fa-search-plus"/>
<icon :icon="faSearchPlus"/>
</button>
</span>
</template>
<script lang="ts" setup>
import { faSearchMinus, faSearchPlus } from '@fortawesome/free-solid-svg-icons'
import { toRefs } from 'vue'
const props = defineProps<{ target: HTMLElement | null }>()

View file

@ -1,8 +1,9 @@
<template>
<i :title="title" class="fa fa-question-circle help-trigger text-blue"/>
<icon :icon="faCircleQuestion" :title="title" class="help-trigger text-blue"/>
</template>
<script lang="ts" setup>
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons'
import { toRefs } from 'vue'
const props = defineProps<{ title: string }>()

View file

@ -7,7 +7,7 @@
title="View as thumbnails"
>
<input v-model="value" class="hidden" name="view-mode" type="radio" value="thumbnails">
<i class="fa fa-th-large"/>
<icon :icon="faThumbnailsHehe"/>
<span class="hidden">View as thumbnails</span>
</label>
@ -18,13 +18,15 @@
title="View as list"
>
<input v-model="value" class="hidden" name="view-mode" type="radio" value="list">
<i class="fa fa-list"/>
<icon :icon="faList"/>
<span class="hidden">View as list</span>
</label>
</span>
</template>
<script lang="ts" setup>
import { faMicrosoft as faThumbnailsHehe } from '@fortawesome/free-brands-svg-icons'
import { faList } from '@fortawesome/free-solid-svg-icons'
import { computed } from 'vue'
const props = withDefaults(defineProps<{ modelValue?: ArtistAlbumViewMode }>(), { modelValue: 'thumbnails' })
@ -46,9 +48,10 @@ const value = computed({
label {
width: 50%;
text-align: center;
line-height: 2rem;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
margin-bottom: 0;
cursor: pointer;

View file

@ -1,7 +1,24 @@
<template>
<span id="volume" class="volume control">
<i v-if="muted" class="fa fa-volume-off" role="button" tabindex="0" title="Unmute" @click="unmute"/>
<i v-else class="fa fa-volume-up" role="button" tabindex="0" title="Mute" @click="mute"/>
<icon
v-if="level === 'muted'"
:icon="faVolumeMute"
fixed-width
role="button"
tabindex="0"
title="Unmute"
@click="unmute"
/>
<icon
v-else
:icon="level === 'discreet' ? faVolumeLow : faVolumeHigh"
fixed-width
role="button"
tabindex="0"
title="Mute"
@click="mute"
/>
<input
id="volumeInput"
class="plyr__volume"
@ -17,33 +34,40 @@
</template>
<script lang="ts" setup>
import { faVolumeHigh, faVolumeLow, faVolumeMute } from '@fortawesome/free-solid-svg-icons'
import { ref } from 'vue'
import { playbackService, socketService } from '@/services'
import { preferenceStore as preferences } from '@/stores'
import { eventBus } from '@/utils'
const muted = ref(false)
const level = ref<'muted' | 'discreet' | 'loud'>()
const mute = () => {
muted.value = true
playbackService.mute()
level.value = 'muted'
}
const unmute = () => {
muted.value = false
playbackService.unmute()
level.value = preferences.volume < 3 ? 'discreet' : 'loud'
}
const setVolume = (e: InputEvent) => {
const volume = parseFloat((e.target as HTMLInputElement).value)
playbackService.setVolume(volume)
muted.value = volume === 0
setLevel(volume)
}
const setLevel = (volume: number) => (level.value = volume === 0 ? 'muted' : volume < 3 ? 'discreet' : 'loud')
/**
* Broadcast the volume changed event to remote controller.
*/
const broadcastVolume = (e: InputEvent) => {
socketService.broadcast('SOCKET_VOLUME_CHANGED', parseFloat((e.target as HTMLInputElement).value))
}
eventBus.on('KOEL_READY', () => setLevel(preferences.volume))
</script>
<style lang="scss">
@ -53,11 +77,11 @@ const broadcastVolume = (e: InputEvent) => {
// More tweaks
[type=range] {
margin: -1px 0 0 5px;
margin: 0 0 0 8px;
transform: rotate(270deg);
transform-origin: 0;
position: absolute;
bottom: -25px;
bottom: -22px;
border: 14px solid var(--color-bg-primary);
border-left-width: 30px;
z-index: 0;
@ -70,8 +94,7 @@ const broadcastVolume = (e: InputEvent) => {
display: block;
}
i {
width: 16px;
[role=button] {
position: relative;
z-index: 1;
}

View file

@ -5,10 +5,10 @@
<span class="name">{{ file.name }}</span>
<span class="controls">
<Btn v-if="canRetry" icon-only title="Retry" transparent unrounded @click="retry">
<i class="fa fa-repeat"></i>
<icon :icon="faRotateBack"/>
</Btn>
<Btn v-if="canRemove" icon-only title="Remove" transparent unrounded @click="remove">
<i class="fa fa-times"></i>
<icon :icon="faTimes"/>
</Btn>
</span>
</span>
@ -17,6 +17,7 @@
<script lang="ts" setup>
import slugify from 'slugify'
import { faRotateBack, faTimes } from '@fortawesome/free-solid-svg-icons'
import { computed, defineAsyncComponent, toRefs } from 'vue'
import { UploadFile } from '@/config'
import { uploadService } from '@/services'

View file

@ -13,12 +13,13 @@
role="button"
@click.prevent="logout"
>
<i class="fa fa-sign-out"></i>
<icon :icon="faSignOutAlt"/>
</a>
</span>
</template>
<script lang="ts" setup>
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons'
import { eventBus } from '@/utils'
import { useAuthorization } from '@/composables'

View file

@ -5,8 +5,13 @@
<main>
<h1>
<span class="name">{{ user.name }}</span>
<i v-if="isCurrentUser" class="you text-orange fa fa-check-circle" title="This is you!"/>
<i v-if="user.is_admin" class="is-admin text-blue fa fa-shield" title="User has admin privileges"/>
<icon v-if="isCurrentUser" :icon="faCircleCheck" class="you text-highlight" title="This is you!"/>
<icon
v-if="user.is_admin"
:icon="faShield"
class="is-admin text-blue"
title="User has admin privileges"
/>
</h1>
<p class="email text-secondary">{{ user.email }}</p>
@ -24,6 +29,7 @@
</template>
<script lang="ts" setup>
import { faCircleCheck, faShield } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { userStore } from '@/stores'
import { alerts, eventBus } from '@/utils'

View file

@ -18,18 +18,16 @@
<p class="none text-secondary" v-else>No song is playing.</p>
<footer>
<a class="favorite" @click.prevent="toggleFavorite">
<i class="fa fa-heart yep" v-if="song?.liked"></i>
<i class="fa fa-heart-o" v-else></i>
<icon :icon="song?.liked ? faHeart : faEmptyHeart"/>
</a>
<a class="prev" @click="playPrev">
<i class="fa fa-step-backward"></i>
<icon :icon="faStepBackward"/>
</a>
<a class="play-pause" @click.prevent="togglePlayback">
<i class="fa fa-pause" v-if="playing"/>
<i class="fa fa-play" v-else/>
<icon :icon="playing ? faPause : faPlay"/>
</a>
<a class="next" @click.prevent="playNext">
<i class="fa fa-step-forward"></i>
<icon :icon="faStepForward"/>
</a>
<span class="volume">
<span
@ -39,8 +37,7 @@
v-koel-clickaway="closeVolumeSlider"
/>
<span class="icon" @click.stop="toggleVolumeSlider">
<i class="fa fa-volume-off" v-if="muted"/>
<i class="fa fa-volume-up" v-else/>
<icon :icon="muted ? faVolumeHigh : faVolumeMute" fixed-width/>
</span>
</span>
</footer>
@ -52,7 +49,7 @@
</div>
<p v-else>
No active Koel instance found.
<a class="rescan text-orange" @click.prevent="rescan">Rescan</a>
<a class="rescan text-highlight" @click.prevent="rescan">Rescan</a>
</p>
</div>
</main>
@ -65,6 +62,17 @@
</template>
<script lang="ts" setup>
import {
faHeart,
faPause,
faPlay,
faStepBackward,
faStepForward,
faVolumeHigh,
faVolumeMute
} from '@fortawesome/free-solid-svg-icons'
import { faHeart as faEmptyHeart } from '@fortawesome/free-regular-svg-icons'
import noUISlider from 'nouislider'
import { authService, socketService } from '@/services'
import { preferenceStore, userStore } from '@/stores'

View file

@ -75,7 +75,6 @@ export default [
thumbnailUrl: bgRosePetal,
properties: {
'--color-bg-primary': '#7d083b',
'--color-highlight': '#d84179',
'--bg-image': `url(${bgRosePetal})`
}
},
@ -86,7 +85,6 @@ export default [
thumbnailUrl: bgPurpleWaves,
properties: {
'--color-bg-primary': '#44115c',
'--color-highlight': '#b854dd',
'--bg-image': `url(${bgPurpleWaves})`
}
},

View file

@ -3,7 +3,6 @@
@import '#/vendor/_reset.scss';
@import '@modules/nouislider/distribute/nouislider.min.css';
@import '@modules/font-awesome/css/font-awesome.min.css';
@import "#/partials/_vars.scss";
@import "#/partials/_hacks.scss";

View file

@ -218,7 +218,6 @@
margin-top: 24px;
font-size: .9rem;
text-align: right;
clear: both;
a {
color: var(--color-text-primary);
@ -246,20 +245,6 @@
}
}
}
button.play {
font-size: 1rem;
width: 2rem;
height: 2rem;
border: 1px solid var(--color-text-secondary);
border-radius: 9999rem;
text-indent: .1rem; // the Play triangle icon doesn't look balanced otherwise
opacity: .5;
&:hover {
opacity: 1;
}
}
}
@mixin inset-when-pressed() {

View file

@ -19,7 +19,7 @@ body, html {
}
input, select, button, textarea, .btn {
-webkit-appearance: none;
appearance: none;
border: 0;
outline: 0;
font-family: var(--font-family);
@ -67,54 +67,19 @@ select {
background-repeat: no-repeat;
}
strong {
font-weight: var(--font-weight-normal);
}
.hidden {
display: none;
display: none !important;
}
input[type="checkbox"] {
border: 1px solid rgba(255, 255, 255, .3);
border-radius: 0;
clear: none;
border-radius: 2px;
cursor: pointer;
display: inline-block;
line-height: 0;
height: 16px;
height: 15px;
margin: -4px 4px 0 0;
outline: 0;
padding: 0 !important;
text-align: center;
vertical-align: middle;
width: 16px;
min-width: 16px;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1);
width: 15px;
background: var(--color-text-primary);
&:before {
font-family: FontAwesome;
color: var(--color-highlight);
display: inline-block;
vertical-align: middle;
width: 14px;
height: 14px;
line-height: 1.3rem;
text-align: center;
}
&:checked {
&:before {
content: "\f00c";
}
}
&:indeterminate {
&:before {
content: "\f068";
}
}
}
a {
@ -130,14 +95,6 @@ a {
}
}
.clear, .clearfix {
&::after {
content: " ";
clear: both;
display: block;
}
}
.ir {
color: transparent;
font: 0/0 serif;
@ -289,7 +246,7 @@ label {
}
.text- {
&white {
&primary {
color: var(--color-text-primary) !important;
}
@ -297,7 +254,7 @@ label {
color: var(--color-text-secondary) !important;
}
&orange {
&highlight {
color: var(--color-highlight) !important;
}
@ -313,6 +270,10 @@ label {
color: var(--color-blue) !important;
}
&green {
color: var(--color-green) !important;
}
&uppercase {
text-transform: uppercase !important;
}

View file

@ -3,7 +3,6 @@
@import '#/vendor/_reset.scss';
@import '@modules/nouislider/distribute/nouislider.min.css';
@import '@modules/font-awesome/css/font-awesome.min.css';
@import "#/partials/_vars.scss";
@import "#/partials/_shared.scss";

View file

@ -25,7 +25,7 @@ export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "#/partials/_mixins.scss";`
additionalData: '@import "#/partials/_mixins.scss";'
}
}
},

View file

@ -973,6 +973,44 @@
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-6.2.0.tgz#6d836ee8a580589b60c9088a3bdb0b669a468825"
integrity sha512-3kIcQ+aTr3I+LqDbJwbINFk5oA+a63LH57GPJt9PM8AWqN7nCwnubuSiWosHYQlyf2NicrvpzXQxllyLeEdpyQ==
"@fortawesome/fontawesome-common-types@6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105"
integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA==
"@fortawesome/fontawesome-svg-core@^6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.1.1.tgz#3424ec6182515951816be9b11665d67efdce5b5f"
integrity sha512-NCg0w2YIp81f4V6cMGD9iomfsIj7GWrqmsa0ZsPh59G7PKiGN1KymZNxmF00ssuAlo/VZmpK6xazsGOwzKYUMg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.1.1"
"@fortawesome/free-brands-svg-icons@^6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.1.1.tgz#3580961d4f42bd51dc171842402f23a18a5480b1"
integrity sha512-mFbI/czjBZ+paUtw5NPr2IXjun5KAC8eFqh1hnxowjA4mMZxWz4GCIksq6j9ZSa6Uxj9JhjjDVEd77p2LN2Blg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.1.1"
"@fortawesome/free-regular-svg-icons@^6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz#3f2f58262a839edf0643cbacee7a8a8230061c98"
integrity sha512-xXiW7hcpgwmWtndKPOzG+43fPH7ZjxOaoeyooptSztGmJxCAflHZxXNK0GcT0uEsR4jTGQAfGklDZE5NHoBhKg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.1.1"
"@fortawesome/free-solid-svg-icons@^6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.1.1.tgz#3369e673f8fe8be2fba30b1ec274d47490a830a6"
integrity sha512-0/5exxavOhI/D4Ovm2r3vxNojGZioPwmFrKg0ZUH69Q68uFhFPs6+dhAToh6VEQBntxPRYPuT5Cg1tpNa9JUPg==
dependencies:
"@fortawesome/fontawesome-common-types" "6.1.1"
"@fortawesome/vue-fontawesome@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.0.1.tgz#ced35cefc52b364f7db973f2fe9f50c3dd160715"
integrity sha512-CdXZJoCS+aEPec26ZP7hWWU3SaJlQPZSCGdgpQ2qGl2HUmtUUNrI3zC4XWdn1JUmh3t5OuDeRG1qB4eGRNSD4A==
"@hapi/hoek@^9.0.0":
version "9.2.1"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17"
@ -3484,11 +3522,6 @@ follow-redirects@^1.14.8:
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7"
integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==
font-awesome@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM=
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"