Compare commits

...

76 Commits

Author SHA1 Message Date
mungai-njoroge
c7036b3a5d fix: enable typing to search on mobile view 2024-01-09 14:29:48 +03:00
mungai-njoroge
3075517af9 add silence skip to quick settings 2024-01-08 17:23:46 +03:00
mungai-njoroge
e8f0bc6b8b add quick actions section in settings
+ set CTRL + B to toggle sidebar
2024-01-07 17:20:50 +03:00
mungai-njoroge
ded3a48e0b extract crossfade into separate file 2024-01-05 01:01:41 +03:00
mungai-njoroge
a04a3c4fe4 fix: queue save as playlist 2024-01-04 20:07:45 +03:00
mungai-njoroge
4aaf70ba68 add context menu option to remove track from queue 2024-01-04 10:53:54 +03:00
mungai-njoroge
fa176cb027 remove queue is too short for shuffle warning 2024-01-04 10:45:55 +03:00
mungai-njoroge
c4ce344487 Add shuffle to bottom bar 2024-01-04 10:43:30 +03:00
mungai-njoroge
f2c7cccba1 increase playlist list grid width 2024-01-04 10:30:34 +03:00
mungai-njoroge
901406a337 respect use_crossfade in preloaded track move forward 2024-01-04 01:38:16 +03:00
mungai-njoroge
4b48b13561 add switch to explictly turn on crossfade
~ makes it possible to provide a default crossfade duration
+ hide the experimental flag on chromium browsers
2024-01-02 00:36:46 +03:00
mungai-njoroge
10b29f6349 rewrite tracker to use keys
+ fix tracker throttle function not working
2024-01-01 22:34:57 +03:00
mungai-njoroge
b24d833d12 add search on social to track context menu 2024-01-01 21:02:51 +03:00
mungai-njoroge
9004c02898 fix track play next
+ fix no repeat
2024-01-01 20:40:04 +03:00
mungai-njoroge
409edba74c add audio settings group
+ add components for crossfade duration
2024-01-01 20:18:08 +03:00
mungai-njoroge
2f1c07f786 disable crossfade if duration < 1000 2023-12-27 21:12:54 +03:00
mungai-njoroge
278e73745a rewrite add tracks to queue methods to use the insertAt method
+ clear timeout on seek
2023-12-27 20:23:56 +03:00
mungai-njoroge
3652432e0b + fix crossfade
+ move silence fetch to worker
+ disablecrossfade on manually triggered track plays
+ move remove track from queue to tracklist
+ fix tracker not working by reassigning event listeners
2023-12-27 19:38:19 +03:00
mungai-njoroge
f2e157a746 gapless initial try 2023-12-22 11:20:51 +03:00
mungai-njoroge
606515ffd5 hide recent items on homepage if they don't exist 2023-12-19 09:27:44 +03:00
mungai-njoroge
6ff67e5f94 resize song items grid size 2023-12-14 10:03:26 +03:00
mungai-njoroge
5f037fc647 add fetch callbacks to sidebar search 2023-12-14 09:52:43 +03:00
mungai-njoroge
d1c62f701e fix broken layout on favorite artists & albums page 2023-12-14 09:51:32 +03:00
mungai-njoroge
b2bef03373 remove see all from top search results 2023-12-14 09:23:14 +03:00
mungai-njoroge
49fe308da0 fix no bottom album rows on album page
+ add store resetter to artist page
2023-12-12 19:54:20 +03:00
mungai-njoroge
bce2772dcb show favorite tracks index in reverse order 2023-12-11 17:40:50 +03:00
mungai-njoroge
dd0b9d6d61 add store resetter methods
+ hide edit button on recently added playlist
2023-12-11 13:29:27 +03:00
mungai-njoroge
1e90298f72 fix padding left on tab headers on search page 2023-12-11 08:31:25 +03:00
mungai-njoroge
7f4385f8cb reduce cardScroller padding
+ spice up hire me
2023-12-11 08:27:21 +03:00
mungai-njoroge
48f2a73291 update settings text 2023-12-10 18:33:34 +03:00
mungai-njoroge
8cf33089a2 default to no sidebar 2023-12-10 18:15:03 +03:00
mungai-njoroge
c16be33d40 fix items glitch on search tracks page 2023-12-10 18:01:33 +03:00
mungai-njoroge
a9691a2a25 rewrite search grid pages with card row virtual list
+ rewrite sidebar tabs with infinite scroll and card row virtual lists
2023-12-10 16:22:43 +03:00
mungai-njoroge
e88e6dcc2d add favorites on recently played
+ create track logger timestamp on track play start
2023-12-10 13:28:52 +03:00
mungai-njoroge
00ffbdbc42 fetch app version from server 2023-12-09 22:16:31 +03:00
mungai-njoroge
97f348daf2 fix play from track card on favorites page 2023-12-09 17:52:18 +03:00
mungai-njoroge
d0f47b4504 move folder to top of browse list 2023-12-09 09:16:53 +03:00
mungai-njoroge
2db6bfebcf move browse to top of homepage
+ move it to separate component
2023-12-08 18:49:53 +03:00
mungai-njoroge
286003cf27 add album and artist list pages
+ fix number localization
+ a lot other things
2023-12-08 09:18:13 +03:00
mungai-njoroge
d27c61c7ce add google, yt, wikipedia and lastfm to album & artist search
+ persist tracker store
+ add generic header to favorites page
+ move lyrics components to Nowplaying page folder
+ update version
2023-12-06 11:05:05 +03:00
mungai-njoroge
c56ee65a73 fix padding right and move card scroller 2023-12-05 15:48:00 +03:00
mungai-njoroge
d3c0c7c596 rewrite appgrid to fix scrollbar paddng-issues 2023-12-04 20:50:34 +03:00
mungai-njoroge
e7fec30b7c fix card size issues on search page 2023-12-04 14:16:44 +03:00
mungai-njoroge
da36f8d7dd Redesign discography with generic header and tabs
+ extract settings tabs into generic tabs
2023-12-04 13:48:52 +03:00
mungai-njoroge
63de7a6613 Remove unused vue files after refactor 2023-12-03 23:31:16 +03:00
mungai-njoroge
b14c814c55 add see all link to card scroller 2023-12-03 20:25:54 +03:00
mungai-njoroge
7f8293e691 migrate horizontal scrollers to use a single component
+ rewrite album view with the dynamic scroller
2023-12-03 20:15:58 +03:00
mungai-njoroge
59a27d4489 log track on end event
+ add recently played to homepage
+ redesign playlist card to match others
+ update pinia persistented state package
2023-12-03 18:27:20 +03:00
mungai-njoroge
4e59e73ec5 maybe: fix scrolling on horizontal card lists 2023-12-03 12:58:19 +03:00
mungai-njoroge
df1c909fce add greetings and misc 2023-12-02 12:43:06 +03:00
mungai-njoroge
3aa0aebfc6 add recent items section in the homepage 2023-12-02 01:58:37 +03:00
mungai-njoroge
afdbb0dbb5 set up track logging via web worker 2023-12-01 10:55:47 +03:00
mungai-njoroge
9fc37034a6 lint code 2023-11-26 15:37:53 +03:00
mungai-njoroge
53fc0c6656 fix play from album 2023-11-26 15:33:22 +03:00
mungai-njoroge
cfe57b788b fix track add to queue 2023-11-25 14:30:26 +03:00
mungai-njoroge
3b55cc1c2c add about page in settings 2023-11-25 12:03:29 +03:00
mungai-njoroge
47c41be79a convert player into setup store 2023-11-25 02:56:31 +03:00
mungai-njoroge
cfc9c2632b break store into 2 and refactor 2023-11-24 00:10:41 +03:00
mungai-njoroge
c0cb2791d0 fix #20 2023-11-14 20:54:23 +03:00
mungai-njoroge
6520b686a3 release v1.4.0 2023-11-14 14:34:59 +03:00
mungai-njoroge
4041c8f588 add context option to search albums and artists on spotify, etc. 2023-11-12 23:21:11 +03:00
mungai-njoroge
da63f481c6 replace now playing with up next on now playing page
+ fix context menu flag watcher bug
2023-11-08 11:06:29 +03:00
mungai-njoroge
a5bcaadafb fix broken settings on ngrok 2023-11-07 23:54:51 +03:00
mungai-njoroge
19142b284a hook plugins settings to settings store
+ misc
2023-11-07 17:14:19 +03:00
mungai-njoroge
511fa58d66 fix disappearing artist separator input on settings page 2023-11-07 10:12:56 +03:00
mungai-njoroge
2fbac120b2 rewrite settings page to use vue router 2023-11-07 09:26:41 +03:00
mungai-njoroge
dea36af5cc clean lyrics plugin
+ update now playing page links to replace current route
+ remove versions from lyrics plugin
+ misc, etc.
2023-11-07 01:39:17 +03:00
mungai-njoroge
81d28461f6 Add settings for lyrics plugin 2023-11-03 17:16:45 +03:00
mungai-njoroge
92302e87e5 move plugin to store and extract dropdown 2023-11-03 16:13:12 +03:00
mungai-njoroge
1fd30b4ac3 add ui for lyrics plugin 2023-11-03 10:40:28 +03:00
mungai-njoroge
f751bbac97 use tabs in now playing page 2023-11-02 12:16:32 +03:00
mungai-njoroge
feb99103b8 break lyrics componnent into 2
+ add lyrics to now playing page
+ remove lyrics from sidebar: it make the sidebar look cluttered
2023-11-01 23:48:31 +03:00
mungai-njoroge
0851c76e65 show lyrics indicatior 2023-10-31 22:36:50 +03:00
mungai-njoroge
bf1f3bf00a refactors and error handling 2023-10-30 17:44:52 +03:00
mungai-njoroge
c0dd04bc94 add lyrics to sidebar 2023-10-30 12:44:33 +03:00
Mungai Njoroge
c2aba79db7 Merge pull request #19 from swing-opensource/handle-album-versions
v1.3.0
2023-10-11 15:00:13 -07:00
196 changed files with 5246 additions and 2543 deletions

View File

@@ -21,9 +21,7 @@
"motion": "^10.15.5",
"node-vibrant": "3.1.6",
"pinia": "^2.0.17",
"pinia-plugin-persistedstate": "^2.1.1",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"pinia-plugin-persistedstate": "^3.2.0",
"v-wave": "^1.5.0",
"vue": "^v3.2.45",
"vue-debounce": "^3.0.2",
@@ -40,6 +38,8 @@
"eslint": "^8.46.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-vue": "^9.17.0",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"typescript": "^5.0.4",
"vite": "^3.0.4",
"vite-plugin-compression": "^0.5.1",

View File

@@ -0,0 +1,15 @@
onmessage = (e) => {
const { trackhash, duration, source, timestamp } = e.data;
const is_dev = location.port === "5173";
const base_url = is_dev ? "http://localhost:1980" : location.origin;
const url = base_url + "/logger/track/log";
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ trackhash, duration, source, timestamp }),
});
};

18
public/workers/silence.js Normal file
View File

@@ -0,0 +1,18 @@
onmessage = async (e) => {
const { ending_file, starting_file } = e.data;
const is_dev = location.port === "5173";
const base_url = is_dev ? "http://localhost:1980" : location.origin;
const url = base_url + "/file/silence";
const res = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ end: ending_file, start: starting_file }),
});
const data = await res.json();
postMessage(data);
};

View File

@@ -10,13 +10,13 @@
NoSideBorders: !xxl,
extendWidth: settings.extend_width && settings.can_extend_width,
}"
:style="{ maxWidth: `${content_height > 1080 ? '2220px' : '1720px'}` }"
:style="{ maxWidth: `${content_height > 1080 ? '2220px' : '1760px'}` }"
>
<LeftSidebar v-if="!isMobile" />
<NavBar />
<div id="acontent" v-element-size="updateContentElemSize">
<div id="acontent" ref="appcontent" v-element-size="updateContentElemSize">
<BalancerProvider>
<router-view />
<RouterView />
</BalancerProvider>
</div>
<RightSideBar v-if="settings.use_sidebar && xl" />
@@ -27,10 +27,10 @@
<script setup lang="ts">
// @libraries
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import { onStartTyping } from "@vueuse/core";
import { vElementSize } from "@vueuse/components";
import { onStartTyping } from "@vueuse/core";
import { onMounted, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { BalancerProvider } from "vue-wrap-balancer";
// @stores
@@ -38,11 +38,13 @@ import {
content_height,
content_width,
isMobile,
updateCardWidth,
} from "@/stores/content-width";
import useModalStore from "@/stores/modal";
import useQStore from "@/stores/queue";
import useSettingsStore from "@/stores/settings";
import useQueueStore from "@/stores/queue";
import useLyrics from "@/stores/lyrics";
import useModal from "@/stores/modal";
import useQueue from "@/stores/queue";
import useSettings from "@/stores/settings";
import useTracker from "@/stores/tracker";
// @utils
import handleShortcuts from "@/helpers/useKeyboard";
@@ -56,33 +58,42 @@ import Notification from "@/components/Notification.vue";
// @app-grid-components
import BottomBar from "@/components/BottomBar/BottomBar.vue";
import LeftSidebar from "@/components/LeftSidebar/index.vue";
import NavBar from "@/components/nav/NavBar.vue";
import RightSideBar from "@/components/RightSideBar/Main.vue";
import LeftSidebar from "@/components/LeftSidebar/index.vue";
import { getAllSettings } from "@/requests/settings";
import { getRootDirs } from "@/requests/settings/rootdirs";
import { baseApiUrl } from "@/config";
// import BubbleManager from "./components/bubbles/BinManager.vue";
const appcontent: Ref<HTMLLegendElement | null> = ref(null);
const queue = useQueue();
const modal = useModal();
const lyrics = useLyrics();
const router = useRouter();
const queue = useQueueStore();
const modal = useModalStore();
const settings = useSettingsStore();
const settings = useSettings();
useTracker();
handleShortcuts(useQStore, useModalStore);
handleShortcuts(useQueue, useModal);
router.afterEach(() => {
(document.getElementById("acontent") as HTMLElement).scrollTo(0, 0);
});
onStartTyping(() => {
if (isMobile.value) return;
const elem = document.getElementById("globalsearch") as HTMLInputElement;
elem.focus();
elem.value = "";
});
function getContentSize() {
const elem = document.getElementById("acontent") as HTMLElement;
return {
width: elem.offsetWidth,
height: elem.offsetHeight,
};
}
function updateContentElemSize({
width,
height,
@@ -90,8 +101,11 @@ function updateContentElemSize({
width: number;
height: number;
}) {
content_width.value = width;
// 1372 is the maxwidth of the #acontent. see app-grid.scss > $maxwidth
const elem_width = Math.min(1372, appcontent.value?.offsetWidth || 0);
content_width.value = elem_width;
content_height.value = height;
updateCardWidth();
}
function handleWelcomeModal() {
@@ -118,16 +132,28 @@ function handleRootDirsPrompt() {
}
onMounted(() => {
queue.startBufferingStatusWatcher();
const { width, height } = getContentSize();
updateContentElemSize({ width, height });
handleWelcomeModal();
settings.initializeVolume();
if (baseApiUrl.value === null) {
modal.showSetIPModal();
return;
}
handleRootDirsPrompt();
getAllSettings()
.then(({ settings: data }) => {
settings.mapDbSettings(data);
})
.then(() => {
if (settings.use_lyrics_plugin) return;
});
if (queue.currenttrack) {
lyrics.checkExists(
queue.currenttrack.filepath,
queue.currenttrack.trackhash
);
}
});
</script>

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.39687 27.1131C9.04913 27.1131 9.53405 26.7937 10.3459 26.0807L14.3572 22.5294H21.4504C24.9784 22.5294 26.9547 20.503 26.9547 17.0251V7.93679C26.9547 4.45891 24.9784 2.4325 21.4504 2.4325H6.50218C2.97625 2.4325 1 4.4493 1 7.93679V17.0251C1 20.5126 3.02476 22.5294 6.42836 22.5294H6.9107V25.4181C6.9107 26.4533 7.45422 27.1131 8.39687 27.1131ZM8.96593 24.532V21.1863C8.96593 20.4986 8.67132 20.2335 8.01319 20.2335H6.56757C4.35225 20.2335 3.29592 19.1066 3.29592 16.9523V8.00007C3.29592 5.84569 4.35225 4.72842 6.56757 4.72842H21.3871C23.5928 4.72842 24.6588 5.84569 24.6588 8.00007V16.9523C24.6588 19.1066 23.5928 20.2335 21.3871 20.2335H14.2321C13.521 20.2335 13.1823 20.3484 12.6864 20.8497L8.96593 24.532Z" fill="currentColor"/>
<path d="M8.2771 11.2555C8.2771 12.6189 9.13022 13.6523 10.4502 13.6523C10.9762 13.6523 11.4768 13.547 11.7878 13.1582H11.9174C11.5281 14.0908 10.6426 14.7372 9.78389 14.9491C9.37561 15.0578 9.23194 15.2495 9.23194 15.5329C9.23194 15.8427 9.4928 16.0804 9.83147 16.0804C11.0784 16.0804 13.4024 14.5982 13.4024 11.7334C13.4024 10.0933 12.3606 8.83142 10.776 8.83142C9.34444 8.83142 8.2771 9.8315 8.2771 11.2555ZM14.6327 11.2555C14.6327 12.6189 15.4837 13.6523 16.8058 13.6523C17.3317 13.6523 17.8302 13.547 18.1412 13.1582H18.273C17.8837 14.0908 16.9982 14.7372 16.1394 14.9491C15.7387 15.0578 15.5875 15.2495 15.5875 15.5329C15.5875 15.8427 15.8462 16.0804 16.1849 16.0804C17.4318 16.0804 19.7559 14.5982 19.7559 11.7334C19.7559 10.0933 18.7141 8.83142 17.1316 8.83142C15.6979 8.83142 14.6327 9.8315 14.6327 11.2555Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.39687 27.1131C9.04913 27.1131 9.53405 26.7937 10.3459 26.0807L14.3572 22.5294H21.4504C24.9784 22.5294 26.9547 20.503 26.9547 17.0251V7.93679C26.9547 4.45891 24.9784 2.4325 21.4504 2.4325H6.50218C2.97625 2.4325 1 4.4493 1 7.93679V17.0251C1 20.5126 3.02476 22.5294 6.42836 22.5294H6.9107V25.4181C6.9107 26.4533 7.45422 27.1131 8.39687 27.1131ZM8.96593 24.532V21.1863C8.96593 20.4986 8.67132 20.2335 8.01319 20.2335H6.56757C4.35225 20.2335 3.29592 19.1066 3.29592 16.9523V8.00007C3.29592 5.84569 4.35225 4.72842 6.56757 4.72842H21.3871C23.5928 4.72842 24.6588 5.84569 24.6588 8.00007V16.9523C24.6588 19.1066 23.5928 20.2335 21.3871 20.2335H14.2321C13.521 20.2335 13.1823 20.3484 12.6864 20.8497L8.96593 24.532Z" fill="currentColor"/>
<path d="M8.2771 11.2555C8.2771 12.6189 9.13022 13.6523 10.4502 13.6523C10.9762 13.6523 11.4768 13.547 11.7878 13.1582H11.9174C11.5281 14.0908 10.6426 14.7372 9.78389 14.9491C9.37561 15.0578 9.23194 15.2495 9.23194 15.5329C9.23194 15.8427 9.4928 16.0804 9.83147 16.0804C11.0784 16.0804 13.4024 14.5982 13.4024 11.7334C13.4024 10.0933 12.3606 8.83142 10.776 8.83142C9.34444 8.83142 8.2771 9.8315 8.2771 11.2555ZM14.6327 11.2555C14.6327 12.6189 15.4837 13.6523 16.8058 13.6523C17.3317 13.6523 17.8302 13.547 18.1412 13.1582H18.273C17.8837 14.0908 16.9982 14.7372 16.1394 14.9491C15.7387 15.0578 15.5875 15.2495 15.5875 15.5329C15.5875 15.8427 15.8462 16.0804 16.1849 16.0804C17.4318 16.0804 19.7559 14.5982 19.7559 11.7334C19.7559 10.0933 18.7141 8.83142 17.1316 8.83142C15.6979 8.83142 14.6327 9.8315 14.6327 11.2555Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,4 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.6328 35L8.625 10.3203H8.41406C8.74219 14.5234 8.90625 17.7891 8.90625 20.1172V35H0.703125V0.734375H13.0312L20.1797 25.0625H20.3672L27.375 0.734375H39.7266V35H31.2188V19.9766C31.2188 19.1953 31.2266 18.3281 31.2422 17.375C31.2734 16.4219 31.3828 14.0859 31.5703 10.3672H31.3594L24.4453 35H15.6328ZM76.2422 0.734375V21.3594C76.2422 25.8438 74.9688 29.3203 72.4219 31.7891C69.8906 34.2422 66.2344 35.4688 61.4531 35.4688C56.7812 35.4688 53.1875 34.2734 50.6719 31.8828C48.1719 29.4922 46.9219 26.0547 46.9219 21.5703V0.734375H56.2266V20.8438C56.2266 23.2656 56.6797 25.0234 57.5859 26.1172C58.4922 27.2109 59.8281 27.7578 61.5938 27.7578C63.4844 27.7578 64.8516 27.2188 65.6953 26.1406C66.5547 25.0469 66.9844 23.2656 66.9844 20.7969V0.734375H76.2422ZM105.844 24.5938C105.844 26.7188 105.305 28.6094 104.227 30.2656C103.148 31.9062 101.594 33.1875 99.5625 34.1094C97.5312 35.0156 95.1484 35.4688 92.4141 35.4688C90.1328 35.4688 88.2188 35.3125 86.6719 35C85.125 34.6719 83.5156 34.1094 81.8438 33.3125V25.0625C83.6094 25.9688 85.4453 26.6797 87.3516 27.1953C89.2578 27.6953 91.0078 27.9453 92.6016 27.9453C93.9766 27.9453 94.9844 27.7109 95.625 27.2422C96.2656 26.7578 96.5859 26.1406 96.5859 25.3906C96.5859 24.9219 96.4531 24.5156 96.1875 24.1719C95.9375 23.8125 95.5234 23.4531 94.9453 23.0938C94.3828 22.7344 92.8672 22 90.3984 20.8906C88.1641 19.875 86.4844 18.8906 85.3594 17.9375C84.25 16.9844 83.4219 15.8906 82.875 14.6562C82.3438 13.4219 82.0781 11.9609 82.0781 10.2734C82.0781 7.11719 83.2266 4.65625 85.5234 2.89062C87.8203 1.125 90.9766 0.242188 94.9922 0.242188C98.5391 0.242188 102.156 1.0625 105.844 2.70312L103.008 9.85156C99.8047 8.38281 97.0391 7.64844 94.7109 7.64844C93.5078 7.64844 92.6328 7.85938 92.0859 8.28125C91.5391 8.70312 91.2656 9.22656 91.2656 9.85156C91.2656 10.5234 91.6094 11.125 92.2969 11.6562C93 12.1875 94.8906 13.1562 97.9688 14.5625C100.922 15.8906 102.969 17.3203 104.109 18.8516C105.266 20.3672 105.844 22.2812 105.844 24.5938ZM111.141 35V0.734375H120.445V35H111.141ZM143.133 7.83594C140.93 7.83594 139.211 8.74219 137.977 10.5547C136.742 12.3516 136.125 14.8359 136.125 18.0078C136.125 24.6016 138.633 27.8984 143.648 27.8984C145.164 27.8984 146.633 27.6875 148.055 27.2656C149.477 26.8438 150.906 26.3359 152.344 25.7422V33.5703C149.484 34.8359 146.25 35.4688 142.641 35.4688C137.469 35.4688 133.5 33.9688 130.734 30.9688C127.984 27.9688 126.609 23.6328 126.609 17.9609C126.609 14.4141 127.273 11.2969 128.602 8.60938C129.945 5.92188 131.867 3.85938 134.367 2.42188C136.883 0.96875 139.836 0.242188 143.227 0.242188C146.93 0.242188 150.469 1.04688 153.844 2.65625L151.008 9.94531C149.742 9.35156 148.477 8.85156 147.211 8.44531C145.945 8.03906 144.586 7.83594 143.133 7.83594Z" fill="white"/>
<path d="M160.289 14.4453C159.008 12.5859 158.367 9.9375 158.367 6.5V3.10156H167.602L185.039 22.2031C186.023 23.2969 186.68 24.2188 187.008 24.9688C187.336 25.7188 187.578 26.4062 187.734 27.0312C188.078 28.2812 188.25 29.8203 188.25 31.6484V35H179.273L160.359 14.4453H160.289ZM178.102 13.1094C178.102 8.8125 178.766 6.01562 180.094 4.71875C180.797 4.03125 181.711 3.59375 182.836 3.40625C183.977 3.20312 185.359 3.10156 186.984 3.10156H188.25V6.80469C188.25 10.1328 187.375 12.3594 185.625 13.4844C184.375 14.2969 182.273 14.7031 179.32 14.7031H178.102V13.1094ZM158.367 31.3438C158.367 28.0156 159.242 25.7812 160.992 24.6406C162.242 23.8438 164.344 23.4453 167.297 23.4453H168.516V25.0391C168.516 29.3828 167.852 32.1719 166.523 33.4062C165.617 34.25 164.297 34.75 162.562 34.9062C161.688 34.9688 160.711 35 159.633 35H158.367V31.3438Z" fill="#4AD168"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -5,7 +5,6 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
padding: 0 1rem;
padding-bottom: 4rem;
overflow: auto;
max-height: 100%;
gap: 2rem 1rem;

View File

@@ -23,9 +23,15 @@ $g-border: solid 1px $gray5;
#acontent {
width: 100%;
grid-area: content;
padding-right: calc($medium);
overflow: hidden;
margin-right: $margright;
}
.content-page {
scrollbar-gutter: stable;
padding-left: $padleft;
padding-right: $padright;
padding-bottom: $padbottom;
}
.vue-recycle-scroller__item-wrapper {
@@ -33,18 +39,23 @@ $g-border: solid 1px $gray5;
}
.vue-recycle-scroller {
padding-left: 1.25rem;
scrollbar-gutter: stable;
padding-left: $padleft;
}
.r-sidebar {
grid-area: r-sidebar;
border-left: $g-border;
.vue-recycle-scroller {
padding-left: 0;
}
}
.topnav {
grid-area: nav;
margin: 1rem 0;
padding: 1rem $padleft;
padding-right: $padright;
}
.b-bar {
@@ -52,13 +63,6 @@ $g-border: solid 1px $gray5;
border-top: $g-border;
}
.content-page {
margin-left: 1.25rem;
margin-right: -$medium;
padding-right: $medium;
scrollbar-gutter: stable;
}
// ====== MODIFIERS =======
#app-grid.extendWidth {
@@ -77,13 +81,7 @@ $g-border: solid 1px $gray5;
#acontent {
margin-right: 0 !important;
padding-right: $medium !important;
}
.topnav {
//reduce width to match #acontent
width: calc(100% - 1rem);
padding-right: 0;
// padding-right: $medium !important;
}
@include allPhones {
@@ -102,19 +100,18 @@ $g-border: solid 1px $gray5;
}
.v-scroll-page {
width: calc(100% + $medium) !important;
.scroller {
padding-right: $padright;
height: 100%;
width: 100%;
padding-right: 1.25rem;
padding-bottom: $content-padding-bottom;
padding-bottom: $padbottom;
}
}
.isSmall {
.songlist-item {
grid-template-columns: 2fr 5.5rem;
grid-template-columns: 2fr 5.5rem !important;
}
.song-artists,

View File

@@ -140,7 +140,6 @@ button {
opacity: 0.5;
}
// NO THIS, NO THAT (OVERRIDES)
.no-border {
border: none;
}
@@ -170,3 +169,75 @@ button {
position: absolute;
left: -20rem;
}
.spinner {
border: solid 3px rgb(0, 0, 0);
border-top: solid 3px transparent;
border-left: solid 3px transparent;
border-radius: 50%;
width: 1.25rem;
height: 1.25rem;
animation: spin 0.45s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.card-list-scroll-x {
overflow: hidden;
h3 {
display: grid;
grid-template-columns: 1fr max-content;
align-items: baseline;
padding: 0 $medium;
margin-bottom: $medium;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
overflow-x: auto;
scroll-snap-type: x mandatory;
flex-direction: row;
padding-bottom: 2rem;
@include hideScrollbars;
}
.album-card {
&:hover {
background-color: $gray;
}
}
}
.rhelp {
text-transform: uppercase;
font-size: 11px;
color: $purple;
font-weight: bold;
margin: $smaller 0;
&.album {
color: $orange;
}
&.track {
color: $pink;
}
&.folder {
color: $teal;
}
&.playlist {
color: $green;
}
}

View File

@@ -37,7 +37,7 @@
}
@mixin tablet-portrait {
@media (max-width: 810) {
@media (max-width: 810px) {
@content;
}
}

View File

@@ -26,14 +26,14 @@ $gray2: rgb(99, 99, 102);
$gray3: rgb(72, 72, 74);
$gray4: rgb(58, 58, 60);
$gray5: rgb(44, 44, 46);
$body: rgba(0, 0, 0, 0.95);
$body: #111111;
$red: #ff453a;
$blue: #0a84ff;
$darkblue: #055ee2;
$green: rgb(20, 160, 55);
$green: rgb(94, 247, 132);
$yellow: rgb(255, 214, 10);
$orange: rgb(255, 159, 10);
$orange: #ff9f0a;
$pink: rgb(255, 55, 95);
$purple: #bf5af2;
$brown: rgb(172, 142, 104);
@@ -55,3 +55,16 @@ $side-nav-svg: $red;
$overshoot: cubic-bezier(0.68, -0.55, 0.265, 1.55);
$separator: $gray4;
$maxwidth: 1372px;
$maxpadleft: 5rem;
$maxpadright: calc(100% - $maxwidth);
$padbottom: 4rem;
$padleft: clamp(2rem, $maxpadright, $maxpadleft);
$padright: clamp(
2rem,
max($maxpadright, 5rem),
calc($maxpadright + $maxpadleft)
);
$margright: calc(0rem - $padright);

View File

@@ -3,8 +3,7 @@
"./variables",
"./ProgressBar.scss",
"./BottomBar/BottomBar.scss",
"./Global",
"./moz.scss"
"./Global"
;

View File

@@ -1,40 +0,0 @@
// Styles that only apply to our dear Firefox
@-moz-document url-prefix() {
#acontent {
margin-right: calc(-1rem + 1px);
padding-right: 1rem;
}
// applies to playlist list page
.content-page {
margin-right: calc(0rem - ($medium + 4px));
}
// virtual scroller pages: folder, playlist, album
.header-list-layout {
margin-right: calc(0rem - ($medium + 4px)) !important;
.scrollable {
padding-right: calc(1rem - 3px) !important;
}
}
.v-scroll-page {
width: calc(100% + 1rem) !important;
.scroller {
height: 100%;
width: 100%;
// padding-right: 1.25rem !important;
}
}
#app-grid.noSidebar > #acontent {
padding-right: 1rem !important;
}
.search-view {
margin-right: -1rem !important;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div
class="album_disc_header no-select"
v-if="album_disc.is_album_disc_number"
class="album_disc_header no-select"
>
<div class="disc_number">Disc {{ album_disc.album_page_disc_number }}</div>
<div></div>

View File

@@ -1,78 +0,0 @@
<template>
<div class="card-list-scroll-x">
<h3>
<span>{{ title }}</span>
<SeeAll
v-if="route && maxAbumCards - 1 <= albums.length"
:route="route"
@click="
!favorites ? useArtistDiscographyStore().setPage(albumType) : null
"
/>
</h3>
<div ref="artistItemsWrappers" class="cards">
<AlbumCard
v-for="a in albums"
:key="a.albumhash"
:album="a"
:show_date="show_date"
:artist_page="artist_page"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Album } from "@/interfaces";
import { discographyAlbumTypes } from "@/enums";
import { maxAbumCards } from "@/stores/content-width";
import useArtistDiscographyStore from "@/stores/pages/artistDiscog";
import AlbumCard from "../shared/AlbumCard.vue";
import SeeAll from "../shared/SeeAll.vue";
defineProps<{
title: string;
albums: Album[];
albumType?: discographyAlbumTypes;
favorites?: boolean;
route?: string;
show_date?: boolean;
artist_page?: boolean;
}>();
const artistItemsWrappers = ref<HTMLElement | null>(null);
</script>
<style lang="scss">
.card-list-scroll-x {
overflow: hidden;
h3 {
display: grid;
grid-template-columns: 1fr max-content;
align-items: baseline;
padding: 0 $medium;
margin-bottom: $medium;
}
.cards {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
scroll-snap-type: x mandatory;
flex-direction: row;
padding-bottom: 2rem;
@include hideScrollbars;
}
.album-card {
&:hover {
background-color: $gray;
}
}
}
</style>

View File

@@ -4,8 +4,8 @@
<HeartSvg
:state="album.is_favorite"
@handleFav="handleFav"
:color="colors.bg ? colors.bg : ''"
@handleFav="handleFav"
/>
<button
class="options"

View File

@@ -16,12 +16,13 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import { Album } from "@/interfaces";
import { formatSeconds } from "@/utils";
import { isSmallPhone } from "@/stores/content-width";
import ArtistName from "@/components/shared/ArtistName.vue";
import { computed } from "vue";
const props = defineProps<{
album: Album;
@@ -33,7 +34,9 @@ const statsText = computed(() => {
// hide track count if it's a single, also add an s to track if it's plural
return `${props.album.date} ${
!is_single
? `${props.album.count} Track${props.album.count > 1 ? "s" : ""}`
? `${props.album.count.toLocaleString()} Track${
props.album.count > 1 ? "s" : ""
}`
: ""
}${formatSeconds(props.album.duration, true)}`;
});

View File

@@ -24,8 +24,8 @@
</template>
<script setup lang="ts">
import { storeToRefs } from "pinia";
import { ref } from "vue";
import { storeToRefs } from "pinia";
import { paths } from "@/config";
import { isHeaderSmall } from "@/stores/content-width";

View File

@@ -1,21 +1,35 @@
<template>
<div style="height: 1px"></div>
<div style="height: 1px">
<button v-if="show_text" @click="fetch_callback">Load More</button>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { nextTick, onMounted } from "vue";
import { onBeforeRouteUpdate } from "vue-router";
import { updateCardWidth } from "@/stores/content-width";
const props = defineProps<{
fetch_callback: () => void;
reset_callback: () => void;
show_text?: boolean;
fetch_callback: () => Promise<void>;
reset_callback?: () => Promise<void>;
outside_route?: boolean;
}>();
const update = async () => {
await nextTick();
updateCardWidth();
};
onMounted(async () => {
props.fetch_callback();
props.fetch_callback().then(update);
});
onBeforeRouteUpdate(() => {
props.reset_callback();
});
!props.outside_route &&
onBeforeRouteUpdate(() => {
if (!props.reset_callback) return;
props.reset_callback().then(update);
});
</script>

View File

@@ -19,13 +19,13 @@
</div>
<div class="stats">
<span v-if="artist.trackcount">
{{ artist.trackcount }} Track{{
{{ artist.trackcount.toLocaleString() }} Track{{
`${artist.trackcount == 1 ? "" : "s"}`
}}
</span>
{{ artist.albumcount && artist.trackcount ? "" : "" }}
{{ artist.albumcount && artist.trackcount.toLocaleString() ? "" : "" }}
<span v-if="artist.albumcount">
{{ artist.albumcount }} Album{{
{{ artist.albumcount.toLocaleString() }} Album{{
`${artist.albumcount == 1 ? "" : "s"}`
}}
</span>

View File

@@ -9,7 +9,7 @@
v-for="(song, index) in tracks"
:key="index"
:track="song"
:index="index + 1"
:index="total ? total - index : index + 1"
:source="source"
@playThis="playHandler(index)"
/>
@@ -31,6 +31,7 @@ defineProps<{
title: string;
playHandler: (index: number) => void;
source: dropSources;
total?: number;
}>();
</script>

View File

@@ -1,126 +0,0 @@
<template>
<div class="artists-view rounded">
<div class="al-header">
<div class="heading">ALL ARTISTS</div>
<div class="search">
<input type="search" placeholder="Search artists" />
</div>
</div>
<div class="all-albums">
<div class="item rounded" v-for="artist in artists" :key="artist">
<div class="album-art image rounded"></div>
<div class="name t-center ellip">{{ artist.name }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
setup() {
const artists = [
{
name: "Juice Wrld",
},
{
name: "Eminem",
},
{
name: "Sting",
},
{
name: "Juice Wrld",
},
{
name: "Eminem",
},
{
name: "Sting",
},
{
name: "Juice Wrld",
},
];
return {
artists,
};
},
};
</script>
<style lang="scss">
.artists-view {
height: calc(100% - 14.5rem);
padding: $small;
margin-top: $small;
overflow: hidden;
.all-albums {
height: calc(100% - 4rem);
border-top: 1px solid $separator;
padding: $small 0 0 0;
overflow-y: auto;
.item {
position: relative;
width: 10.9rem;
height: 12rem;
padding: $small 0.95rem $small 0.95rem;
margin: $smaller;
transition: all 0.2s ease-in-out;
cursor: default;
float: left;
&:hover {
background-color: rgb(74, 84, 87);
// border: solid
}
.album-art {
height: 9em;
width: 9em;
border-radius: 50%;
background-image: url(../../assets/images/null.webp);
}
.name {
margin-top: $small;
}
}
}
.al-header {
display: flex;
align-items: center;
position: relative;
height: 4em;
padding: 0 $small 0 $small;
.search {
position: absolute;
right: 0;
padding-right: $small;
input {
width: 30rem;
border: none;
border-radius: 2rem;
padding-left: 1rem;
background-color: #4645456c;
color: rgba(255, 255, 255, 0.521);
font-size: 1rem;
line-height: 3rem;
outline: none;
}
input::-webkit-search-cancel-button {
position: relative;
right: 20px;
cursor: default;
width: 50px;
height: 50px;
}
}
}
}
</style>

View File

@@ -1,160 +0,0 @@
<template>
<div class="top-artists">
<div class="heading">TOP ARTISTS</div>
<div class="items">
<div class="item rounded" v-for="artist in artists" :key="artist">
<div class="image"></div>
<div class="info">
<div class="name ellip">{{ artist.name }}</div>
<div class="artist ellip">{{ artist.album_count }} Albums</div>
<div class="separator"></div>
<div class="top">
<div class="play-icon"></div>
<div class="text ellip">{{ artist.top_track }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script >
export default {
setup() {
const artists = [
{
name: "Sting",
album_count: "12",
top_track: "Alien in Newyork",
},
{
name: "Juice Wrld",
album_count: "4",
top_track: "Girl Of My Dreams",
},
{
name: "Lil Peep",
album_count: "6",
top_track: "Haunt U",
},
];
return {
artists,
};
},
};
</script>
<style lang="scss">
.top-artists {
height: 14rem;
border-radius: $small;
padding: $small;
.heading {
margin: $small 0 1.5em $small;
}
.items {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: $small;
.item {
height: 10rem;
width: 100%;
max-width: 25rem;
background-color: rgb(7, 6, 6);
display: grid;
align-items: center;
grid-template-columns: 7.5rem 1fr;
padding: $small;
cursor: default;
transition: all 0.2s ease-in-out;
&:hover {
transform: translateY(-.5em);
}
.image {
height: 7rem;
width: 7rem;
// background-image: url("../../assets/images/null.webp");
border-radius: 50%;
}
.info .name {
font-size: 1.5rem;
font-weight: bold;
}
.info .artist {
font-size: small;
color: rgba(255, 255, 255, 0.699);
}
.info .top {
height: 2.5rem;
background-color: rgb(51, 129, 20);
border-radius: $small;
margin-left: auto;
display: grid;
grid-template-columns: 2.5rem 1fr;
align-items: center;
transition: all 0.2s ease-in-out;
user-select: none;
.play-icon {
margin: 0 0 0 $small;
height: 1.2rem;
width: 1.2rem;
background-image: url(../../assets/icons/play.svg);
background-size: 96%;
background-repeat: no-repeat;
background-position: center;
transition: all 0.2s ease-in-out;
}
.text {
color: white;
}
&:hover {
background-color: rgb(0, 134, 89);
transition: all 0.2s ease-in-out;
.play-icon {
transform: scale(1.2);
transition: all 0.2s ease-in-out;
}
}
&:active {
transform: scale(0.95);
transition: all 0.1s ease-in-out;
}
}
&:first-child {
background-color: rgb(177, 116, 2);
.image {
background-image: url("../../assets/images/null.webp");
}
}
&:nth-child(2){
background-color: rgb(0, 74, 117);
.image {
background-image: url("../../assets/images/null.webp");
}
}
&:nth-child(3){
background-color: rgb(161, 106, 106);
}
}
}
}
</style>

View File

@@ -10,9 +10,10 @@
title="Go to Now Playing"
:to="{
name: Routes.nowPlaying,
query: {
tab: 'queue',
params: {
tab: 'home',
},
replace: true,
}"
>
<img

View File

@@ -1,5 +1,6 @@
<template>
<div class="right-group">
<LyricsButton v-if="settings.use_lyrics_plugin || lyrics.exists" />
<Volume />
<button
class="repeat"
@@ -16,8 +17,12 @@
<RepeatOneSvg v-if="settings.repeat_one" />
<RepeatAllSvg v-else />
</button>
<button title="Shuffle" @click="queue.shuffleQueue">
<ShuffleSvg />
</button>
<HeartSvg
v-if="!hideHeart"
title="Favorite"
:state="queue.currenttrack?.is_favorite"
@handleFav="() => $emit('handleFav')"
/>
@@ -25,16 +30,20 @@
</template>
<script setup lang="ts">
import useQStore from "@/stores/queue";
import useSettingsStore from "@/stores/settings";
import useQueue from "@/stores/queue";
import useSettings from "@/stores/settings";
import useLyrics from "@/stores/lyrics";
import Volume from "./Volume.vue";
import HeartSvg from "../shared/HeartSvg.vue";
import LyricsButton from "../shared/LyricsButton.vue";
import RepeatAllSvg from "@/assets/icons/repeat.svg";
import RepeatOneSvg from "@/assets/icons/repeat-one.svg";
import Volume from "./Volume.vue";
import ShuffleSvg from "@/assets/icons/shuffle.svg";
const queue = useQStore();
const settings = useSettingsStore();
const queue = useQueue();
const lyrics = useLyrics();
const settings = useSettings();
defineProps<{
hideHeart?: boolean;
@@ -49,7 +58,7 @@ defineEmits<{
.right-group {
display: grid;
justify-content: flex-end;
grid-template-columns: repeat(3, max-content);
grid-template-columns: repeat(5, max-content);
align-items: center;
height: 4rem;
@@ -69,9 +78,8 @@ defineEmits<{
}
}
button.repeat {
background-color: transparent;
.lyrics,
.repeat {
svg {
transform: scale(0.75);
}

View File

@@ -178,7 +178,8 @@ function runChildAction(action: () => void) {
}
}
&:nth-child(2) .icon > svg {
// add to queue icon
&:nth-child(3) .icon > svg {
transform: scale(0.9);
}

View File

@@ -1,26 +0,0 @@
<template>
<div class="card-list-scroll-x">
<h3>Recent</h3>
<div ref="recentitemswrappers" class="cards">
<Recentsitemcard
v-for="fav in favs.slice(0, maxAbumCards)"
:key="JSON.stringify(fav)"
:fav="fav"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import Recentsitemcard from "@/components/Favorites/RecentsItemCard.vue";
import { RecentFavResult } from "@/interfaces";
import { maxAbumCards } from "@/stores/content-width";
defineProps<{
favs: RecentFavResult[];
}>();
const recentitemswrappers = ref<HTMLElement | null>(null);
</script>

View File

@@ -1,9 +1,9 @@
<template>
<div class="breadcrumb-nav">
<div
class="path"
v-for="path in subPaths"
:key="path.path"
class="path"
:class="{ inthisfolder: path.active }"
@click.prevent="$emit('navigate', path.path)"
>

View File

@@ -1,26 +1,26 @@
<template>
<router-link :to="{ name: Routes.folder, params: { path: folder.path } }">
<div
v-auto-animate
class="f-item"
@click="(e) => (folder_page ? null : handleClick(e))"
@mouseover="mouse_over = true"
@mouseleave="mouse_over = false"
:style="{
backgroundColor: is_checked ? '#234ece' : '',
}"
:class="{ context_menu_showing }"
@click="(e) => (folder_page ? null : handleClick(e))"
@mouseover="mouse_over = true"
@mouseleave="mouse_over = false"
@contextmenu.prevent="(e) => (!folder_page ? null : showContextMenu(e))"
v-auto-animate
>
<SymLinkSvg v-if="folder.is_sym" />
<FolderSvg v-else />
<div class="info">
<div class="f-item-text ellip">{{ folder.name }}</div>
<div class="f-count" v-if="folder.count">
<div v-if="folder.count" class="f-count">
{{ folder.count + ` File${folder.count == 1 ? "" : "s"}` }}
</div>
</div>
<div class="check" v-if="!folder_page">
<div v-if="!folder_page" class="check">
<CheckSvg v-if="!is_checked && mouse_over" />
<CheckFilledSvg v-if="is_checked" />
</div>

View File

@@ -1,3 +0,0 @@
<template>
<h1>This is your Homepage</h1>
</template>

View File

@@ -0,0 +1,68 @@
<template>
<div class="homebrowse">
<div class="btitle"><b>Browse Library</b></div>
<div class="browselist">
<RouterLink
v-for="i in browselist"
:key="i.title"
class="browseitem rounded-sm t-center"
:to="{ name: i.route, params: i.params }"
:style="{ width: `${album_card_with - 24}px` }"
>
{{ i.title }}
</RouterLink>
</div>
</div>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import { album_card_with } from "@/stores/content-width";
const browselist = [
{
title: "Folders",
route: Routes.folder,
params: {
path: "$home",
},
},
{
title: "Albums",
route: Routes.AlbumList,
},
{
title: "Artists",
route: Routes.ArtistList,
},
];
</script>
<style lang="scss">
.homebrowse {
padding: 1.5rem 0;
padding-left: $small;
.btitle {
font-size: 1.15rem;
margin-bottom: 1rem;
}
.browselist {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
margin-top: $small;
}
.browseitem {
padding: 1.5rem 0;
background-color: $gray;
color: $white;
}
.browseitem:hover {
background-color: $gray5;
}
}
</style>

View File

@@ -17,10 +17,10 @@
}"
></div>
<div
v-else
id="home-button"
class="nav-button"
:class="{ active: $route.name === menu.route_name }"
id="home-button"
v-else
>
<div class="in">
<component :is="menu.icon"></component>

View File

@@ -1,7 +1,7 @@
<template>
<div
class="bitrate"
v-if="q.currenttrack?.bitrate"
class="bitrate"
title="file type • bitrate"
>
{{ q.currenttrack.filepath?.split('.').pop() }} {{ q.currenttrack.bitrate }}

View File

@@ -1,31 +1,33 @@
<template>
<div class="hotkeys no-scroll">
<button @click.prevent="q.playPrev">
<button @click.prevent="queue.playPrev">
<PrevSvg />
</button>
<button @click.prevent="q.playPause">
<Spinner v-if="q.buffering && q.playing" />
<PauseSvg v-else-if="q.playing" />
<PlaySvg v-else/>
<button @click.prevent="queue.playPause">
<Spinner v-if="buffering && queue.playing" />
<PauseSvg v-else-if="queue.playing" />
<PlaySvg v-else />
</button>
<button @click.prevent="q.playNext">
<button @click.prevent="queue.playNext">
<NextSvg />
</button>
</div>
</template>
<script setup lang="ts">
import { usePlayer } from "@/stores/player";
import useQStore from "@/stores/queue";
import {
default as NextSvg,
default as PrevSvg,
default as NextSvg,
default as PrevSvg,
} from "@/assets/icons/next.svg";
import PauseSvg from "@/assets/icons/pause.svg";
import PlaySvg from "@/assets/icons/play.svg";
import Spinner from "@/components/shared/Spinner.vue";
const q = useQStore();
const queue = useQStore();
const { buffering } = usePlayer();
</script>
<style lang="scss">
@@ -68,4 +70,4 @@ const q = useQStore();
}
}
}
</style>
</style>

View File

@@ -13,6 +13,9 @@
<router-link
:to="{
name: Routes.nowPlaying,
params: {
tab: 'home',
},
}"
>
<img

View File

@@ -1,9 +1,7 @@
<template>
<div class="side-nav-container">
<router-link
v-for="(menu, index) in menus.filter((i) =>
i.unlock_key ? i.unlock_key() : true
)"
v-for="(menu, index) in menus"
:key="index"
v-wave
:to="{
@@ -18,7 +16,7 @@
}"
>
<div v-if="!menu.separator">
<component :is="menu.icon" :class="menu.class" />
<component :is="menu.icon" />
<span>{{ menu.name }}</span>
</div>
</router-link>
@@ -86,7 +84,7 @@ import { menus } from "./navitems";
transform: scale(0.9);
opacity: 0.75;
}
svg.radiosvg {
transform: scale(0.7);
}

View File

@@ -6,8 +6,14 @@ import HeartSvg from "@/assets/icons/heart.svg";
import PlaylistSvg from "@/assets/icons/playlist-1.svg";
import SearchSvg from "@/assets/icons/search.svg";
import SettingsSvg from "@/assets/icons/settings.svg";
import HomeSvg from "@/assets/icons/home.svg";
export const menus = [
{
name: "home",
route_name: Routes.Home,
icon: HomeSvg,
},
{
name: "folders",
route_name: Routes.folder,
@@ -40,6 +46,7 @@ export const menus = [
{
name: "settings",
route_name: Routes.settings,
params: { tab: "general" },
icon: SettingsSvg,
},
];

View File

@@ -7,7 +7,7 @@
:class="notif.type"
>
<component :is="getSvg(notif.type)" class="notif-icon" />
<div class="notif-text ellip">{{ notif.text }}</div>
<div class="notif-text">{{ notif.text }}</div>
</div>
</div>
</template>

View File

@@ -19,7 +19,7 @@
/>
</RouterLink>
<NowPlayingInfo @handle-fav="handleFav" />
<Progress v-if="isSmallPhone"/>
<Progress v-if="isSmallPhone" />
<div v-if="isSmallPhone" class="below-progress">
<div class="time">
{{ formatSeconds(queue.duration.current) }}
@@ -30,17 +30,13 @@
</div>
</div>
</div>
<h3 v-if="queue.currenttrack">Now Playing</h3>
<h3 v-if="queue.next">Up Next</h3>
<SongItem
v-if="queue.currenttrack"
:track="queue.currenttrack"
:index="queue.currentindex + 1"
v-if="queue.next"
:track="queue.next"
:index="queue.nextindex + 1"
:source="dropSources.folder"
:style="{
backgroundColor: colors.theme1,
color: getTextColor(colors.theme1),
}"
@play-this="() => {}"
@play-this="queue.playNext"
/>
</div>
</template>
@@ -49,21 +45,18 @@
import { paths } from "@/config";
import { Routes } from "@/router";
import useQueueStore from "@/stores/queue";
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
import { formatSeconds } from "@/utils";
import { dropSources, favType } from "@/enums";
import { isSmallPhone } from "@/stores/content-width";
import favoriteHandler from "@/helpers/favoriteHandler";
import useColorStore from "@/stores/colors";
import { getTextColor } from "@/utils/colortools/shift";
import SongItem from "../shared/SongItem.vue";
import NowPlayingInfo from "./NowPlayingInfo.vue";
import PlayingFrom from "./PlayingFrom.vue";
import Buttons from "../BottomBar/Right.vue";
import { isSmallPhone } from "@/stores/content-width";
import { formatSeconds } from "@/utils";
import SongItem from "../shared/SongItem.vue";
import NowPlayingInfo from "./NowPlayingInfo.vue";
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
const queue = useQueueStore();
const colors = useColorStore();
function handleFav() {
favoriteHandler(

View File

@@ -26,22 +26,19 @@
</div>
</div>
</router-link>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { computed } from "vue";
import { RouteLocationRaw } from "vue-router";
import useTracklist from "@/stores/queue/tracklist";
import { FromOptions } from "@/enums";
import useQueueStore from "@/stores/queue";
import playingFrom from "@/utils/playingFrom";
const queue = useQueueStore();
const queue = useTracklist();
const data = computed(() => {
const { name, location, icon, image } = playingFrom(queue.from);
@@ -55,8 +52,6 @@ const data = computed(() => {
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.now-playling-from-link {

View File

@@ -1,30 +0,0 @@
<template>
<div class="card-list-scroll-x">
<h3>{{ title }} <SeeAll v-if="route" :route="route" /></h3>
<div ref="artistItemswrappers" class="cards">
<ArtistCard
v-for="artist in artists.slice(0, maxAbumCards)"
:key="artist.image"
:artist="artist"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Artist } from "@/interfaces";
import { maxAbumCards } from "@/stores/content-width";
import ArtistCard from "@/components/shared/ArtistCard.vue";
import SeeAll from "../shared/SeeAll.vue";
defineProps<{
artists: Artist[];
title: string;
route?: string;
}>();
const artistItemswrappers = ref<HTMLElement | null>(null);
</script>

View File

@@ -11,6 +11,7 @@
:class="{ 'use-sqr_img': useSqrImg }"
>
<div
v-if="Number.isInteger(info.id)"
class="float"
:style="{
color: textColor,

View File

@@ -10,7 +10,7 @@
</div>
<div class="duration">
{{
playlist.info.count +
playlist.info.count.toLocaleString() +
` ${playlist.info.count == 1 ? "Track" : "Tracks"}`
}}

View File

@@ -1,10 +1,16 @@
<template>
<div class="last-updated">
<span class="status" v-if="!isHeaderSmall"
<span v-if="!isHeaderSmall" class="status"
>Last updated {{ playlist.info.last_updated }} &#160;|&#160;&#160;</span
>
<div class="edit" @click="editPlaylist">Edit&#160;&#160;</div>
|
<div
v-if="Number.isInteger(playlist.info.id)"
class="edit"
@click="editPlaylist"
>
Edit&#160;&#160;
</div>
{{ Number.isInteger(playlist.info.id) ? " | " : "" }}
<DeleteSvg class="edit" @click="deletePlaylist" />
</div>
</template>
@@ -34,11 +40,8 @@ function deletePlaylist() {
bottom: 1rem;
right: 1rem;
padding: $smaller $small;
// background-color: $body;
// color: rgb(255, 255, 255);
font-size: 0.9rem;
border-radius: $smaller;
// box-shadow: 0 0 1rem rgba(0, 0, 0, 0.479);
z-index: 12;
display: flex;

View File

@@ -20,9 +20,13 @@
:class="{ border: !playlist.thumb }"
/>
<div class="overlay rounded">
<div v-if="playlist.help_text" class="rhelp playlist">{{ playlist.help_text }}</div>
<div class="p-name ellip">{{ playlist.name }}</div>
<div class="p-count">
{{ playlist.count + ` ${playlist.count === 1 ? "Track" : "Tracks"}` }}
<b>{{
playlist.count.toLocaleString() +
` Track${playlist.count === 1 ? "" : "s"}`
}}</b>
</div>
</div>
</router-link>
@@ -33,8 +37,7 @@ import { paths } from "../../config";
import { Playlist } from "../../interfaces";
const imguri = paths.images.playlist;
const props = defineProps<{
defineProps<{
playlist: Playlist;
}>();
</script>
@@ -44,9 +47,10 @@ const props = defineProps<{
background-color: #2c2c2e45;
display: grid;
grid-template-rows: 1fr max-content;
padding: 1rem;
padding: $medium;
gap: $small;
user-select: none;
height: max-content;
.image-grid {
display: grid;
@@ -55,7 +59,7 @@ const props = defineProps<{
&:hover {
transition: all 0.25s ease;
background-color: $gray3;
background-color: $gray4 !important;
background-blend-mode: screen;
}
@@ -65,6 +69,7 @@ const props = defineProps<{
object-fit: cover;
transition: all 0.5s ease;
}
.overlay {
display: flex;
flex-direction: column;
@@ -74,25 +79,7 @@ const props = defineProps<{
.p-count {
opacity: 0.75;
font-size: 0.75rem;
}
}
&:hover {
.p-name {
text-decoration: underline;
}
}
.bottom {
margin-top: $smaller;
.name {
font-weight: 900;
}
.count {
font-size: $medium;
opacity: 0.5;
margin-top: $smaller;
}
}
}

View File

@@ -1,18 +1,18 @@
<template>
<div class="r-home">
<UpNext :track="queue.tracklist[queue.next]" :playNext="queue.playNext" />
<UpNext :track="queue.tracklist[queue.next]" :play-next="queue.playNext" />
<!-- <Recommendations /> -->
</div>
</template>
<style lang="scss">
.r-home {
height: calc(100% - 1rem);
}
</style>
<script setup lang="ts">
import useQStore from "../../../stores/queue";
import UpNext from "../Queue/upNext.vue";
const queue = useQStore();
</script>
<style lang="scss">
.r-home {
height: calc(100% - 1rem);
}
</style>

View File

@@ -2,7 +2,7 @@
<div class="r-tracks rounded bg-primary">
<div class="heading">Similar tracks</div>
<div class="tracks">
<div class="song-item" v-for="song in songs" :key="song.artist">
<div v-for="song in songs" :key="song.artist" class="song-item">
<img src="" class="rounded" loading="lazy"/>
<div class="tags">
<div class="title">{{ song.title }}</div>

View File

@@ -1,14 +1,14 @@
<template>
<div class="r-sidebar">
<SearchInput />
<div v-auto-animate class="r-content no-scroll" >
<div class="r-dash" v-if="tabs.current === tabs.tabs.home">
<div v-auto-animate class="r-content no-scroll">
<div v-if="tabs.current === tabs.tabs.home" class="r-dash">
<DashBoard />
</div>
<div class="r-search" v-if="tabs.current === tabs.tabs.search">
<div v-if="tabs.current === tabs.tabs.search" class="r-search">
<Search />
</div>
<div class="r-queue" v-if="tabs.current === tabs.tabs.queue">
<div v-if="tabs.current === tabs.tabs.queue" class="r-queue">
<Queue />
</div>
</div>
@@ -16,10 +16,11 @@
</template>
<script setup lang="ts">
import useTabStore from "../../stores/tabs";
import DashBoard from "./Home/Main.vue";
import useTabStore from "@/stores/tabs";
import Queue from "./Queue.vue";
import Search from "./Search/Main.vue";
import DashBoard from "./Home/Main.vue";
import SearchInput from "./SearchInput.vue";
const tabs = useTabStore();

View File

@@ -6,7 +6,7 @@
@mouseout="mouseover = false"
>
<NoItems
:flag="!queue.tracklist.length"
:flag="!store.tracklist.length"
:title="'No songs in queue'"
:description="'When you start playing songs, they will appear here.'"
:icon="QueueSvg"
@@ -36,18 +36,23 @@
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import useQStore from "@/stores/queue";
import useInterface from "@/stores/interface";
import useTracklist from "@/stores/queue/tracklist";
import TrackItem from "@/components/shared/TrackItem.vue";
import QueueActions from "./Queue/QueueActions.vue";
import NoItems from "../shared/NoItems.vue";
import QueueActions from "./Queue/QueueActions.vue";
import TrackItem from "@/components/shared/TrackItem.vue";
import QueueSvg from "@/assets/icons/queue.svg";
const itemHeight = 64;
const queue = useQStore();
const store = useTracklist();
const mouseover = ref(false);
const { focusCurrentInSidebar, setScrollFunction } = useInterface();
const scrollerItems = computed(() => {
return queue.tracklist.map((track, index) => ({
return store.tracklist.map((track, index) => ({
track,
id: index,
}));
@@ -70,12 +75,12 @@ function scrollToCurrent() {
}
onMounted(() => {
queue.setScrollFunction(scrollToCurrent, mouseover);
queue.focusCurrentInSidebar();
setScrollFunction(scrollToCurrent, mouseover);
focusCurrentInSidebar();
});
onBeforeUnmount(() => {
queue.setScrollFunction(() => {}, null);
setScrollFunction(() => {}, null);
});
</script>
@@ -83,9 +88,5 @@ onBeforeUnmount(() => {
.queue-virtual-scroller {
height: 100%;
overflow: hidden;
.vue-recycle-scroller {
padding: 0 !important;
}
}
</style>

View File

@@ -1,13 +1,20 @@
<template>
<div class="queue-actions">
<div class="left">
<button v-wave class="shuffle-queue action" @click="queue.shuffleQueue">
<button
v-if="!onNowPlaying"
v-wave
class="shuffle-queue action"
@click="queue.shuffleQueue"
>
<ShuffleSvg />
<span>Shuffle</span>
</button>
<h2 v-else style="margin: 0">Now Playing</h2>
</div>
<div class="right">
<button
class="menu"
:class="{ 'btn-active': context_showing }"
@click="showContextMenu"
>
@@ -19,20 +26,28 @@
<script setup lang="ts">
import { ref } from "vue";
import useQueueStore from "@/stores/queue";
import useQueue from "@/stores/queue";
import useTracklist from "@/stores/queue/tracklist";
import { showQueueContextMenu } from "@/helpers/contextMenuHandler";
import OptionsSvg from "@/assets/icons/more.svg";
import ShuffleSvg from "@/assets/icons/shuffle.svg";
import { showQueueContextMenu } from "@/helpers/contextMenuHandler";
const queue = useQueueStore();
const queue = useQueue();
const { tracklist } = useTracklist();
const context_showing = ref(false);
function showContextMenu(e: MouseEvent) {
if (!queue.tracklist.length) return;
if (!tracklist.length) return;
showQueueContextMenu(e, context_showing);
}
defineProps<{
onNowPlaying?: boolean;
}>();
</script>
<style lang="scss">
@@ -43,8 +58,23 @@ function showContextMenu(e: MouseEvent) {
margin: 1rem;
margin-bottom: 0;
.lyricsversion {
display: flex;
gap: 1rem;
.save {
background-color: transparent;
}
// hide on screens less than 600px
@media screen and (max-width: 600px) {
display: none;
}
}
.left {
display: flex;
align-items: center;
gap: $small;
}
@@ -56,11 +86,16 @@ function showContextMenu(e: MouseEvent) {
}
}
.right > button {
padding: 0 $smaller;
.right {
display: flex;
gap: $medium;
svg {
transform: scale(1.2) rotate(90deg);
.menu {
padding: 0 $smaller;
svg {
transform: scale(1.2) rotate(90deg);
}
}
}
}

View File

@@ -1,65 +0,0 @@
<template>
<div v-auto-animate class="artists-results">
<div>
<div
v-if="album_grid == true && search.albums.value.length"
class="search-results-grid"
>
<AlbumCard
v-for="a in search.albums.value"
:key="a.albumid"
:album="a"
/>
</div>
<div
v-else-if="!album_grid && search.artists.value.length"
class="search-results-grid"
>
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
:artist="artist"
:alt="true"
/>
</div>
<div v-else class="t-center">
<h5>No {{ album_grid ? "albums" : "artists" }}</h5>
</div>
</div>
<LoadMore
v-if="search.albums.value.length || search.artists.value.length"
:loader="album_grid ? search.loadAlbums : search.loadArtists"
:can_load_more="album_grid ? search.albums.more : search.artists.more"
/>
</div>
</template>
<script setup lang="ts">
import useSearchStore from "../../../stores/search";
import AlbumCard from "@/components/shared/AlbumCard.vue";
import ArtistCard from "../../shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
const search = useSearchStore();
defineProps<{
album_grid?: boolean;
}>();
</script>
<style lang="scss">
.artists-results {
height: 100%;
display: grid;
margin: 0 1rem;
grid-template-rows: 1fr max-content;
}
.search-results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
min-height: 10rem;
padding-bottom: $small;
}
</style>

View File

@@ -1,41 +0,0 @@
<template>
<div class="morexx">
<button
@click.prevent="loader()"
:class="{
load_disabled: !can_load_more,
}"
>
<div class="text">Load More</div>
</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
loader: () => void;
can_load_more: boolean;
}>();
</script>
<style lang="scss">
.morexx {
display: grid;
place-items: center;
margin-top: $small;
button {
padding: 0 1rem !important;
width: calc(100% + 2rem);
border-radius: 0;
height: 3rem;
background: $darkestblue;
border: none;
&:hover {
background: $darkblue;
border-color: $darkblue;
}
}
}
</style>

View File

@@ -2,9 +2,9 @@
<div class="right-search">
<TabsWrapper
:tabs="tabs"
:current-tab="currentTab"
:tab-content="true"
@switchTab="switchTab"
:currentTab="currentTab"
:tabContent="true"
>
<Tab :name="currentTab" />
</TabsWrapper>
@@ -48,5 +48,7 @@ function switchTab(tab: string) {
align-items: center;
position: relative;
}
}
</style>

View File

@@ -3,10 +3,13 @@
</template>
<script setup lang="ts">
import ArtistGrid from "./ArtistGrid.vue";
import useSearch from "@/stores/search";
import TracksGrid from "./TracksGrid.vue";
import TopResults from "./TopResults.vue";
import CardPage from "@/views/SearchView/CardGridPage.vue";
const search = useSearch();
const props = defineProps<{
name: string;
}>();
@@ -24,15 +27,23 @@ function getComponent() {
};
case "albums":
return {
component: ArtistGrid,
component: CardPage,
props: {
album_grid: true,
page: "album",
items: search.albums.value,
outside_route: true,
fetch_callback: search.loadAlbums,
},
};
case "artists":
return {
component: ArtistGrid,
props: {},
component: CardPage,
props: {
page: "artist",
items: search.artists.value,
outside_route: true,
fetch_callback: search.loadArtists,
},
};
default:
return null;

View File

@@ -44,6 +44,14 @@ defineEmits<{
justify-content: center;
align-items: center;
}
.vue-recycle-scroller {
padding: 0 $small;
}
.cardlistrow {
grid-template-columns: repeat(auto-fill, minmax(8.1rem, 1fr));
}
}
#tab-content {

View File

@@ -1,10 +1,7 @@
<template>
<div class="right-search-top-albums-or-artists">
<AlbumCard
v-for="album in search.top_results.albums.slice(
0,
!onSearchPage ? 3 : search.top_results.albums.length
)"
v-for="album in search.top_results.albums.slice(0, 3)"
:key="album.albumhash"
:album="album"
/>
@@ -16,10 +13,6 @@ import useSearchstore from "@/stores/search";
import AlbumCard from "@/components/shared/AlbumCard.vue";
const search = useSearchstore();
defineProps<{
onSearchPage?: boolean;
}>();
</script>
<style lang="scss">

View File

@@ -1,10 +1,7 @@
<template>
<div class="right-search-top-albums-or-artists">
<ArtistCard
v-for="artist in search.top_results.artists.slice(
0,
!onSearchPage ? 3 : search.top_results.artists.length
)"
v-for="artist in search.top_results.artists.slice(0, 3)"
:key="artist.artisthash"
:artist="artist"
/>
@@ -12,12 +9,8 @@
</template>
<script setup lang="ts">
import ArtistCard from "@/components/shared/ArtistCard.vue";
import useSearchStore from "@/stores/search";
import ArtistCard from "@/components/shared/ArtistCard.vue";
const search = useSearchStore();
defineProps<{
onSearchPage?: boolean;
}>();
</script>
</script>

View File

@@ -17,15 +17,17 @@ import { Track } from "@/interfaces";
import useQueueStore from "@/stores/queue";
import useSearchStore from "@/stores/search";
import useTracklist from "@/stores/queue/tracklist";
import TrackItem from "@/components/shared/TrackItem.vue";
const search = useSearchStore();
const queue = useQueueStore();
const tracklist = useTracklist();
function handlePlay(track: Track) {
queue.clearQueue();
queue.playFromSearch(search.query, [track]);
tracklist.setFromSearch(search.query, [track]);
queue.play(0);
}
</script>

View File

@@ -1,44 +1,75 @@
<template>
<div id="tracks-results">
<div v-if="search.tracks.value.length">
<TrackItem
v-for="(track, index) in search.tracks.value"
:key="track.id"
:isCurrent="queue.currenttrackhash === track.trackhash"
:isHighlighted="false"
:isCurrentPlaying="
queue.currenttrackhash === track.trackhash && queue.playing
"
:track="track"
@playThis="updateQueue(index)"
:index="index + 1"
/>
<div v-if="!search.tracks.value.length" class="t-center">
<h5>No tracks</h5>
</div>
<div v-else class="t-center"><h5>No tracks</h5></div>
<LoadMore
:loader="search.loadTracks"
:can_load_more="search.tracks.more"
v-if="search.tracks.value.length"
/>
<RecycleScroller
v-slot="{ item, index }"
class="scroller"
style="height: 100%"
:items="scrollerItems"
:item-size="64"
key-field="id"
>
<component
:is="item.component"
v-bind="item.props"
@playThis="updateQueue(index)"
/>
</RecycleScroller>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import { computed, onMounted } from "vue";
import useQueue from "@/stores/queue";
import useSearch from "@/stores/search";
import useTracklist from "@/stores/queue/tracklist";
import TrackItem from "@/components/shared/TrackItem.vue";
import useQStore from "@/stores/queue";
import useSearchStore from "@/stores/search";
import LoadMore from "./LoadMore.vue";
import AlbumsFetcher from "@/components/ArtistView/AlbumsFetcher.vue";
const queue = useQStore();
const search = useSearchStore();
const queue = useQueue();
const search = useSearch();
const tracklist = useTracklist();
function updateQueue(index: number) {
queue.playFromSearch(search.query, search.tracks.value);
tracklist.setFromSearch(search.query, search.tracks.value);
queue.play(index);
}
const scrollerItems = computed(() => {
const items: any[] = search.tracks.value.map((track, index) => {
return {
track,
id: index,
component: TrackItem,
props: {
track,
index: index + 1,
isCurrent: queue.currenttrackhash === track.trackhash,
isCurrentPlaying:
queue.currenttrackhash === track.trackhash && queue.playing,
isHighlighted: false,
},
};
});
items.push({
id: Math.random(),
component: AlbumsFetcher,
props: {
fetch_callback: search.loadTracks,
showtext: search.tracks.more,
outside_route: true,
},
});
return items;
});
onMounted(() => {
search.switchTab("tracks");
});

View File

@@ -10,7 +10,7 @@
@click.prevent="handleButton"
>
<SearchSvg v-if="on_nav || tabs.current === tabs.tabs.queue" />
<BackSvg v-else-if="tabs.current === tabs.tabs.search" />
<BackSvg v-else />
</button>
<input
id="globalsearch"
@@ -56,7 +56,7 @@ function removeFocusedClass() {
function handleButton() {
if (props.on_nav) return;
if (tabs.current === tabs.tabs.search) {
if (tabs.current === tabs.tabs.search || tabs.current === tabs.tabs.lyrics) {
tabs.switchToQueue();
} else {
tabs.switchToSearch();

View File

@@ -1,12 +0,0 @@
<template></template>
<script setup lang="ts">
import { onMounted } from "vue";
const props = defineProps<{
action: () => void;
}>();
onMounted(() => {
props.action();
});
</script>

View File

@@ -0,0 +1,92 @@
<template>
<div class="aboutswingmusic">
Swing Music is a labor of love developed by
<a href="https://github.com/cwilvx" target="_blank">@<u>cwilvx</u></a> on
GitHub. If you like this software, please consider donating to support
development and giving it a star on GitHub. <br /><br /><br />
<div class="flex">
<a href="https://swingmusic.vercel.app/support-us.html" target="_blank">
<button>Donate</button></a
><a href="https://github.com/cwilvx/swingmusic" target="_blank"
><button>Star on Github</button></a
>
<a href="https://github.com/cwilvx" target="_blank"
><button>Follow @cwilvx on Github</button></a
>
</div>
<br /><br />
If you encounter any bugs, please open an issue on GitHub. If you would like
to get involved in development, start with the
<a
href="https://github.com/cwilvx/swingmusic/blob/master/.github/contributing.md"
target="_blank"
><u>contribution guidelines</u></a
>.
<br /><br /><br />
<div class="flex">
<a
href="https://github.com/cwilvx/swingmusic/issues/new/choose"
target="_blank"
>
<button>Open an Issue</button>
</a>
<a
href="https://github.com/cwilvx/swingmusic/blob/master/.github/contributing.md"
target="_blank"
><button>Contribute</button></a
>
</div>
<br /><br />
Hope you enjoy using Swing Music as much as I enjoy building it.
<br /><br />
<div class="hireme rounded">
<h2>Hire me</h2>
If you like my work, and would like me to work for you or your company,
I'm open to offers. Feel free to reach out to me via email.
<br /><br />
<div class="flex">
<a
href="mailto:geoffreymungai45@gmail.com?subject=Job Offer&body=Hi Mungai,
"
target="_blank"
><button>Write Email</button></a
>
</div>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss">
.aboutswingmusic {
padding: $small;
margin-top: 2rem;
.flex {
gap: 1rem;
}
.hireme {
background-color: #ffffff;
background-image: linear-gradient(
37deg,
#bfeaf0 0%,
#ffffff00 50%,
#a7dcff 100%
);
padding: 1rem;
color: $black;
button {
background-color: $blue;
}
h2 {
margin-top: $small;
}
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="lockernumberinput">
<button class="minus" @click="submit('minus')">-</button>
<div class="number">{{ value }}{{ unit }}</div>
<button class="plus" @click="submit('plus')">+</button>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
value: number;
min: number;
max: number;
step: number;
unit: string;
onChange: (value: number) => void;
}>();
function submit(action: "plus" | "minus") {
const newValue = props.value + (action === "plus" ? props.step : -props.step);
if (newValue < props.min || newValue > props.max) return;
props.onChange(newValue);
}
</script>
<style lang="scss">
.lockernumberinput {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
align-items: center;
.number {
text-align: center;
}
button {
width: 2.25rem;
}
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="settings-quickactions grid">
<button
v-for="a in actions"
:key="a.label"
class="qaction rounded-sm"
@click="a.action"
>
<component :is="a.icon" v-if="a.icon" />
<div class="label">{{ a.label }}</div>
<Switch v-if="a.state" :state="a.state()" />
</button>
</div>
</template>
<script setup lang="ts">
import useSettings from "@/stores/settings";
import { triggerScan } from "@/requests/settings/rootdirs";
import Switch from "./Switch.vue";
import ReloadSvg from "@/assets/icons/reload.svg";
const settings = useSettings();
const actions = [
{
label: "Rescan library",
action: triggerScan,
icon: ReloadSvg,
},
{
label: "Sidebar",
action: settings.toggleDisableSidebar,
state: () => settings.use_sidebar,
},
{
label: "Silcence skip",
action: settings.toggleUseSilenceSkip,
state: () => settings.use_silence_skip,
},
{
label: "Crossfade",
action: settings.toggleCrossfade,
state: () => settings.use_crossfade,
},
];
</script>
<style lang="scss">
.settings-quickactions.grid {
gap: 1.25rem;
padding-top: $medium;
grid-template-columns: 1fr 1fr;
.qaction {
background-color: $gray5;
padding: 1.5rem $small;
font-size: 14px;
border: solid 1px $gray4;
// justify-content: space-between;
gap: $smaller;
&:hover {
background-color: $gray4;
}
}
.switch {
transform: scale(0.8);
}
.qaction svg {
transform: scale(0.75);
}
.qaction:nth-child(2) svg {
transform: rotate(90deg);
}
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<div class="setting-select rounded-sm no-scroll">
<div
class="option"
v-for="option in optionsWithActive"
:key="option.title"
class="option"
:class="{ active: option.active }"
@click="setterFn(option.value)"
>

View File

@@ -24,13 +24,13 @@
</template>
<script setup lang="ts">
import { Ref, computed, ref, watch } from "vue";
import { Ref, computed, onMounted, ref } from "vue";
const separatorinput: Ref<HTMLInputElement | null> = ref(null);
const input = ref("");
const props = defineProps<{
default: () => string[];
default: string[];
submit: (input: string) => void;
}>();
@@ -69,14 +69,16 @@ function submitInput() {
const preview_items = computed(() => splitInput(input.value));
const default_input = computed(() =>
props.default() ? props.default().join(", ") : ""
props.default ? props.default.join(", ") : ""
);
watch(props.default, (newval, _) => {
const text = newval.join(", ");
if (separatorinput.value) separatorinput.value.value = text;
onMounted(() => {
const text = props.default.join(", ");
input.value = text;
if (separatorinput.value) {
separatorinput.value.value = text;
}
});
</script>

View File

@@ -6,7 +6,7 @@
<script setup lang="ts">
defineProps<{
state: boolean | null;
state: undefined | boolean;
}>();
</script>

View File

@@ -6,44 +6,62 @@
Customize your settings and preferences
</template>
</GenericHeader>
<GenericTabs
:items="
settingGroups.map((g) => ({
title: g.title,
params: {
tab: g.title.toLowerCase(),
},
}))
"
:active="(item) => item.title === currentTab?.title"
:route="Routes.settings"
/>
<Group
v-for="(group, index) in settingGroups[current].groups"
v-for="(group, index) in currentTab?.groups"
:key="index"
:group="group"
/>
<About v-if="currentTab?.title === 'About'" />
<div class="version t-center">
<LogoSvg /> <span>Swing Music - v{{ VERSION }}</span>
<b>Swing Music - v{{ settings.version }}</b>
</div>
</div>
</template>
<script setup lang="ts">
import { VERSION } from "@/config";
import { computed } from "vue";
import { Routes } from "@/router";
import { useRoute } from "vue-router";
import settingGroups from "@/settings";
import useSettings from "@/stores/settings";
import Group from "./Group.vue";
import About from "./About.vue";
import GenericTabs from "@/components/shared/GenericTabs.vue";
import GenericHeader from "@/components/shared/GenericHeader.vue";
import LogoSvg from "@/assets/icons/logos/logo-light.svg";
defineProps<{
current: number;
}>();
const route = useRoute();
const settings = useSettings();
const currentTab = computed(() => {
const tab = route.params.tab;
return settingGroups.find((group) => group.title.toLowerCase() === tab);
});
</script>
<style lang="scss">
.settingscontent {
width: 35rem;
max-width: 100%;
padding-bottom: 2rem;
.version {
margin: 2rem auto;
color: $gray1;
height: 3rem;
width: max-content;
display: flex;
align-items: center;
gap: 1rem;
opacity: 0.5;
font-size: 14px;
}
}
</style>

View File

@@ -1,7 +1,12 @@
<template>
<div v-if="group.show_if ? group.show_if() : true" class="settingsgroup">
<div v-if="group.title || group.desc" class="info">
<h4 v-if="group.title">{{ group.title }}</h4>
<h4 v-if="group.title">
{{ group.title
}}<span v-if="group.experimental" class="experimental circular">
{{ group.experimental ? "experimental" : "" }}
</span>
</h4>
<div v-if="group.desc" class="desc">{{ group.desc }}</div>
</div>
<div class="setting rounded pad-lg">
@@ -25,6 +30,9 @@
<div class="title">
<span class="ellip">
{{ setting.title }}
<span v-if="setting.experimental" class="experimental circular">
{{ setting.experimental ? "experimental" : "" }}
</span>
</span>
<button
v-if="setting.type == SettingType.root_dirs"
@@ -55,8 +63,18 @@
>
{{ setting.button_text && setting.button_text() }}
</button>
<LockedNumberInput
v-if="setting.type == SettingType.locked_number_input"
:value="setting.state !== null ? setting.state() : 0"
:min="0"
:max="10"
:step="1"
:unit="'s'"
:on-change="setting.action"
/>
</div>
<QuickActions v-if="setting.type == SettingType.quick_actions" />
<List
v-if="setting.type === SettingType.root_dirs"
icon="folder"
@@ -65,7 +83,7 @@
<SeparatorsInput
v-if="setting.type === SettingType.separators_input && setting.action"
:submit="setting.action"
:default="setting.state ? setting.state : () => []"
:default="setting.state ? setting.state() : []"
/>
</div>
</div>
@@ -76,11 +94,13 @@
import { SettingType } from "@/settings/enums";
import { SettingGroup } from "@/interfaces/settings";
import List from "./Components/List.vue";
import Switch from "./Components/Switch.vue";
import Select from "./Components/Select.vue";
import List from "./Components/List.vue";
import SeparatorsInput from "./Components/SeparatorsInput.vue";
import ReloadSvg from "@/assets/icons/reload.svg";
import QuickActions from "./Components/QuickSettings.vue";
import SeparatorsInput from "./Components/SeparatorsInput.vue";
import LockedNumberInput from "./Components/LockedNumberInput.vue";
defineProps<{
group: SettingGroup;
@@ -96,6 +116,15 @@ defineProps<{
border-bottom: solid 1px $gray;
padding-bottom: 2rem;
.experimental {
font-size: 12px;
margin-left: $small;
opacity: 0.5;
border: solid 1px $yellow;
color: $yellow;
padding: 0 $smaller;
}
&:first-child {
margin-top: 0;
}
@@ -116,8 +145,6 @@ defineProps<{
.setting {
background-color: $gray;
// display: grid;
// gap: 1rem;
.inactive {
opacity: 0.5;

View File

@@ -17,8 +17,8 @@
<div class="creator t-center">
Designed and developed by
<span class="name"
><a target="_blank" href="https://github.com/mungai-njoroge"
>Mungai Njoroge</a
><a target="_blank" href="https://github.com/cwilvx"
>@cwilvx</a
>
</span>
</div>
@@ -44,12 +44,6 @@
color: $pink;
}
.release {
margin-left: $smaller;
font-size: 0.7rem;
color: $gray1;
}
.bottom-banner {
font-size: small;
margin-top: 1rem;

View File

@@ -1,13 +1,13 @@
<template>
<div class="modal" v-if="modal.visible">
<div v-if="modal.visible" class="modal">
<div class="bg" @click="modal.hideModal"></div>
<div
v-motion-slide-top
class="m-content rounded"
:style="{
maxWidth:
modal.component == modal.options.setRootDirs ? '56rem' : '30rem',
}"
v-motion-slide-top
>
<div class="heading">{{ modal.title }}</div>
<div class="close circular" @click="modal.hideModal">
@@ -20,8 +20,8 @@
@setTitle="setTitle"
/>
<UpdatePlaylist
v-bind="modal.props"
v-if="modal.component == modal.options.updatePlaylist"
v-if="modal.component == modal.options.updatePlaylist"
v-bind="modal.props"
@hideModal="hideModal"
@setTitle="setTitle"
/>
@@ -29,14 +29,10 @@
<div v-if="modal.component == modal.options.deletePlaylist">
<ConfirmModal
:text="'Are you sure you want to permanently delete this playlist?'"
:cancelAction="modal.hideModal"
:confirmAction="deletePlaylist"
:cancel-action="modal.hideModal"
:confirm-action="deletePlaylist"
/>
</div>
<SetIP
v-if="modal.component == modal.options.SetIP"
@setTitle="setTitle"
/>
<SetRootDirs
v-if="modal.component == modal.options.setRootDirs"
@hideModal="hideModal"
@@ -59,7 +55,6 @@ import WelcomeModal from "./WelcomeModal.vue";
import ConfirmModal from "./modals/ConfirmModal.vue";
import NewPlaylist from "./modals/NewPlaylist.vue";
import RootDirsPrompt from "./modals/RootDirsPrompt.vue";
import SetIP from "./modals/SetIP.vue";
import SetRootDirs from "./modals/SetRootDirs.vue";
import UpdatePlaylist from "./modals/updatePlaylist.vue";

View File

@@ -15,17 +15,17 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import {
saveAlbumAsPlaylist,
saveArtistAsPlaylist,
saveTrackAsPlaylist,
} from "@/requests/playlists";
import useQueueStore from "@/stores/queue";
import { NotifType, Notification } from "@/stores/notification";
import { createNewPlaylist, saveFolderAsPlaylist } from "@/requests/playlists";
import useTracklist from "@/stores/queue/tracklist";
import usePlaylistStore from "@/stores/pages/playlists";
import { NotifType, Notification } from "@/stores/notification";
const props = defineProps<{
trackhash?: string;
@@ -37,7 +37,6 @@ const props = defineProps<{
}>();
const store = usePlaylistStore();
const route = useRoute();
onMounted(() => {
const input_elem = document.getElementById(
@@ -112,8 +111,8 @@ function create(e: Event) {
};
const createQueuePlaylist = () => {
const queue = useQueueStore();
const trackhashes = queue.tracklist.map((track) => track.trackhash);
const { tracklist } = useTracklist();
const trackhashes = tracklist.map((track) => track.trackhash);
const itemhash = trackhashes.join(",");
saveTrackAsPlaylist(name, itemhash).then((res) => {

View File

@@ -1,48 +0,0 @@
<template>
<div class="set-ip-modal">
<label for="">Change the </label>
<input
id="modal-input"
type="text"
class="rounded-sm"
v-model="ip_address"
/>
<button
class="circular btn-active"
@click.prevent="setBaseApiUrl(ip_address)"
>
Set address
</button>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { setBaseApiUrl, baseApiUrl } from "@/config";
const emit = defineEmits<{
(e: "setTitle", title: string): void;
}>();
const title = "Set backend address";
const ip_address = ref("http://localhost:1970");
onMounted(() => {
emit("setTitle", title);
document.getElementById("modal-input")?.focus();
});
</script>
<style lang="scss">
.set-ip-modal {
#modal-input {
margin-top: 1rem;
}
button {
margin: 0 auto;
margin-top: $small;
padding: 0 1rem;
}
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<br /><br />
<div style="position: relative">
<div class="bread-nav rounded-sm" id="bread-nav">
&nbsp;&nbsp;<span @click="fetchDirs('$root')">📁</span
>&nbsp;&nbsp;<BreadCrumbNav :subPaths="subPaths" @navigate="fetchDirs" />
<div id="bread-nav" class="bread-nav rounded-sm">
&nbsp;&nbsp;<span @click="fetchDirs('$root')">$root</span
>&nbsp;&nbsp;<BreadCrumbNav :sub-paths="subPaths" @navigate="fetchDirs" />
</div>
<div class="set-root-dirs-browser">
<h4 v-if="no_more_dirs">
@@ -16,20 +16,20 @@
v-for="dir in dirs"
:key="dir.name"
:folder="dir"
@navigate="fetchDirs(dir.path)"
@check="handleCheck(dir.path)"
:is_checked="
selected.filter((p) => p == dir.path).length > 0 ? true : false
"
@navigate="fetchDirs(dir.path)"
@check="handleCheck(dir.path)"
/>
</div>
</div>
<div class="buttons">
<button class="btn-active select-here" @click="selectHere">
Select here
Add this folder
</button>
<button class="btn-active finish" @click="submitFolders">
Select checked ({{ getNewDirs().length }})
Add all checked ({{ getNewDirs().length }})
</button>
</div>
</div>
@@ -40,9 +40,9 @@
import { onMounted, Ref, ref } from "vue";
import {
addRootDirs,
getFolders,
getRootDirs,
addRootDirs,
getFolders,
getRootDirs,
} from "@/requests/settings/rootdirs";
import { Folder, subPath } from "@/interfaces";
@@ -113,7 +113,7 @@ function submitFolders() {
}
function selectHere() {
if (current == "$root") return;
if (current == "$root" || current == "/") return;
addRootDirs([current], [])
.then((res) => settings.setRootDirs(res))
@@ -188,7 +188,7 @@ onMounted(() => {
.buttons {
display: flex;
justify-content: space-between;
justify-content: center;
gap: $medium;
margin-right: 1rem;
margin-bottom: -$medium;
@@ -199,17 +199,9 @@ onMounted(() => {
}
button {
font-weight: normal;
padding: 0 1rem;
}
button.select-here {
border: solid $darkestblue;
background: transparent;
&:hover {
background-color: $darkestblue;
}
}
}
.f-item {

View File

@@ -16,28 +16,21 @@
<SearchTitle v-if="$route.name == Routes.search" />
<PlaylistsTitle v-if="$route.name == Routes.playlists" />
<QueueTitle v-if="$route.name == Routes.nowPlaying" />
<ArtistDiscographyTitle
v-if="$route.name == Routes.artistDiscography"
/>
<SimpleNav
v-if="$route.name == Routes.artistTracks"
:text="$route.query.artist as string || 'Artist Tracks'"
/>
<SimpleNav
v-if="$route.name === Routes.favorites"
:text="'Favorites ❤️'"
/>
<SimpleNav
v-if="$route.name === Routes.favoriteAlbums"
:text="'Favorite Albums ❤️'"
:text="'Favorite Albums'"
/>
<SimpleNav
v-if="$route.name === Routes.favoriteArtists"
:text="'Favorite Artists ❤️'"
:text="'Favorite Artists'"
/>
<SimpleNav
v-if="$route.name === Routes.favoriteTracks"
:text="'Favorite Tracks ❤️'"
:text="'Favorite Tracks'"
/>
</div>
</div>
@@ -54,7 +47,6 @@ import { createSubPaths } from "@/utils";
import NavButtons from "./NavButtons.vue";
import ArtistDiscographyTitle from "./Titles/ArtistDiscographyTitle.vue";
import FolderTitle from "./Titles/Folder.vue";
import PlaylistsTitle from "./Titles/PlaylistsTitle.vue";
import QueueTitle from "./Titles/QueueTitle.vue";
@@ -98,8 +90,6 @@ watch(
.topnav {
display: grid;
grid-template-columns: 1fr min-content;
width: 100%;
padding: 0 1.25rem;
.left {
display: grid;

View File

@@ -1,118 +0,0 @@
<template>
<div class="artist-discography-nav">
<h1 class="ellip">{{ store.artistname }}</h1>
<div class="buttons">
<div class="select rounded-sm" v-auto-animate="{ duration: 10 }">
<button class="selected" @click.prevent="showDropDown = !showDropDown">
<span class="ellip">{{ store.page }}</span>
<ArrowSvg />
</button>
<div
ref="dropOptionsRef"
class="options rounded-sm shadow-lg"
v-if="showDropDown"
>
<div
class="option"
v-for="a in albums"
@click.prevent="switchView(a)"
>
{{ a }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onClickOutside } from "@vueuse/core";
import ArrowSvg from "@/assets/icons/expand.svg";
import { discographyAlbumTypes as albums } from "@/enums";
import { Ref, ref } from "vue";
import useArtistDiscogStore from "@/stores/pages/artistDiscog";
const store = useArtistDiscogStore();
const showDropDown = ref(false);
const dropOptionsRef: Ref<HTMLElement | undefined> = ref();
function hideDropDown() {
showDropDown.value = false;
}
function switchView(album: albums) {
store.setAlbums(album);
hideDropDown();
}
onClickOutside(dropOptionsRef, (e) => {
// @ts-ignore
e.stopImmediatePropagation();
hideDropDown();
});
</script>
<style lang="scss">
.artist-discography-nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
h1 {
margin: 0;
}
.buttons {
display: flex;
}
.selected {
display: grid;
grid-template-columns: 1fr 2rem;
gap: $smaller;
width: 100%;
padding-right: 0;
svg {
transform: rotate(90deg) scale(0.9);
}
}
.select {
position: relative;
width: 8rem;
display: flex;
align-items: center;
font-size: calc($medium + 2px);
z-index: 10;
.options {
background-color: $gray;
position: absolute;
top: 120%;
padding: $small $smaller;
display: grid;
}
.option {
padding: $small;
border-bottom: 1px solid $gray4;
width: 7.5rem;
&:hover {
border-radius: $smaller;
border-bottom: 1px solid transparent;
background-color: $darkestblue;
}
&:last-child {
border-bottom: none;
}
}
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="nav-queue-title">
<QueueActions />
<QueueActions :on-now-playing="true" />
</div>
</template>
@@ -17,7 +17,6 @@ import QueueActions from "@/components/RightSideBar/Queue/QueueActions.vue";
.queue-actions {
margin: 0;
width: 100%;
}
}
</style>

View File

@@ -1,18 +1,17 @@
<template>
<div class="nav-search-input">
<SearchInput :on_nav="true" />
<div v-if="!isMobile" class="buttons-area">
<Tabs
:tabs="tabs"
:current-tab="($route.params.page as string)"
@switchTab="(tab: string) => {
<Tabs
v-if="!isMobile"
:tabs="tabs"
:current-tab="($route.params.page as string)"
@switchTab="(tab: string) => {
$router.replace({ name: Routes.search, params: { page: tab }, query: {
q: search.query,
} });
search.switchTab(tab);
}"
/>
</div>
/>
</div>
</template>
@@ -32,33 +31,17 @@ const tabs = ["top", "tracks", "albums", "artists"];
.nav-search-input {
align-items: center;
display: grid;
grid-template-columns: minmax(14rem, 20rem) max-content;
grid-template-columns: minmax(10rem, 20rem) max-content;
justify-content: space-between;
gap: 2rem;
gap: 1rem;
@include allPhones {
grid-template-columns: 1fr;
gap: 0;
}
.buttons-area {
position: relative;
height: 100%;
width: 18rem;
}
#right-tabs {
width: max-content;
height: max-content;
.tabheaders {
height: 38px;
}
}
.tabheaders {
height: 2.25rem;
margin: 0;
position: relative;
}
}
</style>

View File

@@ -22,6 +22,9 @@
/>
</div>
<div>
<div v-if="album.help_text" class="rhelp album">
{{ album.help_text }}
</div>
<h4 v-tooltip class="title ellip">
{{ album.title }}
</h4>
@@ -38,30 +41,6 @@
{{ `${artists[0].name}` }}
</RouterLink>
</div>
<!-- when showing other versions -->
<!-- <div
v-if="
album.versions.length === 0 &&
hide_artists &&
$route.name === Routes.album
"
class="artist ellip"
>
{{ album.date }}
{{
album.albumartists.length > 1
? ` • ${album.albumartists[1].name}`
: ""
}}
</div> -->
<!-- <div v-else>
<div class="artist ellip">
{{ album.date }}
</div>
</div> -->
<!-- end -->
<div v-if="album.versions.length" class="versions">
<MasterFlag
v-for="v in getVersions(
@@ -121,8 +100,6 @@ const artists = computed(() => {
<style lang="scss">
.album-card {
flex: 0 0 10.1rem;
display: grid;
gap: $small;
padding: $medium;

View File

@@ -8,17 +8,39 @@
}"
class="artist-card"
>
<img class="artist-image circular" :src="imguri + artist.image" />
<div class="image circular">
<img class="artist-image circular" :src="imguri + artist.image" />
<div
class="overlay circular"
:style="{
background: `linear-gradient(to top, ${artist.colors[0]} 20%, transparent)`,
}"
></div>
<PlayBtn
:artisthash="artist.artisthash"
:artistname="artist.name"
:source="playSources.artist"
/>
</div>
<div v-if="artist.help_text" class="rhelp t-center">
{{ artist.help_text }}
</div>
<div class="artist-name t-center">
{{ artist.name }}
</div>
<div v-if="artist.help_text && artist.trackcount" class="racount t-center">
{{ artist.trackcount }} Track{{ artist.trackcount == 1 ? "" : "s" }}
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { Artist } from "@/interfaces";
import { Routes } from "@/router";
import { paths } from "../../config";
import { paths } from "@/config";
import PlayBtn from "./PlayBtn.vue";
import { playSources } from "@/enums";
const imguri = paths.images.artist.large;
@@ -29,32 +51,67 @@ defineProps<{
<style lang="scss">
.artist-card {
flex: 0 0 10.1rem;
overflow: hidden;
position: relative;
border-radius: $medium;
display: grid;
gap: $small;
justify-content: center;
padding: 1.2rem 1rem !important;
font-size: 0.9rem;
font-weight: bolder;
height: max-content;
.image {
position: relative;
.overlay {
position: absolute;
width: 100%;
height: calc(100% - $small + 1px);
top: 0;
opacity: 0;
}
}
$btnwidth: 4rem;
.play-btn {
opacity: 0;
position: absolute;
width: 4rem;
bottom: 0;
left: calc(50% - ($btnwidth / 2));
transition: all 0.25s;
}
&:hover {
background-color: $gray4;
.play-btn {
opacity: 1;
transform: translateY(-1.25rem);
}
.overlay {
opacity: 1;
}
}
.artist-image {
width: 100%;
transition: all 0.5s ease-in-out;
object-fit: cover;
margin-bottom: $smaller;
}
.artist-name {
margin-bottom: $smaller;
word-break: break-word;
}
.racount {
font-size: 12px;
color: #ffffffbf;
}
}
</style>

View File

@@ -8,10 +8,10 @@
}"
@click.stop="() => {}"
>
<div v-tooltip v-if="artists === null || artists.length === 0">
<div v-if="artists === null || artists.length === 0" v-tooltip>
<span class="artist">{{ albumartists }}</span>
</div>
<div v-tooltip v-else>
<div v-else v-tooltip>
<template v-for="(artist, index) in artists" :key="index">
<RouterLink
class="artist"
@@ -21,7 +21,7 @@
}"
>{{ `${artist.name}` }}</RouterLink
>
<span class="artist" v-if="index < artists.length - 1"
<span v-if="index < artists.length - 1" class="artist"
>,
</span> </template
>&nbsp;

View File

@@ -0,0 +1,38 @@
<template>
<div v-if="type == 'album'" class="cardlistrow">
<AlbumCard
v-for="item in items"
:key="item.albumhash"
class="hlistitem"
:album="(item as Album)"
/>
</div>
<div v-else-if="type == 'artist'" class="cardlistrow">
<ArtistCard
v-for="item in items"
:key="item.artisthash"
class="hlistitem"
:artist="(item as Artist)"
/>
</div>
</template>
<script setup lang="ts">
import { Album, Artist } from "@/interfaces";
import AlbumCard from "./AlbumCard.vue";
import ArtistCard from "./ArtistCard.vue";
defineProps<{
type: "album" | "artist";
items: Album[] | Artist[];
}>();
</script>
<style lang="scss">
.cardlistrow {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
padding-bottom: 2rem;
z-index: -1;
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="cardscroller">
<div class="rinfo">
<div class="rtitle">
<b>{{ title }}</b>
<SeeAll v-if="route && items.length >= maxAbumCards" :route="route" />
</div>
<div v-if="description" class="rdesc">{{ description }}</div>
</div>
<div class="recentitems">
<component
:is="getComponent(i.type)"
v-for="(i, index) in items.slice(0, maxAbumCards)"
:key="index"
class="hlistitem"
v-bind="getProps(i)"
@playThis="() => $emit('playThis', index)"
></component>
</div>
</div>
</template>
<script setup lang="ts">
import { playSources } from "@/enums";
import { maxAbumCards } from "@/stores/content-width";
import TrackCard from "./TrackCard.vue";
import SeeAll from "../shared/SeeAll.vue";
import FolderCard from "./FolderCard.vue";
import AlbumCard from "./AlbumCard.vue";
import ArtistCard from "./ArtistCard.vue";
import PlaylistCard from "../PlaylistsList/PlaylistCard.vue";
import FavoritesCard from "./FavoritesCard.vue";
const props = defineProps<{
title: string;
description?: string;
items: {
type: string;
item: any;
}[];
playSource?: playSources;
child_props?: any;
route?: string;
}>();
defineEmits<{
playThis: (index: number) => void;
}>();
function getComponent(type: string) {
switch (type) {
case "album":
return AlbumCard;
case "track":
return TrackCard;
case "artist":
return ArtistCard;
case "folder":
return FolderCard;
case "playlist":
return PlaylistCard;
case "favorite_tracks":
return FavoritesCard;
}
}
function getProps(item: { type: string; item: any }) {
switch (item.type) {
case "album":
return {
album: item.item,
...props.child_props,
};
case "track":
return {
track: item.item,
playSource: props.playSource,
};
case "artist":
return {
artist: item.item,
};
case "folder":
return {
folder: item.item,
};
case "playlist":
return {
playlist: item.item,
};
case "favorite_tracks":
return {
item: item.item,
};
}
}
</script>
<style lang="scss">
.cardscroller {
padding: 1.5rem 0;
.recentitems {
gap: 1.5rem 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
}
.p-card {
background-color: transparent;
}
.rinfo {
padding: 0 $medium;
margin-bottom: $medium;
.rtitle {
font-size: 1.15rem;
display: flex;
align-items: baseline;
justify-content: space-between;
}
.rdesc {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.747);
}
}
.to_playlist {
background-color: #fff;
color: #000;
border: 1px solid #fff;
padding: 1.25rem 2rem;
margin: 1rem;
}
}
</style>

View File

@@ -0,0 +1,111 @@
<template>
<div class="smdropdown buttons" :class="component_key">
<div v-auto-animate="{ duration: 10 }" class="select rounded-sm">
<button class="selected" @click.prevent="handleOpener">
<span class="ellip">{{ current }}</span>
<ArrowSvg />
</button>
<div
v-if="showDropDown"
ref="dropOptionsRef"
class="options rounded-sm shadow-lg"
>
<div
v-for="a in items"
:key="a"
class="option"
:class="{ current: current == a }"
@click.prevent="handleClick(a)"
>
{{ a }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, ref } from "vue";
import { onClickOutside } from "@vueuse/core";
import ArrowSvg from "@/assets/icons/expand.svg";
const showDropDown = ref(false);
const dropOptionsRef: Ref<HTMLElement | undefined> = ref();
defineProps<{
items: any[];
current: any;
component_key: string;
}>();
const emit = defineEmits<{
(event: "itemClicked", item: any): void;
}>();
const handleOpener = () => {
showDropDown.value = !showDropDown.value;
};
const handleClick = (item: any) => {
emit("itemClicked", item);
showDropDown.value = false;
};
onClickOutside(dropOptionsRef, (e) => {
// @ts-ignore
e.stopImmediatePropagation();
showDropDown.value = false;
});
</script>
<style lang="scss">
.smdropdown {
.selected {
display: grid;
grid-template-columns: 1fr 2rem;
gap: $smaller;
width: 100%;
padding-right: 0;
svg {
transform: rotate(90deg) scale(0.9);
}
}
.select {
position: relative;
width: 8rem;
display: flex;
align-items: center;
font-size: calc($medium + 2px);
z-index: 10;
.options {
background-color: $gray;
position: absolute;
top: 120%;
padding: $small $smaller;
display: grid;
}
.option {
padding: $small;
width: 7.5rem;
border-radius: $smaller;
margin: 0 $smaller;
&:hover {
background-color: $darkestblue;
}
&:last-child {
border-bottom: none;
}
}
.current {
background-color: $gray5;
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<RouterLink
:to="{ name: Routes.favoriteTracks }"
class="favoritescard rounded"
>
<div class="img">
<svg
width="100"
height="100"
viewBox="0 0 28 28"
fill="#ff453a"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13.9912 22.1445C14.2197 22.1445 14.5449 21.9775 14.8086 21.8105C19.7217 18.6465 22.8682 14.9375 22.8682 11.1758C22.8682 7.9502 20.6445 5.7002 17.8408 5.7002C16.0918 5.7002 14.7822 6.66699 13.9912 8.11719C13.2178 6.67578 11.8994 5.7002 10.1504 5.7002C7.34668 5.7002 5.11426 7.9502 5.11426 11.1758C5.11426 14.9375 8.26074 18.6465 13.1738 21.8105C13.4463 21.9775 13.7715 22.1445 13.9912 22.1445Z"
/>
</svg>
<PlayBtn :source="playSources.favorite" />
</div>
<div class="info">
<div class="rhelp playlist">PLAYLIST</div>
<div class="title">Favorite Tracks</div>
<div class="fcount">
<b>{{ item.count + ` Track${item.count == 1 ? "" : "s"}` }}</b>
</div>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import PlayBtn from "../shared/PlayBtn.vue";
import { playSources } from "@/enums";
defineProps<{
item: any;
}>();
</script>
<style lang="scss">
.favoritescard {
padding: $medium;
.img {
width: 100%;
aspect-ratio: 1/1;
background-color: $gray5;
border-radius: $small;
margin-bottom: $medium;
display: flex;
align-items: center;
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
justify-content: center;
position: relative;
}
.play-btn {
position: absolute;
width: 4rem;
bottom: 0;
opacity: 0;
transition: all 0.25s;
}
.fcount {
font-size: 0.8rem;
opacity: 0.75;
padding-top: 2px;
}
&:hover {
background-color: $gray4;
.play-btn {
opacity: 1;
transform: translateY(-1rem);
}
}
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<RouterLink
:to="{
name: Routes.folder,
params: {
path: folder.path,
},
}"
class="foldercard rounded"
>
<div class="rimg rounded-sm">
<svg
width="30"
height="30"
fill="currentColor"
viewBox="0 0 28 28"
xmlns="http://www.w3.org/2000/svg"
class="bg"
>
<path
d="M7 5.6875C5.55231 5.6875 4.375 6.86481 4.375 8.3125V19.6875C4.375 21.1352 5.55231 22.3125 7 22.3125H21C22.4477 22.3125 23.625 21.1352 23.625 19.6875V10.0625C23.625 8.61481 22.4477 7.4375 21 7.4375H12.7328C12.3369 7.4375 11.9492 7.30146 11.6407 7.05383L10.8914 6.45483C10.2732 5.96046 9.49659 5.6875 8.70471 5.6875H7ZM7 7.4375H8.70471C9.10065 7.4375 9.48873 7.57354 9.79761 7.82117L10.5461 8.42017C11.1643 8.91454 11.9409 9.1875 12.7328 9.1875H21C21.4826 9.1875 21.875 9.57994 21.875 10.0625V10.5H6.125V8.3125C6.125 7.82994 6.51744 7.4375 7 7.4375ZM6.125 12.25H21.875V19.6875C21.875 20.1701 21.4826 20.5625 21 20.5625H7C6.51744 20.5625 6.125 20.1701 6.125 19.6875V12.25ZM15.8705 13.3634L13.8214 13.7787C13.6705 13.8093 13.5625 13.9347 13.5625 14.0795V17.1086C13.5625 17.2556 13.4371 17.3717 13.025 17.4513C12.3867 17.5751 11.8125 17.8221 11.8125 18.5903C11.8125 18.9701 12.1575 19.4551 13.025 19.4551C13.7806 19.4551 14.4375 18.8381 14.4375 17.9631V15.6073C14.4375 15.5106 14.509 15.4271 14.6101 15.4065L15.9286 15.1382C16.0795 15.1076 16.1875 14.9822 16.1875 14.8374V13.6035C16.1875 13.4469 16.0337 13.3302 15.8705 13.3634Z"
fill="currentColor"
/>
</svg>
<PlayBtn :source="playSources.folder" :folderpath="folder.path" />
</div>
<div v-if="folder.help_text" class="rhelp folder">
{{ folder.help_text }}
</div>
<div class="ellip" :title="name(folder.path)">
{{ name(folder.path) }}
</div>
<div class="rtcount">
<b>{{ folder.count }} Track{{ folder.count == 1 ? "" : "s" }}</b>
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import PlayBtn from "../shared/PlayBtn.vue";
import { playSources } from "@/enums";
defineProps<{
folder: {
path: string;
count: number;
help_text: string;
};
}>();
const name = (path: string) => {
const splits = path.split("/");
return splits[splits.length - 1];
};
</script>
<style lang="scss">
.foldercard {
padding: $medium;
display: flex;
flex-direction: column;
height: max-content;
.play-btn {
position: absolute;
width: 4rem;
bottom: 0;
opacity: 0;
transition: all 0.25s;
}
&:hover {
background-color: $gray4;
.play-btn {
opacity: 1;
transform: translateY(-1rem);
}
}
svg.bg {
transform: scale(2);
color: $gray2;
}
.rimg {
position: relative;
width: 100%;
aspect-ratio: 1;
background-color: $gray;
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
margin-bottom: $small;
display: flex;
align-items: center;
justify-content: center;
}
.rtcount {
font-size: 12px;
color: #ffffffbf;
margin-top: $smaller;
}
}
</style>

View File

@@ -1,11 +1,9 @@
<template>
<div class="generichead">
<div class="left">
<h1><slot name="name">Playlists</slot></h1>
<h1><slot name="name"></slot></h1>
<div class="desc">
<slot name="description">
<div>All your playlists, all in one place.</div>
</slot>
<slot name="description"></slot>
</div>
</div>
<div class="right">
@@ -18,8 +16,7 @@
<style lang="scss">
.generichead {
margin: 3rem 0;
padding: 0 $smaller;
padding: 2rem 0 1rem $small;
height: max-content;
display: grid;
grid-template-columns: 1fr max-content;

View File

@@ -0,0 +1,60 @@
<template>
<div class="generictabs">
<RouterLink
v-for="(item, index) in items"
:key="index"
class="tab"
:class="{ active: active(item) }"
:to="{
name: route,
params: item.params,
replace: true,
query: item.query,
}"
>
{{ item.title }}
<div class="indicator"></div>
</RouterLink>
</div>
</template>
<script setup lang="ts">
defineProps<{
items: { title: string; params: any, query?: any }[];
active: (item: any) => boolean;
route: string;
}>();
</script>
<style lang="scss">
.generictabs {
display: flex;
border-bottom: solid 1px $gray;
.tab {
padding: $medium;
position: relative;
color: $gray1;
}
.indicator {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
background-color: $white;
height: 3px;
border-radius: 1rem;
opacity: 0;
}
.tab.active {
color: $white;
.indicator {
width: 3rem;
opacity: 1;
}
}
}
</style>

View File

@@ -1,42 +0,0 @@
<template>
<div
class="loaderx"
:class="{ loader: loader.loading, not_loader: !loader.loading }"
>
<div v-if="!loader.loading">🦋</div>
</div>
</template>
<script setup lang="ts">
import useLoaderStore from "@/stores/loader";
const loader = useLoaderStore();
</script>
<style lang="scss">
.loaderx {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
user-select: none;
}
.loader {
border: dotted $blue;
animation: spin 0.25s linear infinite;
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
}
.not_loader {
display: grid;
place-items: center;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<button
title="Lyrics"
class="lyrics"
:class="{ showStatus: lyrics.exists }"
@click="handleClick"
>
<LyricsSvg /> {{ showText ? "Lyrics" : "" }}
</button>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Routes } from "@/router";
import useLyrics from "@/stores/lyrics";
import LyricsSvg from "@/assets/icons/lyrics.svg";
defineProps<{
showText?: boolean;
}>();
const route = useRoute();
const router = useRouter();
const lyrics = useLyrics();
let prevRoute = ref(route.name);
function handleClick() {
if (route.name === Routes.nowPlaying && route.params.tab === "lyrics") {
return router.back();
}
router.push({
name: Routes.nowPlaying,
params: { tab: "lyrics" },
});
prevRoute.value = route.name;
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<button id="option-drop" @click.stop.prevent="showDropdown" class="btn-more">
<button id="option-drop" class="btn-more" @click.stop.prevent="showDropdown">
<MoreSvg />
</button>
</template>

View File

@@ -7,10 +7,17 @@
<script setup lang="ts">
import { Track } from "@/interfaces";
import { playSources } from "@/enums";
import { playFromAlbumCard, playFromArtistCard } from "@/helpers/usePlayFrom";
import {
playFromAlbumCard,
playFromArtistCard,
playFromFavorites,
playFromFolderCard,
playFromPlaylist,
} from "@/helpers/usePlayFrom";
import PlaySvg from "@/assets/icons/play.svg";
import useQueueStore from "@/stores/queue";
import useQueue from "@/stores/queue";
import useTracklist from "@/stores/queue/tracklist";
import useSearchStore from "@/stores/search";
const props = defineProps<{
@@ -19,6 +26,7 @@ const props = defineProps<{
albumName?: string;
artisthash?: string;
artistname?: string;
folderpath?: string;
track?: Track;
}>();
@@ -31,19 +39,28 @@ function handlePlay() {
case playSources.artist:
playFromArtistCard(props.artisthash || "", props.artistname || "");
break;
case playSources.folder:
playFromFolderCard(props.folderpath || "");
break;
case playSources.recentlyAdded:
playFromPlaylist("recentlyadded", props.track);
break;
case playSources.track: {
// insert after current and play
if (!props.track) break;
const queue = useQueueStore();
const queue = useQueue();
const search = useSearchStore();
const tracklist = useTracklist();
queue.clearQueue();
queue.playFromSearch(search.query, [props.track]);
tracklist.setFromSearch(search.query, [props.track]);
queue.play();
break;
}
case playSources.favorite:
playFromFavorites(props.track);
break;
default:
break;

View File

@@ -2,12 +2,12 @@
<button
v-wave
class="playbtnrect shadow-sm circular btn-active"
@click="playFrom(source)"
:style="{
backgroundColor: bg_color ? bg_color : '',
borderColor: bg_color ? bg_color : '',
color: bg_color ? getShift(bg_color, [100, 100]) : '',
}"
@click="playFrom(source)"
>
<playBtnSvg />
<div class="text">Play</div>

View File

@@ -1,6 +1,6 @@
<template>
<span class="see-all">
<RouterLink :to="route"> SEE ALL </RouterLink>
<RouterLink :to="route"> <b>SEE ALL</b> </RouterLink>
</span>
</template>

View File

@@ -127,7 +127,7 @@ onBeforeUnmount(() => {
<style lang="scss">
.songlist-item {
display: grid;
grid-template-columns: 1.75rem 2fr 1fr 1.5fr 5.5rem;
grid-template-columns: 1.75rem 1.25fr 1fr 1fr 5.5rem;
align-items: center;
justify-content: flex-start;
height: $song-item-height;

View File

@@ -1,8 +1,8 @@
<template>
<router-link
v-if="!hide_album"
class="song-album ellip"
v-tooltip
class="song-album ellip"
:to="{
name: 'AlbumView',
params: {

View File

@@ -5,23 +5,23 @@
style="height: 100%"
>
<RecycleScroller
class="scroller"
id="songlist-scroller"
v-slot="{ item, index }"
class="scroller"
style="height: 100%"
:items="tracks.map((track, index) => ({ track, id: index }))"
:item-size="itemHeight"
key-field="id"
v-slot="{ item, index }"
>
<SongItem
:track="item.track"
:index="index + 1"
:index="total ? total - index : index + 1"
:is_queue_track="is_queue"
@playThis="handlePlay(index)"
:is_last="index == tracks.length - 1"
:droppable="false"
@trackDropped="dropHandler"
:source="source"
@playThis="handlePlay(index)"
@trackDropped="dropHandler"
/>
</RecycleScroller>
</div>
@@ -44,6 +44,7 @@ defineProps<{
oldIndex: number
) => void;
source: dropSources;
total?: number;
}>();
const itemHeight = 64;

View File

@@ -0,0 +1,90 @@
<template>
<RouterLink
:to="{
name: Routes.album,
params: {
albumhash: track.albumhash,
},
}"
class="trackcard rounded"
>
<div class="image">
<img class="rounded-sm" :src="paths.images.thumb.large + track.image" />
<PlayBtn :source="playSource" :track="track" />
</div>
<div class="tinfo">
<div v-if="track.help_text" class="rhelp track">
{{ track.help_text }}
</div>
<div class="ttitle ellip">{{ track.title }}</div>
<ArtistName :albumartists="track.albumartists" :artists="track.artists" />
</div>
</RouterLink>
</template>
<script setup lang="ts">
import { paths } from "@/config";
import { Track } from "@/interfaces";
import { playSources } from "@/enums";
import PlayBtn from "../shared/PlayBtn.vue";
import ArtistName from "../shared/ArtistName.vue";
import { Routes } from "@/router";
defineProps<{
track: Track;
playSource: playSources;
}>();
defineEmits<{
playThis: (index: number) => void;
}>();
</script>
<style lang="scss">
.trackcard {
padding: $medium;
cursor: pointer;
height: max-content;
.image {
position: relative;
}
$btnwidth: 4rem;
.play-btn {
opacity: 0;
position: absolute;
width: 4rem;
bottom: $small;
left: calc(50% - ($btnwidth / 2));
transition: all 0.25s;
}
&:hover {
background-color: $gray4;
.play-btn {
opacity: 1;
transform: translateY(-0.75rem);
}
}
.ttitle {
font-size: 0.9rem;
margin-top: $small;
}
img {
width: 100%;
}
.artist {
font-size: 0.8rem;
font-weight: 700;
opacity: 0.86;
opacity: 0.75;
}
}
</style>

View File

@@ -48,7 +48,7 @@
v-if="isQueueTrack"
class="remove-track"
title="Remove from queue"
@click.stop="queue.removeFromQueue(index)"
@click.stop="player.removeByIndex(index)"
>
<DelSvg />
</div>
@@ -60,10 +60,11 @@
import { useRoute } from "vue-router";
import { onBeforeUnmount, ref, watch } from "vue";
import useTracklist from "@/stores/queue/tracklist";
import { paths } from "@/config";
import { favType } from "@/enums";
import { Track } from "@/interfaces";
import useQueueStore from "@/stores/queue";
import favoriteHandler from "@/helpers/favoriteHandler";
import { showTrackContextMenu as showContext } from "@/helpers/contextMenuHandler";
@@ -79,10 +80,11 @@ const props = defineProps<{
index?: number;
}>();
const queue = useQueueStore();
const player = useTracklist();
const route = useRoute();
const context_on = ref(false);
const is_fav = ref(props.track.is_favorite);
const route = useRoute();
function showMenu(e: MouseEvent) {
showContext(e, props.track, context_on, route);

Some files were not shown because too many files have changed in this diff Show More