76 Commits

Author SHA1 Message Date
geoffrey45
cbb7b2bf25 Merge branch 'add-drag,-drop-and-reorder' 2023-04-17 02:32:53 +03:00
geoffrey45
f6e135a28c make songcard less rounded 2023-04-17 02:25:59 +03:00
geoffrey45
e8b7db4d98 add setting to hide master flag
+ show track bitrate on master flag hover
2023-04-14 20:37:12 +03:00
geoffrey45
731d06a35e minor tweaks 2023-04-06 11:59:00 +03:00
geoffrey45
534e163f76 remove console.logs 2023-03-27 16:45:38 +03:00
geoffrey45
3036d019f0 remove drag and drop code: prep for next release 2023-03-27 16:45:38 +03:00
geoffrey45
d8693f187e remove console.log 2023-03-27 16:45:38 +03:00
geoffrey45
8acb9d984e fix ub40 tracks not playing due to use of decodeURIComponent instead of encodeURIComponent 2023-03-27 16:45:38 +03:00
geoffrey45
2edebc8a4d fix artist page appearances see all bug 2023-03-27 16:45:38 +03:00
geoffrey45
5568ffb4ea add button to toggle list mode 2023-03-27 16:45:38 +03:00
geoffrey45
8eee9fe6e9 paint bottom bar artist name with thumbnail cover
+ break bottom bar and playlist header into components
2023-03-27 16:45:38 +03:00
geoffrey45
2274a5b863 add playlist image grid to playlist card
+ add wave animations on nav items, and songcard
2023-03-27 16:45:38 +03:00
geoffrey45
1fcf3658e9 use @nextcss/colortools/shift to make colored headers balanced 2023-03-27 16:45:38 +03:00
geoffrey45
79ca903ee7 add and hide trackbin 2023-03-27 16:45:38 +03:00
geoffrey45
d14567dd3a redesign playlist header
~ to show the playlist contents images in header
2023-03-27 16:45:38 +03:00
geoffrey45
496840794c fix favorite tracks page behaving similar to queue page
+ add icons to root dirs list in settings page
2023-03-27 16:45:38 +03:00
geoffrey45
86e17a979f add v-wave
+ fix console errrors on queue clear
2023-03-27 16:45:38 +03:00
geoffrey45
a3103d212f add padding to context menu
+ misc
2023-03-27 16:45:38 +03:00
geoffrey45
e84673198c a gazzilion refactors 2023-03-27 16:45:38 +03:00
geoffrey45
7ea6c0c27f add draggables and drop zones to songitem component 2023-03-27 16:45:38 +03:00
geoffrey45
7eba9fe307 remove console.logs 2023-03-27 08:01:05 +03:00
geoffrey45
fdb8b9fa31 remove drag and drop code: prep for next release 2023-03-27 03:35:04 +03:00
geoffrey45
766b705f07 remove console.log 2023-03-26 18:35:50 +03:00
geoffrey45
b4e72c77bc fix ub40 tracks not playing due to use of decodeURIComponent instead of encodeURIComponent 2023-03-26 18:34:56 +03:00
geoffrey45
640a7d7830 fix artist page appearances see all bug 2023-03-26 18:06:15 +03:00
geoffrey45
9f8847823e add button to toggle list mode 2023-03-26 11:07:57 +03:00
geoffrey45
93e4db218f paint bottom bar artist name with thumbnail cover
+ break bottom bar and playlist header into components
2023-03-19 23:36:09 +03:00
geoffrey45
101d9496ca add playlist image grid to playlist card
+ add wave animations on nav items, and songcard
2023-03-18 00:39:41 +03:00
geoffrey45
710c6ca91d use @nextcss/colortools/shift to make colored headers balanced 2023-03-17 00:55:54 +03:00
geoffrey45
8beed7f45c add and hide trackbin 2023-03-15 10:02:01 +03:00
geoffrey45
9cb3820c96 redesign playlist header
~ to show the playlist contents images in header
2023-03-14 23:32:47 +03:00
geoffrey45
ed2e1f11e4 fix favorite tracks page behaving similar to queue page
+ add icons to root dirs list in settings page
2023-03-14 20:04:49 +03:00
geoffrey45
8a4b9b1024 add v-wave
+ fix console errrors on queue clear
2023-03-12 10:35:50 +03:00
geoffrey45
bc58a738da add padding to context menu
+ misc
2023-03-04 21:25:28 +03:00
geoffrey45
cf57f915ba a gazzilion refactors 2023-02-26 09:46:41 +03:00
geoffrey45
a7bfb8fe51 add draggables and drop zones to songitem component 2023-02-25 15:24:54 +03:00
geoffrey45
12f8343cd5 update readme 2023-02-24 15:33:08 +03:00
geoffrey45
226e86ce37 add noitems placeholder for playlist page 2023-02-17 18:57:14 +03:00
geoffrey45
e439d14d58 fix: queue page highligting multiple songs 2023-02-17 17:11:37 +03:00
geoffrey45
25c296be90 make "add to fav" button's bg pink 2023-02-15 22:15:47 +03:00
geoffrey45
01f0f8de03 fix: queue last song bug 2023-02-15 17:25:50 +03:00
geoffrey45
e17c498da0 update readme 2023-02-13 20:50:20 +03:00
geoffrey45
37d449b3c4 minor 2023-02-13 20:06:33 +03:00
geoffrey45
ef0265d974 adjust hotkeys spacing 2023-02-13 09:04:30 +03:00
geoffrey45
4944906408 fix: Add track to queue removing track from queue list
+ remove console logs
2023-02-11 14:04:32 +03:00
geoffrey45
51eeee9e16 adjust bottom bar center items margin bottom 2023-02-11 10:32:30 +03:00
geoffrey45
3cde601008 feat: show album date on album card on artist page 2023-02-09 21:03:10 +03:00
geoffrey45
f3137c418a bottom bar improvements 2023-02-08 10:57:38 +03:00
geoffrey45
dbec08fee2 redesign bottom bar 2023-02-08 02:14:17 +03:00
geoffrey45
c500dc6775 feat: show live albums in album header 2023-02-07 12:50:12 +03:00
geoffrey45
6e21908ee2 add light borders to songItem and album card components 2023-02-07 12:21:38 +03:00
geoffrey45
b752ff4bef UI improvements
~ remove gradients from buttons
~ add light borders to modals, buttons
2023-02-07 03:07:30 +03:00
geoffrey45
35e4c2ae8e feat: hide playlist image (placeholder) when none is set 2023-02-04 00:47:16 +03:00
geoffrey45
10c50b5053 fix: highlighting right clicked track
+ make 📁 clickable on set root dirs modal
2023-02-03 22:18:41 +03:00
geoffrey45
7de1bc1ebf feat: add button to remove root dirs on settings page
+ a List component for setting type of list
+ change button text to () => string; to support reactivity
2023-02-03 18:07:48 +03:00
geoffrey45
243e5b8087 fix: extend app to full screen width not working 2023-02-02 11:59:38 +03:00
geoffrey45
7cd828b3cd fix: key error in album apage 2023-02-01 16:06:07 +03:00
geoffrey45
54b1b494fc fix scrolling in right sidebar track search 2023-02-01 13:37:04 +03:00
geoffrey45
3cde77c350 fix: sidebar search loadmore button position
+ add NoItems component to folder page
+ move setRootDirs component button to bottom
+ misc
2023-01-29 13:44:43 +03:00
geoffrey45
65f492945c update dependencies 2023-01-27 05:13:47 +03:00
geoffrey45
67848d02c0 add a "select here" button in the SetRootDirs modal
+ move focus folders breadcrumb to the bread component
2023-01-27 05:05:40 +03:00
geoffrey45
361de1103b fix subpaths for windows 2023-01-26 22:30:11 +03:00
geoffrey45
6c3683b852 add NoItems compont to show when there's no items in page
+ add the above component to playlist, favorite and queue page
2023-01-26 22:13:25 +03:00
geoffrey45
daaa0c48c2 change song card animation from swipe to scale
+ change the "General" button in settings page to "Reset client"
2023-01-25 11:44:10 +03:00
geoffrey45
c6cac53ba7 add setting page item to modify root dirs 2023-01-24 02:13:59 +03:00
geoffrey45
d0caa3b6c4 feat: handle using esc key to hide modals
+ redesign the modal closing button
2023-01-24 00:01:04 +03:00
geoffrey45
861c1beb03 handle showing prompt to set root dirs if none is set
+ animate left sidebar Now playing image
2023-01-23 23:36:39 +03:00
geoffrey45
19289c6c42 feat: increase content width to 2220px for large screen heights 2023-01-23 16:59:44 +03:00
geoffrey45
f877e574af handle navigation in folder page's folderlist component 2023-01-23 10:10:23 +03:00
geoffrey45
9207412c51 increase page banner height for screen height larger than 1080p
+ improve favorite artist and album cards placement calculation
2023-01-23 09:57:59 +03:00
geoffrey45
3871a5ecf8 fix the artist page's top track last track click bug
+ fix SetRootDirs logic
2023-01-23 09:03:28 +03:00
geoffrey45
8b35ad415b build first version of root directory setter modal
with the core functionality
2023-01-21 18:02:09 +03:00
geoffrey45
ad0c6e6580 add modal to set IP adress when client is running on netlify
and IP adress is not set
2023-01-21 11:53:26 +03:00
geoffrey45
790a6fcaf6 remove lazy loading route components 2023-01-20 13:12:58 +03:00
geoffrey45
b2c292b531 change prod base url to dev_url for deploy tests 2023-01-20 12:39:44 +03:00
geoffrey45
6b12e23cd5 update package.json build path 2023-01-20 12:34:18 +03:00
144 changed files with 3049 additions and 1723 deletions

View File

@@ -1,6 +1,6 @@
### Swing music client
This repo contains the client code for the swing music player.
This repo contains the client code for the [swing music player](https://github.com/geoffrey45/swingmusic).
### Setup
@@ -26,15 +26,6 @@ yarn dev
yarn build
```
If you are developing together with the server, make sure the server code is placed in the same directory as the client code. Such that when you run `yarn build` the output is placed in the server's `client` directory.
```bash
│ 
├── swing-client
│  └── # client code here
├── swingmusic
│   └── # server code here
```
### Contributing

View File

@@ -4,7 +4,6 @@
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="./src/assets/icons/logo-small.svg" />
<title>Swing Music</title>
</head>
<body>

View File

@@ -16,12 +16,13 @@
"@vueuse/integrations": "^9.2.0",
"axios": "^0.26.1",
"fuse.js": "^6.6.2",
"motion": "^10.15.5",
"pinia": "^2.0.17",
"pinia-plugin-persistedstate": "^2.1.1",
"sass": "^1.56.1",
"sass-loader": "^13.2.0",
"vite-svg-loader": "^3.4.0",
"vue": "^3.2.37",
"v-wave": "^1.5.0",
"vue": "^v3.2.45",
"vue-debounce": "^3.0.2",
"vue-router": "^4.1.3",
"vue-template-compiler": "^2.0.0",
@@ -29,11 +30,13 @@
"webpack": "^5.74.0"
},
"devDependencies": {
"@nextcss/color-tools": "^1.0.7",
"@vitejs/plugin-vue": "^3.2.0",
"eslint": "^8.7.0",
"eslint-plugin-vue": "^8.3.0",
"vite": "^3.0.4",
"vue-svg-loader": "^0.16.0"
"vite-svg-loader": "^3.4.0",
"vue-virtual-draglist": "^3.0.4"
},
"packageManager": "yarn@1.22.19"
}

BIN
public/favicon.ico Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

4
public/logo-fill.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="118" height="118" viewBox="0 0 118 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="59" cy="59" r="57.5" fill="white" stroke="#CBCBCB" stroke-width="3"/>
<path d="M59.5145 62.9498C65.9658 62.9498 72.2826 62.5664 77.4604 61.8C81.5668 61.179 82.853 58.1484 81.4581 53.8231L73.7457 29.5215C72.3476 25.0713 68.7697 23 63.8316 23H55.1684C50.2303 23 46.6523 25.0713 45.2544 29.5215L37.5419 53.8231C36.1469 58.1484 37.4332 61.179 41.5397 61.8C46.7174 62.5664 53.0395 62.9498 59.5145 62.9498ZM56.8245 60.9007V93.2057H62.1859V60.9007H56.8245ZM46.3261 95.2486H72.6739C73.9328 95.2486 74.7905 94.3463 74.7905 93.0343C74.7905 90.6048 72.8101 88.9172 69.8281 88.9172H49.172C46.19 88.9172 44.2385 90.6048 44.2385 93.0343C44.2385 94.3463 45.0671 95.2486 46.3261 95.2486Z" fill="#FF2171"/>
</svg>

After

Width:  |  Height:  |  Size: 809 B

4
public/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg width="118" height="118" viewBox="0 0 118 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="59" cy="59" r="57.5" stroke="#FF2171" stroke-width="3"/>
<path d="M59.5118 62.9828C65.9124 62.9828 72.2055 62.6018 77.3811 61.8344C81.5487 61.2062 82.8644 58.1137 81.4561 53.7443L73.7855 29.5747C72.3743 25.1017 68.7741 23 63.8107 23H55.1892C50.2259 23 46.6257 25.1017 45.2145 29.5747L37.5439 53.7443C36.1356 58.1137 37.4513 61.2062 41.6189 61.8344C46.7945 62.6018 53.0875 62.9828 59.5118 62.9828ZM59.5118 58.0762C54.1754 58.0762 49.0021 57.7818 43.5558 57.1664C42.1608 57.0191 41.9873 56.1769 42.4268 54.7242L49.6984 31.4809C50.4479 29.0507 52.4363 27.8778 55.1892 27.8778H63.8107C66.5636 27.8778 68.552 29.0507 69.3253 31.4809L76.5732 54.7242C77.0127 56.1769 76.8392 57.0191 75.4443 57.1664C69.9977 57.7818 64.8246 58.0762 59.5118 58.0762ZM56.8364 60.82V92.9501H62.1687V60.82H56.8364ZM46.3949 94.9817H72.605C73.8519 94.9817 74.705 94.0842 74.705 92.7793C74.705 90.363 72.7351 88.6848 69.7747 88.6848H49.2253C46.2648 88.6848 44.3238 90.363 44.3238 92.7793C44.3238 94.0842 45.148 94.9817 46.3949 94.9817Z" fill="#FF2171"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -2,6 +2,7 @@
<ContextMenu />
<Modal />
<Notification />
<div id="drag-img" class="ellip2" style=""></div>
<section
id="app-grid"
:class="{
@@ -9,6 +10,7 @@
NoSideBorders: !xxl,
extendWidth: settings.extend_width && settings.can_extend_width,
}"
:style="{ maxWidth: `${content_height > 1080 ? '2220px' : '1720px'}` }"
>
<LeftSidebar />
<NavBar />
@@ -17,6 +19,7 @@
</div>
<RightSideBar v-if="settings.use_sidebar && xl" />
<BottomBar />
<!-- <BubbleManager /> -->
</section>
</template>
@@ -28,8 +31,7 @@ import { onMounted } from "vue";
import { useRouter } from "vue-router";
// @stores
import { content_width } from "@/stores/content-width";
import useContextStore from "@/stores/context";
import { content_width, content_height } from "@/stores/content-width";
import useModalStore from "@/stores/modal";
import useQStore from "@/stores/queue";
import useSettingsStore from "@/stores/settings";
@@ -45,10 +47,13 @@ import Modal from "@/components/modal.vue";
import Notification from "@/components/Notification.vue";
// @app-grid-components
import BottomBar from "@/components/BottomBar.vue";
import BottomBar from "@/components/BottomBar/BottomBar.vue";
import NavBar from "@/components/nav/NavBar.vue";
import RightSideBar from "@/components/RightSideBar/Main.vue";
import LeftSidebar from "./components/LeftSidebar/index.vue";
import { baseApiUrl } from "./config";
import { getRootDirs } from "./composables/fetch/settings/rootdirs";
// import BubbleManager from "./components/bubbles/BinManager.vue";
const queue = useQStore();
const router = useRouter();
@@ -56,24 +61,27 @@ const modal = useModalStore();
const settings = useSettingsStore();
queue.readQueue();
handleShortcuts(useQStore);
handleShortcuts(useQStore, useModalStore);
router.afterEach(() => {
(document.getElementById("acontent") as HTMLElement).scrollTo(0, 0);
});
onStartTyping((e) => {
if (e.ctrlKey) {
console.log("ctrl pressed");
}
const elem = document.getElementById("globalsearch") as HTMLInputElement;
elem.focus();
elem.value = "";
});
function updateContentElemSize({ width }: { width: number }) {
function updateContentElemSize({
width,
height,
}: {
width: number;
height: number;
}) {
content_width.value = width;
content_height.value = height;
}
function handleWelcomeModal() {
@@ -89,8 +97,25 @@ function handleWelcomeModal() {
}
}
function handleRootDirsPrompt() {
getRootDirs().then((dirs) => {
if (dirs.length === 0) {
modal.showRootDirsPromptModal();
} else {
settings.setRootDirs(dirs);
}
});
}
onMounted(() => {
handleWelcomeModal();
if (baseApiUrl.value === null) {
modal.showSetIPModal();
return;
}
handleRootDirsPrompt();
});
</script>

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M6.84421 24.8972H21.0295C23.5685 24.8972 24.8737 23.5919 24.8737 21.0914V6.82921C24.8737 4.32656 23.5685 3.02344 21.0295 3.02344H6.84421C4.31484 3.02344 3 4.31695 3 6.82921V21.0914C3 23.6016 4.31484 24.8972 6.84421 24.8972Z" fill="white"/>
<path d="M12.6617 19.7301C12.219 19.7301 11.8571 19.5387 11.5314 19.1137L8.65744 15.6204C8.45002 15.3523 8.34033 15.0818 8.34033 14.7848C8.34033 14.1879 8.81588 13.7037 9.42033 13.7037C9.7822 13.7037 10.0611 13.8281 10.3646 14.2148L12.6212 17.0827L17.4669 9.32416C17.7233 8.92409 18.0554 8.71338 18.4225 8.71338C18.9981 8.71338 19.5376 9.12401 19.5376 9.73807C19.5376 10.0085 19.4033 10.2949 19.232 10.5642L13.7474 19.1053C13.4802 19.5152 13.0982 19.7301 12.6617 19.7301Z" fill="blue"/>
</svg>

After

Width:  |  Height:  |  Size: 847 B

View File

@@ -1,3 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.81055 21.7666H21.3916C23.0879 21.7666 24.0723 20.7822 24.0723 18.9014V9.85742C24.0723 7.97656 23.0791 6.99219 21.1807 6.99219H13.0332C12.4004 6.99219 12.0225 6.85156 11.5303 6.44727L11.0381 6.04297C10.4141 5.5332 9.95703 5.36621 9.03418 5.36621H6.54688C4.89453 5.36621 3.91895 6.33301 3.91895 8.1875V18.9014C3.91895 20.791 4.91211 21.7666 6.81055 21.7666ZM5.66797 8.33691C5.66797 7.53711 6.11621 7.11523 6.89844 7.11523H8.56836C9.19238 7.11523 9.56152 7.26465 10.0625 7.66895L10.5547 8.08203C11.1699 8.57422 11.6445 8.75 12.5674 8.75H21.0664C21.875 8.75 22.3232 9.17188 22.3232 10.0156V10.5342H5.66797V8.33691ZM6.9248 20.0176C6.11621 20.0176 5.66797 19.5957 5.66797 18.7432V12.0723H22.3232V18.752C22.3232 19.5957 21.875 20.0176 21.0664 20.0176H6.9248Z" fill="#F2F2F2"/>
<svg width="28" height="28" viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M6.81055 21.7666H21.3916C23.0879 21.7666 24.0723 20.7822 24.0723 18.9014V9.85742C24.0723 7.97656 23.0791 6.99219 21.1807 6.99219H13.0332C12.4004 6.99219 12.0225 6.85156 11.5303 6.44727L11.0381 6.04297C10.4141 5.5332 9.95703 5.36621 9.03418 5.36621H6.54688C4.89453 5.36621 3.91895 6.33301 3.91895 8.1875V18.9014C3.91895 20.791 4.91211 21.7666 6.81055 21.7666ZM5.66797 8.33691C5.66797 7.53711 6.11621 7.11523 6.89844 7.11523H8.56836C9.19238 7.11523 9.56152 7.26465 10.0625 7.66895L10.5547 8.08203C11.1699 8.57422 11.6445 8.75 12.5674 8.75H21.0664C21.875 8.75 22.3232 9.17188 22.3232 10.0156V10.5342H5.66797V8.33691ZM6.9248 20.0176C6.11621 20.0176 5.66797 19.5957 5.66797 18.7432V12.0723H22.3232V18.752C22.3232 19.5957 21.875 20.0176 21.0664 20.0176H6.9248Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 885 B

After

Width:  |  Height:  |  Size: 898 B

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 24 24" width="24px" height="24px">
<g id="surface88165820">
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 5 3 C 3.90625 3 3 3.90625 3 5 L 3 9 C 3 10.09375 3.90625 11 5 11 L 9 11 C 10.09375 11 11 10.09375 11 9 L 11 5 C 11 3.90625 10.09375 3 9 3 Z M 15 3 C 13.90625 3 13 3.90625 13 5 L 13 9 C 13 10.09375 13.90625 11 15 11 L 19 11 C 20.09375 11 21 10.09375 21 9 L 21 5 C 21 3.90625 20.09375 3 19 3 Z M 5 5 L 9 5 L 9 9 L 5 9 Z M 15 5 L 19 5 L 19 9 L 15 9 Z M 5 13 C 3.90625 13 3 13.90625 3 15 L 3 19 C 3 20.09375 3.90625 21 5 21 L 9 21 C 10.09375 21 11 20.09375 11 19 L 11 15 C 11 13.90625 10.09375 13 9 13 Z M 15 13 C 13.90625 13 13 13.90625 13 15 L 13 19 C 13 20.09375 13.90625 21 15 21 L 19 21 C 20.09375 21 21 20.09375 21 19 L 21 15 C 21 13.90625 20.09375 13 19 13 Z M 5 15 L 9 15 L 9 19 L 5 19 Z M 15 15 L 19 15 L 19 19 L 15 19 Z M 15 15 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:0.8;" d="M 5 3 C 3.90625 3 3 3.90625 3 5 L 3 9 C 3 10.09375 3.90625 11 5 11 L 9 11 C 10.09375 11 11 10.09375 11 9 L 11 5 C 11 3.90625 10.09375 3 9 3 Z M 15 3 C 13.90625 3 13 3.90625 13 5 L 13 9 C 13 10.09375 13.90625 11 15 11 L 19 11 C 20.09375 11 21 10.09375 21 9 L 21 5 C 21 3.90625 20.09375 3 19 3 Z M 5 5 L 9 5 L 9 9 L 5 9 Z M 15 5 L 19 5 L 19 9 L 15 9 Z M 5 13 C 3.90625 13 3 13.90625 3 15 L 3 19 C 3 20.09375 3.90625 21 5 21 L 9 21 C 10.09375 21 11 20.09375 11 19 L 11 15 C 11 13.90625 10.09375 13 9 13 Z M 15 13 C 13.90625 13 13 13.90625 13 15 L 13 19 C 13 20.09375 13.90625 21 15 21 L 19 21 C 20.09375 21 21 20.09375 21 19 L 21 15 C 21 13.90625 20.09375 13 19 13 Z M 5 15 L 9 15 L 9 19 L 5 19 Z M 15 15 L 19 15 L 19 19 L 15 19 Z M 15 15 "/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,3 @@
<svg width="28" fill="currentColor" height="28" viewBox="0 0 28 28" xmlns="http://www.w3.org/2000/svg">
<path d="M5.09668 11.1846C5.09668 14.9375 8.25195 18.6465 13.1562 21.8105C13.4287 21.9863 13.7627 22.1621 13.9912 22.1621C14.2197 22.1621 14.5537 21.9863 14.8262 21.8105C19.7393 18.6465 22.8857 14.9375 22.8857 11.1846C22.8857 7.94141 20.6445 5.69141 17.7705 5.69141C16.0918 5.69141 14.7822 6.45605 13.9912 7.61621C13.2178 6.46484 11.8994 5.69141 10.2207 5.69141C7.33789 5.69141 5.09668 7.94141 5.09668 11.1846ZM6.90723 11.1758C6.90723 8.96094 8.36621 7.45801 10.3262 7.45801C11.9082 7.45801 12.7959 8.41602 13.3496 9.25098C13.5957 9.61133 13.7627 9.72559 13.9912 9.72559C14.2285 9.72559 14.3779 9.60254 14.6328 9.25098C15.2305 8.43359 16.083 7.45801 17.6562 7.45801C19.625 7.45801 21.084 8.96094 21.084 11.1758C21.084 14.2695 17.8672 17.6973 14.1582 20.1582C14.0791 20.2109 14.0264 20.2461 13.9912 20.2461C13.9561 20.2461 13.9033 20.2109 13.833 20.1582C10.124 17.6973 6.90723 14.2695 6.90723 11.1758Z" />
</svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="375" viewBox="0 0 375 375" height="375" version="1.0"><defs><clipPath id="a"><path d="M 73.445312 66 L 301 66 L 301 308.839844 L 73.445312 308.839844 Z M 73.445312 66"/></clipPath></defs><path fill="#FFF" d="M 187.5 0 C 83.945312 0 0 83.945312 0 187.5 C 0 291.054688 83.945312 375 187.5 375 C 291.054688 375 375 291.054688 375 187.5 C 375 83.945312 291.054688 0 187.5 0 Z M 187.5 0"/><g clip-path="url(#a)"><path fill="#4AD168" d="M 88.226562 152.753906 C 78.492188 138.632812 73.621094 118.523438 73.621094 92.417969 L 73.621094 66.613281 L 143.804688 66.613281 L 276.328125 211.664062 C 283.808594 219.96875 288.796875 226.96875 291.289062 232.664062 C 293.785156 238.359375 295.625 243.578125 296.8125 248.324219 C 299.425781 257.816406 300.730469 269.503906 300.730469 283.386719 L 300.730469 308.835938 L 232.507812 308.835938 L 88.761719 152.753906 Z M 223.601562 142.609375 C 223.601562 109.980469 228.648438 88.742188 238.742188 78.894531 C 244.085938 73.671875 251.035156 70.351562 259.585938 68.925781 C 268.253906 67.382812 278.761719 66.613281 291.113281 66.613281 L 300.730469 66.613281 L 300.730469 94.734375 C 300.730469 120.003906 294.082031 136.914062 280.78125 145.457031 C 271.28125 151.625 255.308594 154.710938 232.867188 154.710938 L 223.601562 154.710938 Z M 73.621094 281.074219 C 73.621094 255.800781 80.273438 238.832031 93.570312 230.171875 C 103.070312 224.121094 119.042969 221.09375 141.488281 221.09375 L 150.75 221.09375 L 150.75 233.199219 C 150.75 266.183594 145.703125 287.363281 135.609375 296.734375 C 128.722656 303.140625 118.6875 306.9375 105.507812 308.125 C 98.855469 308.601562 91.433594 308.835938 83.242188 308.835938 L 73.621094 308.835938 Z M 73.621094 281.074219"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="60" height="50" viewBox="0 0 60 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.6854 3.09975C56.5653 1.69314 54.4815 0 50.7566 0C46.146 0 40.2851 3.0737 35.1276 8.20522C34.3722 8.93457 33.6949 9.66392 33.0437 10.4193V5.20966C33.0437 4.38065 32.7144 3.58559 32.1282 2.99939C31.542 2.41319 30.7469 2.08386 29.9179 2.08386C29.0889 2.08386 28.2938 2.41319 27.7076 2.99939C27.1214 3.58559 26.7921 4.38065 26.7921 5.20966V10.4193C26.1409 9.66392 25.4636 8.93457 24.7082 8.20522C19.5507 3.0737 13.6898 0 9.07926 0C5.35435 0 3.27048 1.69314 2.15041 3.09975C-1.28797 7.50191 -0.0116033 15.082 1.88992 22.636C3.32258 28.4187 6.31814 31.1538 9.13135 32.4041C8.40766 34.0087 8.03468 35.7493 8.03733 37.5096C8.03992 40.0481 8.81518 42.5258 10.26 44.6131C11.7049 46.7004 13.7509 48.2984 16.1259 49.1947C18.501 50.091 21.0927 50.2432 23.5563 49.6308C26.0199 49.0185 28.2388 47.6708 29.9179 45.7669C31.8971 48.0002 34.6097 49.4521 37.5658 49.8601C40.5219 50.2681 43.5265 49.6054 46.0366 47.9918C48.5468 46.3781 50.3971 43.9198 51.2531 41.0611C52.1092 38.2024 51.9146 35.1318 50.7045 32.4041C53.5177 31.1538 56.5132 28.4187 57.9459 22.636C59.8474 15.082 61.1238 7.50191 57.6854 3.09975V3.09975ZM20.5405 43.7612C18.9172 43.8013 17.3419 43.2082 16.1482 42.1074C14.9544 41.0066 14.2358 39.4846 14.1444 37.8633C14.053 36.2421 14.596 34.6489 15.6585 33.4209C16.721 32.1929 18.2197 31.4266 19.8372 31.284C20.2452 31.2419 20.6408 31.1192 21.0011 30.9231C21.3613 30.727 21.6791 30.4613 21.9359 30.1415C22.1927 29.8217 22.3835 29.4541 22.4972 29.06C22.6109 28.6659 22.6453 28.2531 22.5983 27.8456C22.5066 27.0244 22.0934 26.2728 21.449 25.7554C20.8047 25.238 19.9817 24.9968 19.16 25.0845C17.12 25.2999 15.1671 26.0255 13.4814 27.1944C11.4497 27.0121 9.10531 25.7097 7.93313 21.1252C6.0316 13.4409 5.74507 8.67409 7.09959 6.9549C7.25588 6.74651 7.62055 6.25159 9.07926 6.25159C11.9446 6.25159 16.477 8.83038 20.3061 12.6334C24.1352 16.4365 26.7921 20.9949 26.7921 23.9644V37.5096C26.7853 39.1655 26.1244 40.7516 24.9535 41.9225C23.7826 43.0935 22.1964 43.7543 20.5405 43.7612ZM51.9027 21.1252C50.7305 25.7097 48.3862 27.0121 46.3544 27.1944C44.6687 26.0255 42.7158 25.2999 40.6759 25.0845C40.2538 25.0083 39.8206 25.0198 39.4032 25.1182C38.9858 25.2166 38.5932 25.3999 38.2496 25.6566C37.9061 25.9133 37.619 26.2379 37.4063 26.6103C37.1936 26.9827 37.0598 27.3948 37.0133 27.8212C36.9667 28.2475 37.0084 28.6788 37.1356 29.0883C37.2629 29.4978 37.4731 29.8768 37.7531 30.2016C38.0331 30.5264 38.377 30.7901 38.7633 30.9763C39.1496 31.1626 39.5701 31.2673 39.9986 31.284C41.1968 31.4197 42.3302 31.899 43.2622 32.6642C44.1942 33.4294 44.885 34.4478 45.2514 35.5966C45.6177 36.7455 45.644 37.9758 45.327 39.1393C45.01 40.3027 44.3633 41.3497 43.4648 42.154C42.5663 42.9582 41.4544 43.4855 40.2631 43.6722C39.0718 43.8588 37.8519 43.697 36.7504 43.2061C35.649 42.7152 34.713 41.9163 34.0553 40.9055C33.3977 39.8948 33.0463 38.7154 33.0437 37.5096V23.9644C33.0437 20.9949 35.5964 16.5407 39.5297 12.6334C43.463 8.72618 47.8912 6.25159 50.7566 6.25159C52.2153 6.25159 52.5799 6.74651 52.7362 6.9549C54.0907 8.67409 53.8042 13.4409 51.9027 21.1252Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,3 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5322 19.0332C13.9297 19.0332 15.2393 18.6113 16.3291 17.8906L20.1787 21.749C20.4336 21.9951 20.7588 22.1182 21.1104 22.1182C21.8398 22.1182 22.376 21.5469 22.376 20.8262C22.376 20.4922 22.2617 20.167 22.0156 19.9209L18.1924 16.0801C18.9834 14.9551 19.4492 13.5928 19.4492 12.1162C19.4492 8.31055 16.3379 5.19922 12.5322 5.19922C8.73535 5.19922 5.61523 8.31055 5.61523 12.1162C5.61523 15.9219 8.72656 19.0332 12.5322 19.0332ZM12.5322 17.1875C9.74609 17.1875 7.46094 14.9023 7.46094 12.1162C7.46094 9.33008 9.74609 7.04492 12.5322 7.04492C15.3184 7.04492 17.6035 9.33008 17.6035 12.1162C17.6035 14.9023 15.3184 17.1875 12.5322 17.1875Z" fill="#F2F2F2"/>
<path d="M12.5322 19.0332C13.9297 19.0332 15.2393 18.6113 16.3291 17.8906L20.1787 21.749C20.4336 21.9951 20.7588 22.1182 21.1104 22.1182C21.8398 22.1182 22.376 21.5469 22.376 20.8262C22.376 20.4922 22.2617 20.167 22.0156 19.9209L18.1924 16.0801C18.9834 14.9551 19.4492 13.5928 19.4492 12.1162C19.4492 8.31055 16.3379 5.19922 12.5322 5.19922C8.73535 5.19922 5.61523 8.31055 5.61523 12.1162C5.61523 15.9219 8.72656 19.0332 12.5322 19.0332ZM12.5322 17.1875C9.74609 17.1875 7.46094 14.9023 7.46094 12.1162C7.46094 9.33008 9.74609 7.04492 12.5322 7.04492C15.3184 7.04492 17.6035 9.33008 17.6035 12.1162C17.6035 14.9023 15.3184 17.1875 12.5322 17.1875Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 768 B

After

Width:  |  Height:  |  Size: 773 B

View File

@@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="white" xmlns="http://www.w3.org/2000/svg">
<path d="M6.84421 24.8972H21.0295C23.5685 24.8972 24.8737 23.5919 24.8737 21.0914V6.82921C24.8737 4.32656 23.5685 3.02344 21.0295 3.02344H6.84421C4.31484 3.02344 3 4.31695 3 6.82921V21.0914C3 23.6016 4.31484 24.8972 6.84421 24.8972ZM6.97733 22.6013C5.88772 22.6013 5.29592 22.0371 5.29592 20.8985V7.01999C5.29592 5.88139 5.88772 5.31936 6.97733 5.31936H20.8964C21.9764 5.31936 22.5778 5.88139 22.5778 7.01999V20.8985C22.5778 22.0371 21.9764 22.6013 20.8964 22.6013H6.97733Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B

View File

@@ -1,12 +1,12 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="28" height="28" viewBox="0 0 28 28" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_308_586)">
<path d="M23.1318 14.1377C25.5928 14.1377 27.6582 12.0811 27.6582 9.61133C27.6582 7.12402 25.6104 5.08496 23.1318 5.08496C20.6533 5.08496 18.6055 7.13281 18.6055 9.61133C18.6055 12.0986 20.6533 14.1377 23.1318 14.1377ZM6.81055 21.7666H21.3916C23.0879 21.7666 24.0723 20.7822 24.0723 18.9014V15.3066C23.5977 15.4121 22.8857 15.4209 22.3232 15.3066V18.752C22.3232 19.5957 21.875 20.0176 21.0664 20.0176H6.9248C6.11621 20.0176 5.66797 19.5957 5.66797 18.7432V12.0723H17.9551C17.709 11.5977 17.5244 11.0791 17.4365 10.5342H5.66797V8.33691C5.66797 7.53711 6.11621 7.11523 6.89844 7.11523H8.56836C9.19238 7.11523 9.56152 7.26465 10.0625 7.66895L10.5547 8.08203C11.1699 8.57422 11.6445 8.75 12.5674 8.75H17.4453C17.5156 8.12598 17.6562 7.58105 17.9551 6.99219H13.0332C12.4004 6.99219 12.0225 6.85156 11.5303 6.44727L11.0381 6.04297C10.4141 5.5332 9.95703 5.36621 9.03418 5.36621H6.54688C4.89453 5.36621 3.91895 6.33301 3.91895 8.1875V18.9014C3.91895 20.791 4.91211 21.7666 6.81055 21.7666ZM20.8643 10.2178C20.5391 10.2178 20.2578 9.92773 20.2578 9.61133C20.2578 9.28613 20.5391 9.00488 20.8643 9.00488H25.3994C25.7334 9.00488 26.0059 9.28613 26.0059 9.61133C26.0059 9.92773 25.7334 10.2178 25.3994 10.2178H20.8643Z" fill="#fff"/>
<path d="M26.7314 6.32392C28.484 8.0765 28.4777 10.9912 26.7376 12.7314C24.9788 14.4902 22.0827 14.4902 20.3239 12.7314C18.5713 10.9788 18.5713 8.08271 20.3301 6.32392C22.0827 4.57135 24.9788 4.57135 26.7314 6.32392Z" fill="#fff"/>
<path d="M21.4986 10.6246C21.2438 10.8794 21.2438 11.2896 21.5048 11.5506C21.7721 11.8178 22.1885 11.8241 22.4433 11.5693L23.9783 10.0342L24.662 9.27599V9.95962L24.6247 11.0783C24.6185 11.2461 24.6806 11.4201 24.8111 11.5382C25.0535 11.7806 25.4326 11.7992 25.6688 11.5506C25.793 11.4139 25.849 11.2585 25.8366 11.0721L25.7371 8.01438C25.7309 7.77201 25.6812 7.62285 25.5569 7.49856C25.4388 7.38047 25.2834 7.33697 25.0535 7.33076L21.9834 7.21889C21.8031 7.21268 21.6478 7.26861 21.5173 7.39912C21.2749 7.6415 21.2749 8.02681 21.5297 8.25676C21.6478 8.37484 21.8218 8.43699 21.9834 8.43699L23.1083 8.40592L23.7919 8.39349L23.0337 9.08955L21.4986 10.6246Z" fill="black"/>
<path d="M23.1318 14.1377C25.5928 14.1377 27.6582 12.0811 27.6582 9.61133C27.6582 7.12402 25.6104 5.08496 23.1318 5.08496C20.6533 5.08496 18.6055 7.13281 18.6055 9.61133C18.6055 12.0986 20.6533 14.1377 23.1318 14.1377ZM6.81055 21.7666H21.3916C23.0879 21.7666 24.0723 20.7822 24.0723 18.9014V15.3066C23.5977 15.4121 22.8857 15.4209 22.3232 15.3066V18.752C22.3232 19.5957 21.875 20.0176 21.0664 20.0176H6.9248C6.11621 20.0176 5.66797 19.5957 5.66797 18.7432V12.0723H17.9551C17.709 11.5977 17.5244 11.0791 17.4365 10.5342H5.66797V8.33691C5.66797 7.53711 6.11621 7.11523 6.89844 7.11523H8.56836C9.19238 7.11523 9.56152 7.26465 10.0625 7.66895L10.5547 8.08203C11.1699 8.57422 11.6445 8.75 12.5674 8.75H17.4453C17.5156 8.12598 17.6562 7.58105 17.9551 6.99219H13.0332C12.4004 6.99219 12.0225 6.85156 11.5303 6.44727L11.0381 6.04297C10.4141 5.5332 9.95703 5.36621 9.03418 5.36621H6.54688C4.89453 5.36621 3.91895 6.33301 3.91895 8.1875V18.9014C3.91895 20.791 4.91211 21.7666 6.81055 21.7666ZM20.8643 10.2178C20.5391 10.2178 20.2578 9.92773 20.2578 9.61133C20.2578 9.28613 20.5391 9.00488 20.8643 9.00488H25.3994C25.7334 9.00488 26.0059 9.28613 26.0059 9.61133C26.0059 9.92773 25.7334 10.2178 25.3994 10.2178H20.8643Z" fill="currentColor"/>
<path d="M26.7314 6.32392C28.484 8.0765 28.4777 10.9912 26.7376 12.7314C24.9788 14.4902 22.0827 14.4902 20.3239 12.7314C18.5713 10.9788 18.5713 8.08271 20.3301 6.32392C22.0827 4.57135 24.9788 4.57135 26.7314 6.32392Z" fill="green"/>
<path d="M21.4986 10.6246C21.2438 10.8794 21.2438 11.2896 21.5048 11.5506C21.7721 11.8178 22.1885 11.8241 22.4433 11.5693L23.9783 10.0342L24.662 9.27599V9.95962L24.6247 11.0783C24.6185 11.2461 24.6806 11.4201 24.8111 11.5382C25.0535 11.7806 25.4326 11.7992 25.6688 11.5506C25.793 11.4139 25.849 11.2585 25.8366 11.0721L25.7371 8.01438C25.7309 7.77201 25.6812 7.62285 25.5569 7.49856C25.4388 7.38047 25.2834 7.33697 25.0535 7.33076L21.9834 7.21889C21.8031 7.21268 21.6478 7.26861 21.5173 7.39912C21.2749 7.6415 21.2749 8.02681 21.5297 8.25676C21.6478 8.37484 21.8218 8.43699 21.9834 8.43699L23.1083 8.40592L23.7919 8.39349L23.0337 9.08955L21.4986 10.6246Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_308_586">
<rect width="28" height="28" fill="white"/>
<rect width="28" height="28" fill="currentColor"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -3,10 +3,11 @@
.scrollable {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
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

@@ -9,13 +9,11 @@ $g-border: solid 1px $gray5;
"l-sidebar content r-sidebar"
"bottombar bottombar bottombar";
// gap: 0 1.5rem;
height: 100%;
border: $g-border;
border-top: none;
border-bottom: none;
margin: 0 auto;
max-width: 1720px;
}
#acontent {
@@ -68,10 +66,10 @@ $g-border: solid 1px $gray5;
// ====== MODIFIERS =======
#app-grid.extendWidth {
max-width: 100%;
padding-right: 0;
border-left: none;
border-right: none;
max-width: 100% !important;
}
#app-grid.noSidebar {

View File

@@ -77,9 +77,13 @@ button {
align-items: center;
justify-content: center;
height: 2.25rem;
background: linear-gradient(70deg, $gray3, $gray2);
padding: 0 $small;
background-color: $gray4;
border: solid 1px $gray3;
font-weight: 700;
cursor: pointer;
svg {
transition: all 0.2s;
}
@@ -91,12 +95,14 @@ button {
}
&:hover {
background-image: linear-gradient($darkestblue, $darkblue);
background-color: $darkestblue;
border: solid 1px $darkblue;
}
}
.btn-active {
background-image: linear-gradient($darkestblue, $darkblue);
background-color: $darkestblue;
border: solid 1px $darkblue;
}
.btn-disabled {
@@ -146,5 +152,17 @@ button {
.load_disabled {
pointer-events: all;
background: $gray5 !important;
border-color: $gray5 !important;
opacity: 1;
}
#drag-img {
width: max-content;
max-width: 15rem;
background-color: $darkblue;
padding: $smaller $small;
border-radius: $smaller;
opacity: 1;
position: absolute;
left: -20rem;
}

View File

@@ -12,6 +12,7 @@
html {
cursor: default !important;
overflow: hidden;
& > * {
overflow: visible !important;
@@ -40,8 +41,4 @@ body {
width: 100%;
height: 100%;
}
a {
cursor: default !important;
}
}

View File

@@ -4,7 +4,7 @@ input[type="range"] {
width: calc(100% - 2px);
height: 0.3rem;
border-radius: 5px;
background: $gray4 linear-gradient(90deg, $darkblue, $darkestblue) no-repeat;
background: $gray4 linear-gradient(37deg, $pink, $pink) no-repeat;
background-size: 100% 100%;
&::-webkit-slider-thumb {
@@ -13,7 +13,7 @@ input[type="range"] {
height: 0;
width: 0.8rem;
border-radius: 50%;
background: $darkestblue;
background: $pink;
}
&::-moz-range-thumb {
@@ -21,7 +21,7 @@ input[type="range"] {
height: 0;
border-radius: 50%;
background: $darkestblue;
background: $pink;
border: none;
}
@@ -31,7 +31,7 @@ input[type="range"] {
height: 0;
width: 0.8rem;
border-radius: 50%;
background: $darkestblue;
background: $pink;
border: none;
}
}

View File

@@ -11,5 +11,6 @@
@font-face {
font-family: "SFCompactDisplay";
src: url("../sf-compact.woff") format("woff");
font-display: swap;
}

View File

@@ -3,7 +3,7 @@
<h3>
<span>{{ title }} </span>
<SeeAll
v-if="maxAbumCards <= albums.length"
v-if="maxAbumCards - 1 <= albums.length"
:route="route"
@click="
!favorites ? useArtistDiscographyStore().setPage(albumType) : null
@@ -11,7 +11,16 @@
/>
</h3>
<div class="cards">
<AlbumCard v-for="a in albums.slice(0, maxAbumCards)" :album="a" />
<AlbumCard
v-for="a in artist_page
? albums
.slice(0, maxAbumCards)
.sort((a, b) => parseInt(b.date) - parseInt(a.date))
: albums.slice(0, maxAbumCards)"
:album="a"
:show_date="show_date"
:artist_page="artist_page"
/>
</div>
</div>
</template>
@@ -31,17 +40,20 @@ defineProps<{
albumType?: discographyAlbumTypes;
favorites?: boolean;
route: string;
show_date?: boolean;
artist_page?: boolean;
}>();
</script>
<style lang="scss">
.artist-albums {
overflow: hidden;
max-height: 18rem;
h3 {
display: grid;
grid-template-columns: 1fr max-content;
align-items: center;
align-items: baseline;
padding: 0 $medium;
margin-bottom: $small;
}

View File

@@ -1,17 +1,24 @@
<template>
<div
class="genres-banner"
:class="{
nocontrast: album.info.colors ? isLight(album.info.colors[0]) : false,
:style="{
color: album.info.colors ? getTextColor(album.info.colors[0]) : '',
}"
>
<!-- :class="{
nocontrast: album.info.colors ? isLight(album.info.colors[0]) : false,
}" -->
<div class="rounded pad-sm">
{{ album.info.genres.length ? "Genres" : "No genres" }}
</div>
<div
v-for="genre in album.info.genres"
class="rounded pad-sm"
:style="{ backgroundColor: album.info.colors[0] }"
:style="{
backgroundColor: album.info.colors
? getBackgroundColor(album.info.colors[0])
: '',
}"
>
{{ genre }}
</div>
@@ -23,6 +30,8 @@ import { onMounted } from "vue";
import useAlbumStore from "@/stores/pages/album";
import { isLight } from "@/composables/colors/album";
import { getTextColor, getBackgroundColor } from "@/utils/colortools/shift";
const album = useAlbumStore();
onMounted(async () => {
@@ -33,10 +42,6 @@ onMounted(async () => {
</script>
<style lang="scss">
.genres-banner.nocontrast {
color: $black;
}
.genres-banner {
display: flex;
gap: 1rem;
@@ -52,6 +57,7 @@ onMounted(async () => {
text-align: center;
outline: solid 1px $gray;
padding: $small 1rem;
font-weight: 700;
&:first-child {
background-color: white;
@@ -61,8 +67,8 @@ onMounted(async () => {
}
&:hover {
background-color: $darkestblue !important;
outline-color: $darkestblue;
background-color: $pink !important;
outline-color: $pink;
color: $white;
}
}

View File

@@ -3,14 +3,19 @@
class="album-header-ambient rounded"
style="height: 100%; width: 100%"
:style="{
boxShadow: album.colors ? `0 .5rem 2rem ${album.colors[0]}` : '',
boxShadow: album.colors[0]
? `0 .5rem 2rem ${album.colors[0]}`
: '0 .5rem 2rem black',
}"
></div>
<div
class="a-header rounded"
ref="albumheaderthing"
:style="{
backgroundColor: album.colors ? album.colors[0] : '',
backgroundColor: album.colors[0]
? getBackgroundColor(album.colors[0])
: '',
height: `${heightLarge ? '24rem' : '18rem'}`,
}"
>
<div
@@ -21,12 +26,14 @@
</div>
<div
class="info"
:class="{ nocontrast: album.colors ? isLight(album.colors[0]) : false }"
:style="{ color: album.colors[0] ? getTextColor(album.colors[0]) : '' }"
>
<!-- :class="{ nocontrast: album.colors ? isLight(album.colors[0]) : false }" -->
<div class="album-info">
<div class="top">
<div v-auto-animate class="h">
<span v-if="album.is_soundtrack">Soundtrack</span>
<span v-else-if="album.is_live">Concert</span>
<span v-else-if="album.is_compilation">Compilation</span>
<span v-else-if="album.is_EP">EP</span>
<span v-else-if="album.is_single">Single</span>
@@ -43,7 +50,7 @@
:artists="album.albumartists"
:albumartists="''"
:small="true"
/>&nbsp; {{ album.date }} {{ album.count }}
/>&nbsp; {{ album.date }} {{ album.count }}
{{ album.count === 1 ? "Track" : "Tracks" }}
{{ formatSeconds(album.duration, true) }}
</div>
@@ -54,7 +61,18 @@
</div>
</div>
</div>
<div class="art" v-if="!albumHeaderSmall">
<Motion
class="art"
v-if="!isMedium && !isSmall"
:initial="{ opacity: 0, x: 10 }"
:animate="{
opacity: 1,
x: 0,
transition: {
delay: 0.1,
},
}"
>
<RouterLink
v-for="a in album.albumartists"
:to="{
@@ -64,39 +82,46 @@
>
<img
:src="imguri.artist.small + a.image"
class="shadow-lg circular"
class="circular"
loading="lazy"
:title="a.name"
:style="{ border: `solid 2px ${album.colors[0]}` }"
:style="{
border: `solid 4px ${
album.colors[0] ? getBackgroundColor(album.colors[0]) : ''
}`,
}"
/>
</RouterLink>
</div>
</Motion>
</div>
</div>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import { storeToRefs } from "pinia";
import { ref } from "vue";
import ArtistName from "@/components/shared/ArtistName.vue";
import { isLight } from "@/composables/colors/album";
import { favType, playSources } from "@/composables/enums";
import { paths } from "@/config";
import { Routes } from "@/router";
import { albumHeaderSmall } from "@/stores/content-width";
import useNavStore from "@/stores/nav";
import useAlbumStore from "@/stores/pages/album";
import {
albumHeaderSmall,
heightLarge,
isMedium,
isSmall,
} from "@/stores/content-width";
import { getBackgroundColor, getTextColor } from "@/utils/colortools/shift";
import { favType, playSources } from "@/composables/enums";
import { formatSeconds, useVisibility } from "@/utils";
import HeartSvg from "../shared/HeartSvg.vue";
import ArtistName from "@/components/shared/ArtistName.vue";
import favoriteHandler from "@/composables/favoriteHandler";
import { storeToRefs } from "pinia";
import HeartSvg from "../shared/HeartSvg.vue";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
// const props = defineProps<{
// album: Album;
// }>();
const albumheaderthing = ref<any>(null);
const imguri = paths.images;
const nav = useNavStore();
@@ -146,7 +171,8 @@ function handleFav() {
grid-template-columns: max-content 1fr;
gap: 1rem;
padding: 1rem;
height: $banner-height;
height: 24rem;
// height: $banner-height;
background-color: $black;
align-items: flex-end;
@@ -158,12 +184,14 @@ function handleFav() {
.big-img {
height: calc(100%);
width: 16rem;
height: 16rem;
display: flex;
align-items: flex-end;
img {
height: 16rem;
aspect-ratio: 1;
object-fit: cover;
}
}
@@ -178,6 +206,12 @@ function handleFav() {
.nocontrast {
color: $black;
.top {
.h {
color: $pink;
}
}
}
.info {
@@ -190,11 +224,17 @@ function handleFav() {
.art {
display: inline-flex;
gap: $small;
max-width: 10rem;
flex-wrap: wrap;
.shadow-inset {
height: max-content;
}
img {
height: 3rem;
background-color: $gray;
border: solid 2px $white;
margin-left: -1.5rem;
}
a {
@@ -202,7 +242,12 @@ function handleFav() {
}
a:hover {
transform: scale(1.4);
img {
z-index: 100;
border-color: $pink !important;
// margin-right: 1.5rem;
// border: solid 2px white !important;
}
}
}
@@ -216,25 +261,29 @@ function handleFav() {
.top {
.h {
font-size: 14px;
opacity: 0.5;
font-weight: 700;
}
.title {
font-size: 2.5rem;
font-weight: 600;
font-size: 2.75rem;
font-weight: 700;
width: fit-content;
cursor: text;
}
.artist {
font-size: 1.15rem;
background-color: red;
}
}
.bottom {
margin-top: $smaller;
.heart-button {
background-color: pink !important;
border-color: pink;
}
.stats {
font-weight: bold;
margin-bottom: 0.75rem;
@@ -252,8 +301,6 @@ function handleFav() {
font-size: 0.8rem;
display: flex;
flex-wrap: wrap;
// width: fit-content;
// cursor: text;
}
}
}

View File

@@ -8,13 +8,23 @@
: undefined,
}"
></div>
<div class="artist-page-header rounded no-scroll">
<div
class="artist-page-header rounded no-scroll"
:style="{
height: `${heightLarge ? '24rem' : '18rem'}`,
}"
>
<div
class="artist-info"
:class="{
nocontrast: artist.info.colors ? isLight(artist.info.colors[0]) : false,
:style="{
color: artist.info.colors[0]
? getTextColor(artist.info.colors[0])
: undefined,
}"
>
<!-- :class="{
nocontrast: artist.info.colors ? isLight(artist.info.colors[0]) : false,
}" -->
<section class="text">
<div class="card-title">Artist</div>
<div class="artist-name ellip2">{{ artist.info.name }}</div>
@@ -34,7 +44,12 @@
<HeartSvg @handleFav="handleFav" :state="artist.info.is_favorite" />
</div>
</div>
<div class="artist-img no-select">
<div
class="artist-img no-select"
:style="{
height: `${heightLarge ? '24rem' : '18rem'}`,
}"
>
<img :src="paths.images.artist.large + artist.info.image" />
</div>
<div
@@ -57,6 +72,9 @@ import formatSeconds from "@/utils/useFormatSeconds";
import { isLight } from "@/composables/colors/album";
import { favType, playSources } from "@/composables/enums";
import favoriteHandler from "@/composables/favoriteHandler";
import { heightLarge } from "@/stores/content-width";
import { getTextColor } from "@/utils/colortools/shift";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import HeartSvg from "@/components/shared/HeartSvg.vue";
@@ -81,16 +99,14 @@ function handleFav() {
opacity: 0.25;
margin-right: -1rem;
}
.artist-page-header {
height: 18rem;
display: grid;
grid-template-columns: 50% 50%;
grid-template-columns: 1fr minmax(min-content, 50%);
position: relative;
.artist-img {
// border: solid red;
height: 18rem;
width: 100%;
// width: 100%;
img {
height: 100%;
@@ -103,12 +119,7 @@ function handleFav() {
.gradient {
position: absolute;
background-image: linear-gradient(
to left,
transparent 10%,
$gray 50%,
$gray 100%
);
background-image: linear-gradient(to left, transparent 10%, $gray 50%, $gray 100%);
height: 100%;
width: 100%;
}
@@ -131,18 +142,19 @@ function handleFav() {
}
.card-title {
opacity: 0.5;
font-size: small;
font-weight: 700;
}
.artist-name {
font-size: 3rem;
font-size: 3.5rem;
font-weight: bold;
word-wrap: break-word;
}
.stats {
font-size: small;
font-weight: 700;
}
}
@@ -153,6 +165,11 @@ function handleFav() {
.buttons {
display: flex;
gap: $small;
.heart-button {
background-color: pink !important;
border-color: pink;
}
}
}
</style>

View File

@@ -10,6 +10,7 @@
:track="song"
:index="index + 1"
@playThis="playHandler(index)"
:source="source"
/>
</div>
<div class="error" v-if="!tracks.length">No tracks</div>
@@ -21,21 +22,24 @@ import SongItem from "../shared/SongItem.vue";
import { Track } from "@/interfaces";
import { isMedium, isSmall } from "@/stores/content-width";
import SeeAll from "../shared/SeeAll.vue";
import { dropSources } from "@/composables/enums";
defineProps<{
tracks: Track[];
route: string;
title: string;
playHandler: (index: number) => void;
source: dropSources;
}>();
</script>
<style lang="scss">
.artist-top-tracks {
margin-top: 2rem;
padding-top: 1rem;
.section-title {
margin-left: 0;
align-items: baseline;
}
.error {

View File

@@ -1,253 +0,0 @@
<template>
<div class="b-bar">
<div class="centered">
<div class="inner">
<div class="with-icons rounded-sm border">
<RouterLink
title="go to album"
:to="{
name: Routes.album,
params: {
hash: queue.currenttrack?.albumhash || ' ',
},
}"
>
<img
class="rounded-sm"
:src="paths.images.thumb.small + queue.currenttrack?.image"
alt=""
/>
</RouterLink>
<button>
<HeartSvg
:state="queue.currenttrack?.is_favorite"
@handleFav="handleFav"
/>
</button>
<button
class="repeat"
:class="{ 'repeat-disabled': settings.no_repeat }"
@click="settings.toggleRepeatMode"
:title="
settings.repeat_all
? 'Repeat all'
: settings.no_repeat
? 'No repeat'
: 'Repeat one'
"
>
<RepeatOneSvg v-if="settings.repeat_one" />
<RepeatAllSvg v-else />
</button>
</div>
<div class="info">
<div class="with-title">
<div class="time time-current">
<span>
{{ formatSeconds(queue.duration.current || 0) }}
</span>
</div>
<div class="tags">
<div v-tooltip class="title ellip">
{{ queue.currenttrack?.title || "Hello there" }}
</div>
<ArtistName
:artists="queue.currenttrack?.artist || []"
:albumartists="
queue.currenttrack?.albumartist || 'Welcome to Swing Music'
"
class="artist"
/>
</div>
<div class="time time-full">
<span>
{{
formatSeconds(
queue.currenttrack ? queue.currenttrack.duration : 0
)
}}
</span>
</div>
</div>
<Progress />
</div>
<div class="buttons rounded-sm border">
<HotKeys />
</div>
</div>
<div></div>
</div>
</div>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import { paths } from "@/config";
import { formatSeconds } from "@/utils";
import { favType } from "@/composables/enums";
import favoriteHandler from "@/composables/favoriteHandler";
import useQStore from "@/stores/queue";
import useSettingsStore from "@/stores/settings";
import HotKeys from "@/components/LeftSidebar/NP/HotKeys.vue";
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
import ArtistName from "@/components/shared/ArtistName.vue";
import HeartSvg from "./shared/HeartSvg.vue";
import RepeatAllSvg from "@/assets/icons/repeat.svg";
import RepeatOneSvg from "@/assets/icons/repeat-one.svg";
const queue = useQStore();
const settings = useSettingsStore();
function handleFav() {
favoriteHandler(
queue.currenttrack?.is_favorite,
favType.track,
queue.currenttrack?.trackhash || "",
() => null,
() => null
);
}
</script>
<style lang="scss">
.b-bar {
background-color: rgb(22, 22, 22);
display: grid;
align-items: center;
z-index: 1;
&:hover {
::-moz-range-thumb {
height: 0.8rem;
}
::-webkit-slider-thumb {
height: 0.8rem;
}
::-ms-thumb {
height: 0.8rem;
}
}
.centered {
display: grid;
align-items: center;
width: max-content;
padding: $small $medium;
margin: 0 auto;
.inner {
display: grid;
height: 3rem;
grid-template-columns: max-content 1fr max-content;
gap: 1rem;
align-items: center;
}
.with-icons {
background-color: rgba(255, 255, 255, 0.048);
display: grid;
gap: $small;
grid-template-columns: repeat(3, max-content);
align-items: center;
padding: 0 5px;
margin-top: -$smaller;
button {
height: 2rem;
width: 2rem;
background: transparent;
padding: 0;
border: none;
}
button.repeat {
svg {
transform: scale(0.75);
}
}
button.repeat.repeat-disabled {
opacity: 0.25;
}
}
img {
height: 2.75rem;
width: 100%;
aspect-ratio: 1;
object-fit: cover;
cursor: pointer;
margin-top: $smaller;
}
.info {
width: 30rem;
@media (max-width: 833px) {
width: 20rem !important;
}
.with-title {
display: grid;
grid-template-columns: max-content 1fr max-content;
align-items: flex-end;
gap: $smaller;
}
.time {
font-size: $medium;
height: fit-content;
width: 3rem;
span {
background-color: $gray3;
border-radius: $smaller;
padding: 0 $smaller;
}
}
.time-full {
text-align: end;
}
.tags {
font-size: small;
display: grid;
grid-template-rows: 1fr 1fr;
place-items: center;
.title {
font-weight: bold;
}
.artist {
opacity: 0.75;
margin-bottom: -$smaller;
font-size: $medium;
}
}
}
.buttons {
height: 100%;
margin-top: -$smaller;
background-color: rgba(255, 255, 255, 0.048);
display: grid;
place-items: center;
}
}
.right {
display: grid;
place-content: end;
}
}
</style>

View File

@@ -0,0 +1,196 @@
<template>
<div class="b-bar">
<LeftGroup @handleFav="handleFav" />
<div class="center">
<div class="with-time">
<div class="time time-current">
<span>
{{ formatSeconds(queue.duration.current || 0) }}
</span>
</div>
<div class="buttons rounded-sm border">
<HotKeys />
</div>
<div class="time time-full">
<span>
{{
formatSeconds(
queue.currenttrack ? queue.currenttrack.duration : 0
)
}}
</span>
</div>
</div>
<Progress />
</div>
<div class="right-group">
<HeartSvg
:state="queue.currenttrack?.is_favorite"
@handleFav="handleFav"
/>
<button
class="repeat"
:class="{ 'repeat-disabled': settings.no_repeat }"
@click="settings.toggleRepeatMode"
:title="
settings.repeat_all
? 'Repeat all'
: settings.no_repeat
? 'No repeat'
: 'Repeat one'
"
>
<RepeatOneSvg v-if="settings.repeat_one" />
<RepeatAllSvg v-else />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { formatSeconds } from "@/utils";
import { favType } from "@/composables/enums";
import favoriteHandler from "@/composables/favoriteHandler";
import useQStore from "@/stores/queue";
import useSettingsStore from "@/stores/settings";
import HotKeys from "@/components/LeftSidebar/NP/HotKeys.vue";
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
import HeartSvg from "../shared/HeartSvg.vue";
import RepeatAllSvg from "@/assets/icons/repeat.svg";
import RepeatOneSvg from "@/assets/icons/repeat-one.svg";
import LeftGroup from "./Left.vue";
const queue = useQStore();
const settings = useSettingsStore();
function handleFav() {
favoriteHandler(
queue.currenttrack?.is_favorite,
favType.track,
queue.currenttrack?.trackhash || "",
() => null,
() => null
);
}
</script>
<style lang="scss">
.b-bar {
background-color: rgb(22, 22, 22);
display: grid;
grid-template-columns: 1fr max-content 1fr;
align-items: center;
z-index: 1;
button {
height: 2rem;
width: 2rem;
background: transparent;
border-radius: $small;
&:hover {
border: solid 1px $gray3 !important;
background-color: $gray !important;
}
}
gap: 1rem;
&:hover {
::-moz-range-thumb {
height: 0.8rem;
}
::-webkit-slider-thumb {
height: 0.8rem;
}
::-ms-thumb {
height: 0.8rem;
}
}
.with-time {
display: grid;
grid-template-columns: max-content 1fr max-content;
align-items: flex-end;
height: 2rem;
button {
background: transparent;
}
}
.center {
display: grid;
align-items: center;
gap: $small;
margin-bottom: -$small;
width: 30rem;
@media (max-width: 1080px) {
width: 20rem !important;
}
.time {
font-size: $medium;
height: fit-content;
width: 3rem;
span {
background-color: $gray3;
border-radius: $smaller;
padding: 0 $smaller;
}
}
.time-full {
text-align: end;
}
}
// hotkeys
.buttons {
display: grid;
place-items: center;
scale: 1.2;
border: none;
}
}
.right-group {
display: grid;
justify-content: flex-end;
grid-template-columns: repeat(2, max-content);
align-items: center;
height: 4rem;
padding-right: 2rem;
button {
padding: 0;
height: 3rem;
width: 3rem;
border: none;
}
button.repeat {
background-color: transparent;
svg {
transform: scale(0.75);
}
}
button.repeat.repeat-disabled {
svg {
opacity: 0.25;
}
}
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="left-group" v-auto-animate>
<HeartSvg
v-if="settings.use_np_img"
:state="queue.currenttrack?.is_favorite"
@handleFav="emit('handleFav')"
/>
<RouterLink
v-else
title="go to album"
:to="{
name: Routes.album,
params: {
hash: queue.currenttrack?.albumhash || ' ',
},
}"
>
<img
class="rounded-sm"
:src="paths.images.thumb.small + queue.currenttrack?.image"
alt=""
/>
</RouterLink>
<div
class="track-info"
:style="{
color: getShift(colors.theme1, [0, -170]),
}"
>
<ArtistName
:artists="queue.currenttrack?.artist || []"
:albumartists="
queue.currenttrack?.albumartist || 'Welcome to Swing Music'
"
class="artist"
/>
<div v-tooltip class="title">
<span class="ellip">
{{ queue.currenttrack?.title || "Hello there" }}
</span>
<MasterFlag :bitrate="queue.currenttrack?.bitrate || 0" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Routes } from "@/router";
import { paths } from "@/config";
import ArtistName from "@/components/shared/ArtistName.vue";
import { getShift } from "@/utils/colortools/shift";
import useColorStore from "@/stores/colors";
import useSettingsStore from "@/stores/settings";
import useQStore from "@/stores/queue";
import HeartSvg from "../shared/HeartSvg.vue";
import MasterFlag from "../shared/MasterFlag.vue";
const queue = useQStore();
const settings = useSettingsStore();
const colors = useColorStore();
const emit = defineEmits<{
(e: "handleFav"): void;
}>();
</script>
<style lang="scss">
.left-group {
display: grid;
padding-left: 1rem;
grid-template-columns: max-content 1fr;
gap: $small;
align-items: center;
font-size: small;
a {
font-size: small;
}
img {
height: 3rem;
}
.heart-button {
height: 3rem;
width: 3rem;
border: solid 1px $gray4;
padding: 0;
}
.track-info {
.artistname {
font-size: 0.9rem;
opacity: 0.75;
font-weight: bold;
margin-bottom: 2px;
}
.title {
color: $white;
font-weight: bold;
display: flex;
align-items: center;
}
}
}
</style>

View File

@@ -43,7 +43,6 @@ context.$subscribe((mutation, state) => {
watcher = onClickOutside(
contextMenuRef,
(e) => {
e.stopImmediatePropagation();
context.hideContextMenu();
},
{
@@ -57,8 +56,6 @@ context.$subscribe((mutation, state) => {
if (watcher !== null) {
watcher();
}
// wat();
});
</script>
@@ -72,7 +69,7 @@ context.$subscribe((mutation, state) => {
transform: scale(0);
height: min-content;
padding: $small 0;
padding: $medium;
background: $context;
transform-origin: top left;
font-size: 0.875rem;

View File

@@ -121,8 +121,9 @@ function runChildAction(action: () => void) {
width: 100%;
display: flex;
align-items: center;
padding: 0.4rem 1rem;
padding: 0.4rem;
position: relative;
border-radius: $small;
.more {
height: 1.5rem;
@@ -134,14 +135,16 @@ function runChildAction(action: () => void) {
.children {
position: absolute;
width: 13rem;
width: 12rem;
background-color: $context;
transform: scale(0);
padding: $small 0;
padding: $medium;
border: solid 1px $gray;
.context-item {
padding: $small 1rem;
padding: 0.4rem;
}
.separator {
@@ -171,6 +174,7 @@ function runChildAction(action: () => void) {
.folder {
background-image: url("../../assets/icons/folder.svg");
filter: invert(100%);
}
.artist {

View File

@@ -0,0 +1,80 @@
<template>
<div class="breadcrumb-nav">
<div
class="path"
v-for="path in subPaths"
:key="path.path"
:class="{ inthisfolder: path.active }"
@click.prevent="emit('navigate', path.path)"
>
<a class="text">{{ path.name }}</a>
<!-- 👆 the a tag was misused to avoid rewriting css after moving this code to a component -->
</div>
</div>
</template>
<script setup lang="ts">
import { subPath } from "@/interfaces";
import { focusElemByClass } from "@/utils";
import { onUpdated } from "vue";
defineProps<{
subPaths: subPath[];
}>();
const emit = defineEmits<{
(e: "navigate", path: string): void;
}>();
onUpdated(() => {
focusElemByClass("inthisfolder");
});
</script>
<style lang="scss">
.breadcrumb-nav {
display: flex;
gap: $smaller;
&::-webkit-scrollbar {
display: none;
}
.path {
white-space: nowrap;
margin: auto 0;
cursor: pointer;
.text {
padding: $smaller;
border-radius: $smaller;
}
&::before {
content: "";
margin-right: $smaller;
color: $gray2;
font-size: 1rem;
}
// &:first-child {
// display: none;
// }
&:last-child {
padding-right: $smaller;
}
&:hover {
.text {
background-color: $gray;
}
}
}
.inthisfolder > .text {
background-color: $gray;
transition: all 0.5s;
}
}
</style>

View File

@@ -1,40 +1,99 @@
<template>
<router-link :to="{ name: Routes.folder, params: { path: folder.path } }">
<div class="f-item">
<!-- <div class="icon"> -->
<div
class="f-item"
@click="(e) => (folder_page ? null : handleClick(e))"
@mouseover="mouse_over = true"
@mouseleave="mouse_over = false"
:style="{
backgroundColor: is_checked ? '#234ece' : '',
}"
v-auto-animate
>
<div class="check" v-if="!folder_page">
<CheckSvg v-if="!is_checked && mouse_over" />
<CheckFilledSvg v-if="is_checked" />
</div>
<FolderSvg v-if="!folder.is_sym" />
<SymLinkSvg v-if="folder.is_sym" />
<!-- </div> -->
<div class="info">
<div class="f-item-text ellip">{{ folder.name }}</div>
<!-- <div class="separator no-border"></div> -->
<!-- <div class="f-item-count">{{ folder.has_tracks }} tracks</div> -->
<div class="f-count" v-if="folder.count">
{{ folder.count + ` File${folder.count == 1 ? "" : "s"}` }}
</div>
</div>
</div>
</router-link>
</template>
<script setup lang="ts">
import { Folder } from "@/interfaces";
import { ref } from "vue";
import { Routes } from "@/router";
import { Folder } from "@/interfaces";
import FolderSvg from "@/assets/icons/folder.svg";
import SymLinkSvg from "@/assets/icons/symlink.svg";
defineProps<{
import CheckFilledSvg from "@/assets/icons/check.filled.svg";
import CheckSvg from "@/assets/icons/square.svg";
const props = defineProps<{
folder: Folder;
is_checked?: boolean;
folder_page?: boolean;
}>();
const emit = defineEmits<{
(e: "navigate"): void;
(e: "check"): void;
}>();
const mouse_over = ref(false);
function handleClick(e: MouseEvent) {
e.preventDefault();
// check if the click was on the checkbox
if (e.target instanceof Element && e.target.closest(".check")) {
emit("check");
return;
}
if (!props.is_checked) {
emit("navigate");
}
}
</script>
<style lang="scss">
.f-container .f-item {
.f-item {
height: 5rem;
display: grid;
grid-template-columns: max-content 1fr;
align-items: center;
background-color: $gray;
transition: all 0.2s ease;
border-radius: 0.75rem;
border-radius: $medium;
position: relative;
.f-count {
font-size: $medium;
font-weight: 700;
color: $gray1;
margin-top: $smaller;
}
.check {
z-index: 10;
position: absolute;
top: $smaller;
right: -$smaller;
border: none;
outline: none;
scale: 0.75;
color: $darkblue;
}
@include phone-only {
height: 4rem;
@@ -42,6 +101,7 @@ defineProps<{
svg {
margin: 0 $small 0 1rem;
color: $gray1;
}
.f-item-text {
@@ -50,13 +110,6 @@ defineProps<{
&:hover {
background: $gray3;
// background: linear-gradient(to top right, #021b79, #0575e6);
background-size: 105% 105%;
background-position-x: -$small;
.f-item-count {
color: #ffffff;
}
}
}
</style>

View File

@@ -1,10 +1,14 @@
<template>
<div class="f-container rounded">
<div
class="f-container rounded-sm"
:class="{ 'list-mode': settings.folder_list_mode }"
>
<div id="f-items" class="rounded">
<FolderItem
v-for="folder in folders"
:key="JSON.stringify(folder)"
:key="folder.path"
:folder="folder"
:folder_page="true"
/>
</div>
</div>
@@ -13,34 +17,55 @@
<script setup lang="ts">
import { Folder } from "@/interfaces";
import FolderItem from "./FolderItem.vue";
import useSettingsStore from "@/stores/settings";
defineProps<{
folders: Folder[];
}>();
const settings = useSettingsStore();
</script>
<style lang="scss">
.f-container {
padding-bottom: 1.25rem;
}
#f-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 1.5rem;
padding-bottom: 1.25rem;
}
#f-items.list-mode {
.f-item:hover {
// cursor: pointer;
background-color: $gray5;
}
.f-container.list-mode > #f-items {
grid-template-columns: 1fr;
gap: 0;
// border: solid 1px $gray;
// padding: 1rem;
.f-item {
transition: none;
height: 3.25rem;
border-radius: $small;
background-color: transparent;
}
.f-item:hover {
background-color: $gray !important;
.f-count {
position: absolute;
right: 1.25rem;
bottom: 50%;
translate: 0 50%;
}
&:hover {
background-color: $gray !important;
}
}
}
</style>
<!-- TODO: ADD BUTTON TO TOGGLE LIST MODE -->
<!-- TODO: ADD BUTTON TO TOGGLE LIST MODE -->

View File

@@ -1,9 +1,12 @@
<template>
<div class="hotkeys rounded-sm no-scroll">
<div
class="hotkeys rounded-sm no-scroll"
>
<button @click.prevent="q.playPrev">
<PrevSvg />
</button>
<button v-auto-animate @click.prevent="q.playPause">
<button @click.prevent="q.playPause">
<PauseSvg v-if="q.playing" />
<PlaySvg v-else />
</button>
@@ -16,23 +19,29 @@
<script setup lang="ts">
import useQStore from "@/stores/queue";
import { default as NextSvg, default as PrevSvg } from "../../../assets/icons/next.svg";
import {
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";
const q = useQStore();
</script>
<style lang="scss">
.hotkeys {
display: grid;
grid-template-columns: 4rem 4rem 4rem;
grid-template-columns: 1fr 4rem 1fr;
gap: 1rem;
height: 100%;
button {
height: 100%;
padding: 0;
background: none;
border: none;
border-radius: 0;
&:hover {
@@ -45,5 +54,9 @@ const q = useQStore();
transform: rotate(180deg);
}
}
button:nth-child(2) {
width: 100%;
}
}
</style>

View File

@@ -1,43 +1,56 @@
<template>
<div class="sidebar-songcard">
<router-link
:to="{
name: 'AlbumView',
params: {
hash: track?.albumhash ? track.albumhash : ' ',
},
}"
>
<img
:src="imguri + track?.image"
alt=""
class="l-image rounded force-lm"
/>
</router-link>
<div id="bitrate" v-if="track?.bitrate" title="file type • bitrate">
{{ track.filetype }} {{ track.bitrate }}
<Motion
:key="q.currenttrack?.filepath"
:initial="{ opacity: 0, scale: 0.9 }"
:animate="{
opacity: 1,
scale: 1,
transition: { duration: 0.5, ease: 'ease-in' },
}"
:exit="{ opacity: 0, scale: 0.9 }"
>
<div class="sidebar-songcard rounded-sm" v-wave>
<router-link
:to="{
name: 'AlbumView',
params: {
hash: q.currenttrack?.albumhash ? q.currenttrack.albumhash : ' ',
},
}"
>
<img
:src="imguri + q.currenttrack?.image"
alt=""
class="l-image rounded-sm force-lm"
/>
</router-link>
<div
id="bitrate"
v-if="q.currenttrack?.bitrate"
title="file type • bitrate"
>
{{ q.currenttrack.filetype }} {{ q.currenttrack.bitrate }}
</div>
</div>
</div>
</Motion>
</template>
<script setup lang="ts">
import { ref } from "vue";
import useQueueStore from "@/stores/queue";
import { paths } from "@/config";
import { Track } from "@/interfaces";
defineProps<{
track: Track | undefined;
}>();
const imguri = paths.images.thumb.large;
const q = useQueueStore();
</script>
<style lang="scss">
.sidebar-songcard {
width: 100%;
position: relative;
width: 13rem;
height: 13rem;
img {
cursor: pointer;
width: 100%;

View File

@@ -9,21 +9,23 @@
query: menu.query && menu.query(),
}"
>
<div
v-if="menu.separator"
:class="{
separator: menu.separator,
}"
></div>
<div
class="nav-button"
:class="{ active: $route.name === menu.route_name }"
id="home-button"
v-else
>
<div class="in">
<component :is="menu.icon"></component>
<span>{{ menu.name }}</span>
<div v-wave class="rounded-sm">
<div
v-if="menu.separator"
:class="{
separator: menu.separator,
}"
></div>
<div
class="nav-button"
:class="{ active: $route.name === menu.route_name }"
id="home-button"
v-else
>
<div class="in">
<component :is="menu.icon"></component>
<span>{{ menu.name }}</span>
</div>
</div>
</div>
</router-link>
@@ -48,7 +50,7 @@ const menus = [
},
{
name: "folders",
route_name: "FolderView",
route_name: Routes.folder,
params: { path: "$home" },
icon: FolderSvg,
},
@@ -90,23 +92,35 @@ const menus = [
margin-top: 1rem;
.nav-button {
border-radius: $medium;
border-radius: $small;
display: flex;
align-items: flex-start;
justify-content: flex-start;
padding: $small 0;
position: relative;
font-size: 14px;
font-weight: 700;
&.active::before {
content: "";
content: " ";
position: absolute;
left: -$small;
top: $medium;
top: 50%;
transform: translateY(-50%);
opacity: 0.75;
height: 40%;
width: 4px;
background-color: $pink;
border-radius: 1rem;
}
&:hover {
background-color: $darkestblue;
&::before {
background-color: $white;
}
}
.in {

View File

@@ -1,15 +1,13 @@
<template>
<div class="now-playing-card t-center rounded">
<SongCard :track="queue.currenttrack" />
<SongCard />
</div>
</template>
<script setup lang="ts">
import useQStore from "../../stores/queue";
import SongCard from "./NP/SongCard.vue";
const queue = useQStore();
</script>
<style lang="scss">
.now-playing-card {
padding: 1rem;

View File

@@ -1,24 +1,42 @@
<template>
<div id="logo-container"
>
<router-link :to="{ name: 'Home' }">
<div id="logo"></div
></router-link>
</div>
<router-link class="swing-logo" :to="{ name: 'Home' }">
<div class="link">
<img src="/logo-fill.svg" alt="" />
<div>Swing <br />Music</div>
</div>
</router-link>
</template>
<style lang="scss">
@import "../assets/scss/mixins.scss";
.swing-logo {
background-color: $gray4;
padding: $medium;
display: flex;
align-items: center;
justify-content: space-between;
gap: $small;
cursor: pointer !important;
line-height: 1rem;
border-radius: 12px;
#logo-container {
overflow: hidden;
}
svg {
scale: 1.5;
}
#logo {
height: 4.5rem !important;
background-image: url(./../assets/images/logo.webp);
background-size: contain;
@include ximage;
border-radius: $medium;
.link {
display: flex;
align-items: center;
gap: $small;
}
span {
font-size: 1rem;
font-weight: 600;
color: $white;
}
img {
height: 2.25rem;
}
}
</style>

View File

@@ -29,19 +29,21 @@ defineProps<{
<style lang="scss">
.f-artists {
overflow: hidden;
max-height: 17rem;
h3 {
display: flex;
justify-content: space-between;
padding-left: $medium;
align-items: center;
align-items: baseline;
margin-bottom: $small;
}
.artist-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
gap: 10rem 0;
}
}
</style>

View File

@@ -1,83 +1,62 @@
<template>
<div
class="p-header image rounded no-scroll"
ref="playlistheader"
:style="[
{
backgroundImage: info.image ? `url(${imguri + info.image})` : undefined,
background: bg,
backgroundPosition: `center ${bannerPos}%`,
height: `${heightLarge ? '24rem' : '18rem'}`,
},
]"
:class="{ border: !info.image }"
:class="{ border: !info.images.length }"
>
<div class="gradient" v-if="info.image"></div>
<div class="carddd">
<div class="info">
<div class="btns">
<PlayBtnRect :source="playSources.playlist" :store="usePStore" />
</div>
<div class="duration">
{{ info.count + ` ${info.count == 1 ? "Track" : "Tracks"}` }}
{{ formatSeconds(info.duration, true) }}
</div>
<div class="title ellip">{{ info.name }}</div>
<div class="type">Playlist</div>
</div>
</div>
<div class="last-updated" :class="{ lightbg: !info.image }">
<span class="status"
>Last updated {{ info.last_updated }} &#160;|&#160;&#160;</span
>
<div class="edit" @click="editPlaylist">Edit&#160;&#160;</div>
|
<DeleteSvg class="edit" @click="deletePlaylist" />
</div>
<div class="gradient" v-if="!(info.image as string).endsWith('None')"></div>
<BannerImages />
<Info />
<LastUpdated />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import useNavStore from "@/stores/nav";
import usePStore from "@/stores/pages/playlist";
import useModalStore from "@/stores/modal";
import { heightLarge } from "@/stores/content-width";
import { paths } from "@/config";
import { playSources } from "@/composables/enums";
import { formatSeconds, useVisibility } from "@/utils";
import BannerImages from "./Header/BannerImages.vue";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import DeleteSvg from "@/assets/icons/delete.svg";
import Info from "./Header/Info.vue";
import { getBackgroundColor } from "@/utils/colortools/shift";
import LastUpdated from "./Header/LastUpdated.vue";
const modal = useModalStore();
const nav = useNavStore();
const playlist = usePStore();
const imguri = paths.images.playlist;
const playlistheader = ref<HTMLElement | null>(null);
const { info, bannerPos } = storeToRefs(playlist);
const bg = ref("");
useVisibility(playlistheader, nav.toggleShowPlay);
function getBg() {
if (playlist.info.has_image) {
return `url(${imguri + info.value.image})`;
}
function editPlaylist() {
modal.showEditPlaylistModal(info.value);
if (info.value.images.length > 2) {
return getBackgroundColor(info.value.images[2].color);
}
}
function deletePlaylist() {
modal.showDeletePlaylistModal(parseInt(playlist.info.id));
}
onMounted(() => (bg.value = getBg()));
</script>
<style lang="scss">
.p-header {
display: grid;
grid-template-columns: 1fr;
height: $banner-height;
position: relative;
background-color: $gray5;
background-color: $gray;
background-position: center 50%;
.gradient {
@@ -90,6 +69,24 @@ function deletePlaylist() {
opacity: 0.5;
}
.playlist-banner-images {
width: 21rem;
position: absolute;
right: 0;
top: -10rem;
rotate: -40deg;
display: flex;
flex-wrap: wrap;
gap: $medium;
transition: all 0.2s ease-in-out;
img {
height: 9rem;
transition: all 0.2s ease-in-out;
}
}
.last-updated {
position: absolute;
bottom: 1rem;
@@ -134,8 +131,9 @@ function deletePlaylist() {
.type {
font-size: small;
font-weight: bold;
color: rgba(255, 255, 255, 0.692);
font-weight: 700;
// color: rgb(218, 218, 218);
opacity: 0.85;
}
.title {
@@ -144,13 +142,22 @@ function deletePlaylist() {
cursor: text;
}
.info.is_light {
color: $gray5;
.type {
color: $pink;
// opacity: 0.5;
}
}
.duration {
font-size: 0.8rem;
color: $white;
padding: $smaller;
padding-left: 0;
font-weight: 900;
cursor: text;
opacity: 0.85;
}
.btns {
@@ -160,4 +167,8 @@ function deletePlaylist() {
}
}
}
.p-header.border {
border: solid 1px $gray4;
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="playlist-banner-images" v-if="!playlist.info.has_image">
<img
v-for="(img, index) in playlist.info.images"
:key="index"
:src="paths.images.thumb.large + img.image"
alt=""
class="rounded-sm"
:style="{
boxShadow: `0 0 1rem ${
!playlist.info.has_image && playlist.info.images.length > 2
? getShift(playlist.info.images[2].color, [40, 60])
: ''
}`,
}"
/>
</div>
</template>
<script setup lang="ts">
import usePStore from "@/stores/pages/playlist";
import { getShift } from "@/utils/colortools/shift";
import { paths } from "@/config";
const playlist = usePStore();
</script>

View File

@@ -0,0 +1,37 @@
<template>
<div class="carddd">
<div
class="info"
:style="{
color:
!playlist.info.has_image && playlist.info.images.length > 2
? getTextColor(playlist.info.images[2].color)
: '',
}"
>
<div class="btns">
<PlayBtnRect :source="playSources.playlist" :store="usePStore" />
</div>
<div class="duration">
{{
playlist.info.count +
` ${playlist.info.count == 1 ? "Track" : "Tracks"}`
}}
{{ formatSeconds(playlist.info.duration, true) }}
</div>
<div class="title ellip">{{ playlist.info.name }}</div>
<div class="type">Playlist</div>
</div>
</div>
</template>
<script setup lang="ts">
import { formatSeconds, useVisibility } from "@/utils";
import { playSources } from "@/composables/enums";
import { getTextColor } from "@/utils/colortools/shift";
import PlayBtnRect from "@/components/shared/PlayBtnRect.vue";
import usePStore from "@/stores/pages/playlist";
const playlist = usePStore();
</script>

View File

@@ -0,0 +1,27 @@
<template>
<div class="last-updated" :class="{ lightbg: !playlist.info.image }">
<span class="status"
>Last updated {{ playlist.info.last_updated }} &#160;|&#160;&#160;</span
>
<div class="edit" @click="editPlaylist">Edit&#160;&#160;</div>
|
<DeleteSvg class="edit" @click="deletePlaylist" />
</div>
</template>
<script setup lang="ts">
import DeleteSvg from "@/assets/icons/delete.svg";
import usePStore from "@/stores/pages/playlist";
import useModalStore from "@/stores/modal";
const playlist = usePStore();
const modal = useModalStore();
function editPlaylist() {
modal.showEditPlaylistModal(playlist.info);
}
function deletePlaylist() {
modal.showDeletePlaylistModal(parseInt(playlist.info.id));
}
</script>

View File

@@ -3,9 +3,19 @@
:to="{ name: 'PlaylistView', params: { pid: playlist.id } }"
class="p-card rounded no-scroll"
>
<div
class="image-grid rounded-sm no-scroll"
v-if="!playlist.has_image && playlist.images.length"
>
<img
v-for="img in playlist.images"
:src="paths.images.thumb.large + img"
/>
</div>
<img
v-else
:src="imguri + playlist.thumb"
class="rounded"
class="rounded-sm"
:class="{ border: !playlist.thumb }"
/>
<div class="overlay rounded">
@@ -35,11 +45,18 @@ const props = defineProps<{
grid-template-rows: 1fr max-content;
padding: 1rem;
gap: $small;
border: solid 1px $gray5;
.image-grid {
display: grid;
grid: repeat(2, 1fr) / repeat(2, 1fr);
}
&:hover {
transition: all .25s ease;
transition: all 0.25s ease;
background-color: $gray3;
background-blend-mode: screen;
border: solid 1px $gray3;
}
img {

View File

@@ -5,6 +5,12 @@
@mouseover="mouseover = true"
@mouseout="mouseover = false"
>
<NoItems
:flag="!queue.tracklist.length"
:title="'No songs in queue'"
:description="'When you start playing songs, they will appear here.'"
:icon="QueueSvg"
/>
<RecycleScroller
class="scroller"
id="queue-scrollable"
@@ -33,15 +39,21 @@ import useQStore from "@/stores/queue";
import TrackItem from "@/components/shared/TrackItem.vue";
import QueueActions from "./Queue/QueueActions.vue";
import NoItems from "../shared/NoItems.vue";
import QueueSvg from "@/assets/icons/queue.svg";
const itemHeight = 64;
const queue = useQStore();
const mouseover = ref(false);
const items = ref([
{ id: "1", text: "abc" },
{ id: "2", text: "def" },
]);
const scrollerItems = computed(() => {
return queue.tracklist.map((track) => ({
track,
id: track.filepath,
id: Math.random(),
}));
});
@@ -49,10 +61,12 @@ function playFromQueue(index: number) {
queue.play(index);
}
const show_above = 1; // the number of tracks to show above the current track
function scrollToCurrent() {
const elem = document.getElementById("queue-scrollable") as HTMLElement;
const top = (queue.currentindex - 1) * itemHeight;
const top = (queue.currentindex - show_above) * itemHeight;
elem.scroll({
top,
behavior: "smooth",

View File

@@ -1,15 +1,21 @@
<template>
<div class="queue-actions">
<div class="left">
<button class="clear-queue action" @click="queue.clearQueue">
<ClearSvg />
<span>Clear</span>
</button>
<button class="shuffle-queue action" @click="queue.shuffleQueue">
<button v-wave class="shuffle-queue action" @click="queue.shuffleQueue">
<ShuffleSvg />
<span>Shuffle</span>
</button>
</div>
<div class="right">
<button
v-wave
class="go-to-source action"
href="#"
@click="queue.clearQueue"
>
<ClearSvg />
</button>
</div>
</div>
</template>
@@ -36,11 +42,17 @@ const queue = useQueueStore();
}
.action {
padding-left: $smaller;
padding: 0 $medium;
svg {
transform: scale(0.8);
}
}
.right {
.go-to-source {
padding: 0 $smaller;
}
}
}
</style>

View File

@@ -1,33 +1,36 @@
<template>
<div class="artists-results" v-auto-animate>
<div
class="search-results-grid"
v-if="album_grid == true && search.albums.value.length"
>
<AlbumCard v-for="a in search.albums.value" :key="a.albumid" :album="a" />
<div>
<div
class="search-results-grid"
v-if="album_grid == true && search.albums.value.length"
>
<AlbumCard
v-for="a in search.albums.value"
:key="a.albumid"
:album="a"
/>
</div>
<div
class="search-results-grid"
v-else-if="!album_grid && search.artists.value.length"
>
<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>
<div
class="search-results-grid"
v-else-if="!album_grid && search.artists.value.length"
>
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
:artist="artist"
:alt="true"
/>
</div>
<div v-else class="t-center"><h5>💔 No results 💔</h5></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"
/>
<!-- <LoadMore
v-if="!album_grid"
:loader="search.loadArtists"
:can_load_more="search.artists.more"
/> -->
</div>
</template>
@@ -47,8 +50,10 @@ defineProps<{
<style lang="scss">
.artists-results {
height: 100%;
display: grid;
margin: 0 1rem;
grid-template-rows: 1fr max-content;
}
.search-results-grid {
@@ -57,5 +62,9 @@ defineProps<{
gap: 0.75rem;
min-height: 10rem;
padding-bottom: $small;
.album-card:hover {
border: solid 1px $gray3;
}
}
</style>

View File

@@ -23,7 +23,6 @@ defineProps<{
display: grid;
place-items: center;
margin-top: $small;
background-color: red;
button {
padding: 0 1rem !important;
@@ -31,6 +30,12 @@ defineProps<{
border-radius: 0;
height: 3rem;
background: $darkestblue;
border: none;
&:hover {
background: $darkblue;
border-color: $darkblue;
}
}
}
</style>

View File

@@ -6,7 +6,7 @@
:currentTab="currentTab"
:tabContent="true"
>
<Tab :name="currentTab" :isOnSearchPage="isOnSearchPage" />
<Tab :name="currentTab" />
</TabsWrapper>
</div>
</template>
@@ -19,9 +19,6 @@ import Tab from "./Tab.vue";
import TabsWrapper from "./TabsWrapper.vue";
const search = useSearchStore();
defineProps<{
isOnSearchPage?: boolean;
}>();
const tabs = ["tracks", "albums", "artists"];
@@ -42,14 +39,6 @@ function switchTab(tab: string) {
display: grid;
grid-template-rows: max-content 1fr;
.heading {
padding: $medium;
border-radius: $small;
margin-bottom: $small;
font-size: 2rem;
color: $white;
}
.input {
display: flex;
align-items: center;

View File

@@ -8,7 +8,6 @@ import TracksGrid from "./TracksGrid.vue";
const props = defineProps<{
name: string;
isOnSearchPage?: boolean;
}>();
function getComponent() {
@@ -16,9 +15,6 @@ function getComponent() {
case "tracks":
return {
component: TracksGrid,
props: {
isOnSearchPage: props.isOnSearchPage,
},
};
case "albums":
return {

View File

@@ -1,7 +1,7 @@
<template>
<div id="tracks-results" class="no-scroll">
<div id="tracks-results">
<div v-if="search.tracks.value.length">
<TrackComponent
<TrackItem
v-for="(track, index) in search.tracks.value"
:key="track.id"
:isCurrent="queue.currenttrackhash === track.trackhash"
@@ -14,7 +14,7 @@
:index="index + 1"
/>
</div>
<div v-else class="t-center"><h5>💔 No results 💔</h5></div>
<div v-else class="t-center"><h5>No tracks</h5></div>
<LoadMore
:loader="search.loadTracks"
:can_load_more="search.tracks.more"
@@ -24,9 +24,8 @@
</template>
<script setup lang="ts">
import { computed, onMounted } from "vue";
import { onMounted } from "vue";
import SongItem from "@/components/shared/SongItem.vue";
import TrackItem from "@/components/shared/TrackItem.vue";
import useQStore from "@/stores/queue";
import useSearchStore from "@/stores/search";
@@ -40,30 +39,18 @@ function updateQueue(index: number) {
queue.play(index);
}
const props = defineProps<{
isOnSearchPage?: boolean;
}>();
const TrackComponent = computed(() => {
if (props.isOnSearchPage) {
return SongItem;
}
return TrackItem;
});
let use_song_item: boolean = false;
if (props.isOnSearchPage) {
use_song_item = true;
}
onMounted(() => {
search.switchTab("tracks");
});
</script>
<style lang="scss">
#tracks-results {
height: 100%;
display: grid;
grid-template-rows: 1fr max-content;
}
#tracks-results .morexx {
margin-top: 1rem;
}

View File

@@ -68,7 +68,6 @@ function handleButton() {
.gsearch-input {
display: grid;
grid-template-columns: 1fr max-content;
border-radius: 3rem;
#ginner {
width: 100%;
@@ -80,6 +79,7 @@ function handleButton() {
button {
background: transparent;
border: none;
width: 3rem;
padding: 0;
border-radius: 3rem;

View File

@@ -0,0 +1,87 @@
<template>
<div class="list-items">
<div class="option-list-item" v-for="i in items" :key="i.title">
<div class="with-icon">
<component :is="icon" />
<div class="text ellip">
{{ i.title }}
</div>
</div>
<div class="icon" @click="i.action">
<span>{{ i.buttontext }}</span>
</div>
</div>
<div v-if="!items.length" class="option-list-item" style="opacity: 0.5">
Root directories not configured. Use the "modify" button above to
configure
</div>
</div>
</template>
<script setup lang="ts">
import FolderSvg from "../../../assets/icons/folder.svg";
const props = defineProps<{
items: {
title: string;
buttontext: string;
action: () => void;
}[];
icon: "folder";
}>();
function getIcon() {
switch (props.icon) {
case "folder":
return FolderSvg;
default:
return FolderSvg;
}
}
const icon = getIcon();
</script>
<style lang="scss">
.setting-item.is-list {
$color: $gray5;
.list-items {
background-color: $color;
border-radius: $small;
margin-top: 1rem;
overflow: hidden;
padding: 1rem 0;
}
.option-list-item {
padding: $small 1rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
.with-icon {
display: flex;
gap: $small;
align-items: center;
font-family: "SF Mono", monospace;
font-size: 0.9rem;
}
&:hover {
background-color: $gray4;
}
span {
color: white;
cursor: pointer;
background-color: $red;
padding: $smaller $small;
border-radius: 6px;
z-index: 20;
}
}
}
</style>

View File

@@ -20,7 +20,7 @@ defineProps<{
<style lang="scss">
.settingscontent {
width: 100%;
// max-width: 40rem;
margin: 0 auto;
padding-bottom: 2rem;
}
</style>

View File

@@ -6,9 +6,13 @@
</div>
<div class="setting rounded pad-lg">
<div
class="setting-item"
v-for="(setting, index) in group.settings"
:key="index"
:class="{ inactive: setting.inactive && setting.inactive() }"
:class="{
inactive: setting.inactive && setting.inactive(),
'is-list': setting.type === SettingType.list,
}"
>
<div
class="title ellip"
@@ -22,15 +26,27 @@
<Switch
v-if="setting.type == SettingType.binary"
@click="setting.action()"
:state="setting.source()"
:state="setting.state && setting.state()"
/>
<Select
v-if="setting.type === SettingType.select"
:options="setting.options"
:source="setting.source"
:source="setting.state !== null ? setting.state : () => ''"
:setterFn="setting.action"
/>
<button
v-if="setting.type === SettingType.button"
@click="setting.action"
>
{{ setting.button_text && setting.button_text() }}
</button>
</div>
<List
icon="folder"
:items="setting.state !== null ? setting.state() : []"
v-if="setting.type === SettingType.list"
/>
</div>
</div>
</div>
@@ -42,6 +58,7 @@ import { SettingGroup } from "@/interfaces/settings";
import Switch from "./Components/Switch.vue";
import Select from "./Components/Select.vue";
import List from "./Components/List.vue";
defineProps<{
group: SettingGroup;
@@ -75,6 +92,7 @@ defineProps<{
background-color: $gray;
display: grid;
gap: 1rem;
border: solid 1px $gray5;
.inactive {
opacity: 0.5;

View File

@@ -1,49 +1,18 @@
<template>
<div class="settingsnav">
<div class="buttongroup rounded-sm bg-primary">
<button
v-for="(group, index) in settingGroups"
:key="index"
@click="increment"
>
{{ group.title }}
</button>
</div>
<button @click="resetStorate">
&nbsp;&nbsp; Reset client &nbsp;&nbsp;
</button>
</div>
</template>
<script setup lang="ts">
import settingGroups from "@/settings";
import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const clicks = ref(0);
const increment = () => {
clicks.value++;
if (clicks.value === 3) {
localStorage.clear();
router.go(0);
}
const resetStorate = () => {
localStorage.clear();
router.go(0);
};
</script>
<style lang="scss">
.settingsnav {
.buttongroup {
display: grid;
grid-auto-flow: column;
width: max-content;
margin: 0 auto;
overflow: hidden;
button {
padding: 0 1rem;
border-radius: 0;
width: 5rem;
}
}
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div id="bubble-manager" class="rounded">
<DropArea />
</div>
</template>
<script setup lang="ts">
import DropArea from "./DropArea.vue";
</script>
<style lang="scss">
#bubble-manager {
position: fixed;
bottom: 6rem;
width: 15rem;
background-color: $gray;
padding: $small;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="bin-drop-area t-center rounded-sm">
<div class="bin-count circular">10</div>
DROP TRACKS HERE
<br />
</div>
</template>
<style lang="scss">
.bin-drop-area {
// background-color: $gray;
color: $pink;
padding: $medium;
border-radius: $small;
height: 5rem;
position: relative;
display: grid;
align-items: center;
justify-content: center;
font-weight: 700;
background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='10' ry='10' stroke='%23989696FF' stroke-width='4' stroke-dasharray='6%2c 14' stroke-dashoffset='100' stroke-linecap='square'/%3e%3c/svg%3e");
border-radius: 10px;
.bin-count {
position: absolute;
top: -$medium;
right: -$medium;
font-size: 14px;
width: 2rem;
aspect-ratio: 1;
font-weight: 600;
background-color: $pink;
color: white;
padding: $smaller;
margin-bottom: $small;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -1,9 +1,19 @@
<template>
<div class="modal" v-if="modal.visible">
<div class="bg" @click="modal.hideModal"></div>
<div class="m-content rounded">
<Motion
class="m-content rounded"
:style="{
maxWidth:
modal.component == modal.options.setRootDirs ? '56rem' : '30rem',
}"
:initial="{ opacity: 0, y: -20 }"
:animate="{ opacity: 1, y: 0 }"
>
<div class="heading">{{ modal.title }}</div>
<div class="close image" @click="modal.hideModal"></div>
<div class="close circular" @click="modal.hideModal">
<PlusSvg />
</div>
<NewPlaylist
v-if="modal.component == modal.options.newPlaylist"
:track="modal.props.track"
@@ -24,33 +34,43 @@
:confirmAction="deletePlaylist"
/>
</div>
</div>
<SetIP
v-if="modal.component == modal.options.SetIP"
@setTitle="setTitle"
/>
<SetRootDirs
v-if="modal.component == modal.options.setRootDirs"
@hideModal="hideModal"
/>
<RootDirsPrompt
v-if="modal.component == modal.options.rootDirsPrompt"
@hideModal="hideModal"
/>
</Motion>
</div>
</template>
<script setup lang="ts">
import useModalStore from "../stores/modal";
import NewPlaylist from "./modals/NewPlaylist.vue";
import UpdatePlaylist from "./modals/updatePlaylist.vue";
import WelcomeModal from "./WelcomeModal.vue";
import ConfirmModal from "./modals/ConfirmModal.vue";
import { deletePlaylist as delPlaylist } from "@/composables/fetch/playlists";
import { useRouter } from "vue-router";
import useModalStore from "../stores/modal";
import { deletePlaylist as delPlaylist } from "@/composables/fetch/playlists";
import SetIP from "./modals/SetIP.vue";
import WelcomeModal from "./WelcomeModal.vue";
import NewPlaylist from "./modals/NewPlaylist.vue";
import SetRootDirs from "./modals/SetRootDirs.vue";
import ConfirmModal from "./modals/ConfirmModal.vue";
import UpdatePlaylist from "./modals/updatePlaylist.vue";
import RootDirsPrompt from "./modals/RootDirsPrompt.vue";
import PlusSvg from "@/assets/icons/plus.svg";
const modal = useModalStore();
const router = useRouter();
/**
* Sets the modal title
* @param title
*/
function setTitle(title: string) {
modal.setTitle(title);
}
/**
* Handle the emit to hide the modal
*/
function hideModal() {
modal.hideModal();
}
@@ -70,7 +90,17 @@ function deletePlaylist() {
width: 100vw;
display: grid;
place-items: center;
// padding: 1rem;
input[type="search"] {
margin: $small 0;
border: 2px solid $gray3;
background-color: transparent;
color: #fff;
width: 100%;
padding: 0.5rem;
font-size: 1rem;
outline: none;
}
.bg {
position: absolute;
@@ -82,24 +112,32 @@ function deletePlaylist() {
}
.m-content {
width: 100%;
max-width: 30rem;
width: calc(100% - 4rem);
max-height: 40rem;
padding: 2rem;
position: relative;
background-color: $black;
border: solid 1px $gray5;
.close {
width: 2rem;
height: 2rem;
position: absolute;
top: 1rem;
right: 1rem;
background-image: url("../assets/icons/plus.svg");
top: 1.5rem;
right: 1.25rem;
transform: rotate(45deg);
svg {
background-color: $gray3;
border-radius: 1rem;
}
cursor: pointer;
opacity: 0.75;
&:hover {
transform: rotate(45deg) scale(1.2);
svg {
background-color: $red;
}
}
}
}

View File

@@ -1,15 +1,15 @@
<template>
<form @submit="create" class="new-p-form">
<form @submit="create" class="playlist-modal">
<label for="name">Playlist name</label>
<br />
<input
type="text"
type="search"
class="rounded-sm"
name="name"
id="modal-playlist-name-input"
/>
<br /><br>
<button type="submit" class="circular btn-active">Create</button>
<br /><br />
<button type="submit">Create</button>
</form>
</template>
@@ -67,24 +67,14 @@ function create(e: Event) {
</script>
<style lang="scss">
.new-p-form {
.playlist-modal {
grid-gap: 1rem;
margin-top: 1rem;
label {
font-size: 0.9rem;
color: $gray1;
}
input[type="text"] {
margin: $small 0;
border: 2px solid $gray3;
background-color: transparent;
color: #fff;
width: 100%;
padding: 0.5rem;
font-size: 1rem;
outline: none;
font-weight: 700;
}
.submit {
@@ -94,12 +84,10 @@ function create(e: Event) {
button {
margin: 0 auto;
width: 6rem;
transition: all .25s ease-out;
&:hover {
width: 10rem;
}
width: 8rem;
transition: all 0.25s ease-out;
background-color: $pink;
border: solid 1px $pink;
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="root-dirs-prompt">
<h3 class="t-center">Where do you want to look for music?</h3>
<div class="options-group">
<Motion
class="option"
v-for="option in options"
:key="option.id"
:class="{
active: option.active,
}"
@click="option.action()"
:initial="{ opacity: 0, y: -20 }"
:animate="{
opacity: 1,
y: 0,
transition: {
delay: option.delay,
easing: 'ease-out',
},
}"
>
<b>{{ option.title }}</b>
<div class="info">{{ option.info }}</div>
<div class="check" v-if="option.active"></div>
</Motion>
</div>
</div>
</template>
<script setup lang="ts">
import useModalStore from "@/stores/modal";
import useSettingsStore from "@/stores/settings";
import {
addRootDirs,
getRootDirs,
} from "@/composables/fetch/settings/rootdirs";
import { onMounted, ref } from "vue";
const settings = useSettingsStore();
const modal = useModalStore();
const emit = defineEmits<{
(e: "hideModal"): void;
}>();
const root_dirs: string[] = [];
const options = ref<any[]>([]);
onMounted(() => {
getRootDirs()
.then((res) => root_dirs.push(...res))
.then(() => {
settings.setRootDirs(root_dirs);
options.value = [
{
id: "$home",
title: "Home directory",
info: "Scan all folders in your home directory.",
active: settings.root_dirs[0] === "$home",
delay: 0,
action: () =>
addRootDirs(["$home"], [])
.then(() => settings.setRootDirs(["$home"]))
.then(() => emit("hideModal")),
},
{
id: "wtf",
title: "Customize root directories",
info: "Select folders to scan for music.",
delay: 0.1,
action: () => modal.showSetRootDirsModal(),
active:
settings.root_dirs[0] !== "$home" &&
settings.root_dirs[0] !== undefined,
},
];
});
});
</script>
<style lang="scss">
.root-dirs-prompt {
height: 14rem;
.option.active {
background-color: #3f6bac38;
}
.option {
padding: 1.25rem;
border-radius: $small;
position: relative;
background-color: #4e4b4b3f;
margin-top: 1.25rem;
cursor: pointer;
&:hover {
background-color: $darkblue;
}
.info {
margin-top: $smaller;
font-size: small;
}
.check {
position: absolute;
right: 1.25rem;
top: 0;
bottom: 0;
margin: auto;
height: max-content;
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
<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

@@ -0,0 +1,223 @@
<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>
<div class="set-root-dirs-browser">
<h4 v-if="no_more_dirs">
📂 No folders here. Use the "Select here" button to select this
location.
</h4>
<div class="scrollable">
<div class="content">
<FolderItem
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
"
/>
</div>
</div>
<div class="buttons">
<button class="btn-active select-here" @click="selectHere">
Select here
</button>
<button class="btn-active finish" @click="submitFolders">
Select checked ({{ getNewDirs().length }})
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, Ref, ref } from "vue";
import {
getFolders,
addRootDirs,
getRootDirs,
} from "@/composables/fetch/settings/rootdirs";
import { createSubPaths } from "@/utils";
import { Folder, subPath } from "@/interfaces";
import useSettingsStore from "@/stores/settings";
import FolderItem from "../FolderView/FolderItem.vue";
import BreadCrumbNav from "../FolderView/BreadCrumbNav.vue";
const settings = useSettingsStore();
const dirs: Ref<Folder[]> = ref([]);
const no_more_dirs = ref(false);
const selected = ref<string[]>([]);
const subPaths = ref<subPath[]>([]);
const prev_selected: string[] = [];
let oldpath = "";
let current = "";
const emit = defineEmits<{
(e: "hideModal"): void;
}>();
function fetchDirs(path: string) {
getFolders(path)
.then((folders) => {
dirs.value = folders;
no_more_dirs.value = folders.length == 0;
[oldpath, subPaths.value] = createSubPaths(path, oldpath);
})
.then(() => (current = path == "$home" ? "" : path));
}
function handleCheck(path: string) {
if (selected.value.includes(path)) {
selected.value = selected.value.filter((p) => p != path);
} else {
selected.value.push(path);
}
}
// All dir entries that were unchecked.
function getRemovedDirs() {
return prev_selected.filter((dir) => !selected.value.includes(dir));
}
// All dir entries that were newly checked.
function getNewDirs() {
return selected.value.filter((dir) => !prev_selected.includes(dir));
}
function submitFolders() {
const new_dirs = getNewDirs();
const removed_dirs = getRemovedDirs();
if (new_dirs.length == 0 && removed_dirs.length == 0) {
emit("hideModal");
return;
}
addRootDirs(new_dirs, removed_dirs)
.then((res) => settings.setRootDirs(res))
.then(() => emit("hideModal"));
}
function selectHere() {
if (current == "$root") return;
addRootDirs([current], [])
.then((res) => settings.setRootDirs(res))
.then(() => emit("hideModal"));
}
onMounted(() => {
fetchDirs("$root");
getRootDirs().then((_dirs) => {
selected.value = _dirs;
prev_selected.push(..._dirs);
});
});
</script>
<style lang="scss">
.bread-nav {
background-color: $gray4;
padding: $small;
width: max-content;
margin-bottom: 1rem;
position: absolute;
top: -3.25rem;
max-width: calc(100% - 2rem);
overflow-x: scroll;
scrollbar-width: none;
display: flex;
align-items: center;
&::-webkit-scrollbar {
display: none;
}
span {
cursor: pointer;
}
}
.bottom-text {
position: absolute;
font-size: small;
bottom: -1.25rem;
width: 100%;
opacity: 0.5;
}
.set-root-dirs-browser {
height: 27rem;
margin-right: -1rem;
display: grid;
grid-template-rows: 1fr max-content;
gap: 1.25rem;
.scrollable {
overflow-x: hidden;
height: 100%;
padding-right: 1rem;
padding: 1rem 1rem 1rem 0;
.content {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 1.5rem;
overflow: hidden;
}
overflow-y: scroll;
scrollbar-gutter: stable;
}
.buttons {
display: flex;
justify-content: space-between;
gap: $medium;
margin-right: 1rem;
margin-bottom: -$medium;
& > * {
cursor: pointer;
margin: 0;
}
button {
padding: 0 1rem;
}
button.select-here {
border: solid $darkestblue;
background: transparent;
&:hover {
background-color: $darkestblue;
}
}
}
.f-item {
background-color: $gray5;
&:hover {
background-color: $gray3;
}
}
}
</style>

View File

@@ -1,17 +1,20 @@
<template>
<form
@submit.prevent="update_playlist"
class="new-p-form"
id="playlist-update-modal"
class="playlist-modal"
enctype="multipart/form-data"
autocomplete="off"
>
<label for="name">Playlist name</label>
<br />
<input
type="text"
type="search"
class="rounded-sm"
name="name"
id="modal-playlist-name-input"
:value="props.playlist.name"
v-model="pname"
@keypress.enter="playlist.has_image && update_playlist"
/>
<br />
<input
@@ -23,7 +26,13 @@
@change="handleUpload"
ref="dropZoneRef"
/>
<div id="upload" class="boxed rounded-sm" @click="selectFiles">
<div
id="upload"
class="boxed rounded-sm"
@click="selectFiles"
tabindex="0"
@keydown.space.enter.stop="selectFiles"
>
<div>Click to upload cover image</div>
<div
id="update-pl-img-preview"
@@ -35,19 +44,24 @@
}"
/>
</div>
<div class="boxed banner-position-adjust rounded-sm">
<div class="t-center">Adjust image position</div>
<div
class="boxed banner-position-adjust rounded-sm"
v-if="playlist.has_image"
>
<div class="t-center">
Adjust image position - {{ pStore.bannerPos }}%
</div>
<div class="buttons">
<button @click.prevent="pStore.minusBannerPos">
<div @click.prevent="pStore.minusBannerPos">
<ExpandSvg />
</button>
<button @click.prevent="pStore.plusBannerPos">
</div>
<div @click.prevent="pStore.plusBannerPos">
<ExpandSvg />
</button>
</div>
</div>
</div>
<button class="circular btn-active">
<button>
{{ clicked ? "Updating" : "Update" }}
</button>
</form>
@@ -55,23 +69,20 @@
<script setup lang="ts">
import { onMounted, ref } from "vue";
// import { useDropZone } from "@vueuse/core";
import { updatePlaylist } from "@/composables/fetch/playlists";
import { paths } from "@/config";
import { Playlist } from "@/interfaces";
import usePStore from "@/stores/pages/playlist";
import { updatePlaylist } from "@/composables/fetch/playlists";
import ExpandSvg from "@/assets/icons/expand.svg";
const pStore = usePStore();
const bannerPos = ref(0);
const props = defineProps<{
playlist: Playlist;
}>();
// const dropZoneRef = ref<HTMLDivElement>();
// const { isOverDropZone } = useDropZone(dropZoneRef, handleDrop);
const pname = ref(props.playlist.name);
onMounted(() => {
(document.getElementById("modal-playlist-name-input") as HTMLElement).focus();
@@ -103,12 +114,6 @@ function handleUpload() {
}
}
// function handleDrop(files: File[] | null) {
// if (files) {
// handleFile(files[0]);
// }
// }
function handleFile(file: File) {
if (!file || !file.type.startsWith("image/")) {
return;
@@ -127,7 +132,9 @@ function handleFile(file: File) {
let clicked = ref(false);
function update_playlist(e: Event) {
const form = e.target as HTMLFormElement;
const form = document.getElementById(
"playlist-update-modal"
) as HTMLFormElement;
const formData = new FormData(form);
const name = formData.get("name") as string;
@@ -150,10 +157,17 @@ function update_playlist(e: Event) {
});
}
}
// Future TODO: Implement drag and drop for images here
</script>
<style lang="scss">
.new-p-form {
#playlist-update-modal {
input {
height: 3rem !important;
}
}
.playlist-modal {
.boxed {
border: solid 2px $gray3;
color: $gray1;
@@ -167,6 +181,7 @@ function update_playlist(e: Event) {
width: 100%;
padding: $small;
cursor: pointer;
margin-bottom: 1rem;
#update-pl-img-preview {
width: 4.5rem;
@@ -186,20 +201,31 @@ function update_playlist(e: Event) {
position: relative;
}
button {
aspect-ratio: 1;
height: 2rem;
width: 2rem;
padding: 0;
background: transparent;
}
.buttons {
display: grid;
gap: $small;
button:last-child {
transform: rotate(90deg);
}
div {
aspect-ratio: 1;
height: 2rem;
button:first-child {
transform: rotate(-90deg);
background-color: $gray4;
border-radius: $small;
display: grid;
place-content: center;
&:first-child {
rotate: -90deg;
}
&:last-child {
rotate: 90deg;
}
&:hover {
background-color: $pink;
}
}
}
}
}

View File

@@ -50,6 +50,7 @@ function switchView(album: albums) {
}
onClickOutside(dropOptionsRef, (e) => {
// @ts-ignore
e.stopImmediatePropagation();
hideDropDown();
});

View File

@@ -9,43 +9,45 @@
$router.push({ name: Routes.folder, params: { path: '$home' } })
"
></div>
<div class="paths">
<div
class="path"
v-for="path in subPaths.slice(1)"
:key="path.path"
:class="{ inthisfolder: path.active }"
>
<router-link
class="text"
:to="{ name: Routes.folder, params: { path: path.path } }"
>{{ path.name }}</router-link
>
</div>
</div>
<BreadCrumbNav :subPaths="subPaths" @navigate="navigate" />
</div>
</div>
<SearchInput :page="Routes.folder" />
<!-- <div>
</div> -->
<button
class="toggle-list-mode"
@click="settings.toggleFolderListMode"
title="toggle list mode for folders"
v-auto-animate
>
<GridSvg v-if="settings.folder_list_mode" />
<ListSvg v-else />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import SearchInput from "@/components/shared/NavSearchInput.vue";
import { subPath } from "@/interfaces";
import { useRouter } from "vue-router";
import { Routes } from "@/router";
import { focusElemByClass } from "@/utils";
import { onUpdated } from "vue";
import { subPath } from "@/interfaces";
import SearchInput from "@/components/shared/NavSearchInput.vue";
import BreadCrumbNav from "@/components/FolderView/BreadCrumbNav.vue";
import GridSvg from "@/assets/icons/grid.svg";
import ListSvg from "@/assets/icons/playlist.svg";
import useSettingsStore from "@/stores/settings";
const router = useRouter();
const settings = useSettingsStore();
defineProps<{
subPaths: subPath[];
}>();
onUpdated(() => {
focusElemByClass("inthisfolder");
});
function navigate(path: string) {
router.push({ name: Routes.folder, params: { path } });
}
</script>
<style lang="scss">
@@ -54,7 +56,7 @@ onUpdated(() => {
.folder {
display: grid;
grid-template-columns: 1fr max-content;
grid-template-columns: 1fr max-content max-content;
.fname-wrapper {
width: 100%;
@@ -84,51 +86,14 @@ onUpdated(() => {
background-size: 1.5rem;
margin-left: $smaller;
}
}
.paths {
display: flex;
gap: $smaller;
&::-webkit-scrollbar {
display: none;
}
.path {
white-space: nowrap;
margin: auto 0;
.text {
padding: $smaller;
border-radius: $smaller;
}
&::before {
content: "";
margin-right: $smaller;
color: $gray2;
font-size: 1rem;
}
&:first-child {
display: none;
}
&:last-child {
padding-right: $smaller;
}
&:hover {
.text {
background-color: $gray;
}
}
}
.inthisfolder > .text {
background-color: $gray;
transition: all 0.5s;
}
}
.toggle-list-mode {
margin-left: 1rem;
height: 2.25rem;
width: 2.25rem;
aspect-ratio: 1;
padding: 0;
}
}
}

View File

@@ -19,6 +19,7 @@ const { showNewPlaylistModal } = useModalStore();
.playlists-nav {
display: grid;
grid-template-columns: 1fr max-content;
align-items: center;
button {
padding-right: $small;

View File

@@ -33,6 +33,7 @@ const tabs = ["tracks", "albums", "artists"];
display: grid;
grid-template-columns: 1fr max-content;
gap: 1rem;
.buttons-area {
position: relative;

View File

@@ -14,5 +14,6 @@ import Nav from "@/components/SettingsView/Nav.vue";
.settings-nav {
display: grid;
grid-template-columns: 1fr max-content;
align-items: center;
}
</style>

View File

@@ -26,7 +26,22 @@
{{ album.title }}
</h4>
<div class="artist ellip" @click.prevent.stop="() => {}">
<template v-if="show_date"> {{ album.date }} </template>
<span
v-if="
show_date &&
artist_page &&
album.albumartists[0].artisthash != $route.params.hash
"
>
</span>
<RouterLink
v-if="
!show_date ||
(artist_page &&
album.albumartists[0].artisthash != $route.params.hash)
"
:to="{
name: Routes.artist,
params: { hash: album.albumartists[0].artisthash },
@@ -51,6 +66,8 @@ import useAlbumStore from "@/stores/pages/album";
const imguri = paths.images.thumb.large;
defineProps<{
album: Album;
show_date?: boolean;
artist_page?: boolean;
}>();
</script>
@@ -60,6 +77,7 @@ defineProps<{
gap: $small;
padding: $medium;
border-radius: 1rem;
border: solid 1px transparent;
.with-img {
position: relative;
@@ -100,15 +118,11 @@ defineProps<{
transform: translateY(1rem);
transition: all 0.25s;
width: $btn-width;
&:hover {
transition: all 0.25s;
background: $darkestblue;
}
}
&:hover {
background-color: $gray4;
border: solid 1px $gray5;
}
img {
@@ -131,6 +145,7 @@ defineProps<{
font-size: 0.8rem;
text-align: left;
opacity: 0.75;
font-weight: 700;
a {
cursor: pointer !important;

View File

@@ -33,6 +33,7 @@ defineProps<{
flex: 0 0 auto;
overflow: hidden;
position: relative;
border: solid 1px transparent;
border-radius: $medium;
display: grid;
@@ -45,6 +46,7 @@ defineProps<{
&:hover {
background-color: $gray4;
border: solid 1px $gray3;
}
.artist-image {

View File

@@ -22,7 +22,7 @@
}"
>{{ `${artist.name}` }}</RouterLink
>
{{ index === artists.length - 1 ? "" : ",&nbsp;" }}
<span>{{ index === artists.length - 1 ? "" : ",&nbsp;" }}</span>
</template>
</div>
</div>

View File

@@ -1,13 +1,27 @@
<template>
<button
v-wave
class="heart-button circular"
@click="!no_emit && emit('handleFav')"
:class="{
is_fav: state,
}"
>
<HeartFillSvg v-if="state" />
<HeartSvg v-else />
<Motion
:initial="{
opacity: 0,
}"
:animate="{
opacity: 1,
transition: {
delay: 0.25,
duration: 0.5,
},
}"
>
<HeartFillSvg v-if="state" />
<HeartSvg v-else />
</Motion>
</button>
</template>
@@ -16,8 +30,8 @@ import HeartSvg from "@/assets/icons/heart.svg";
import HeartFillSvg from "@/assets/icons/heart.fill.svg";
defineProps<{
state: boolean | undefined;
no_emit?: boolean;
state: Boolean | undefined;
no_emit?: Boolean;
}>();
const emit = defineEmits<{
@@ -36,6 +50,15 @@ $bg: rgb(250, 33, 33);
background: transparent;
color: rgb(250, 33, 33);
div {
height: max-content;
scale: 1;
svg {
height: 1.5rem;
}
}
&:hover {
background: transparent;
}

View File

@@ -1,7 +1,22 @@
<template>
<span class="master-flag" title="Master audio file">M</span>
<span
v-if="store.show_master_quality_flag && bitrate > 1024"
class="master-flag"
:title="'Master audio bitrate - ' + `${bitrate} Kbps`"
>M</span
>
</template>
<script setup lang="ts">
import useSettingsStore from "@/stores/settings";
defineProps<{
bitrate: number;
}>();
const store = useSettingsStore();
</script>
<style lang="scss">
.master-flag {
font-size: 10px;

View File

@@ -1,17 +1,18 @@
<template>
<div class="header-input-wrapper rounded-sm" :class="{ showInput: clicked }">
<button
class="search-btn circular"
class="search-btn"
id="page-search-trigger"
:class="{ 'btn-active': clicked }"
@click="handleFocus"
v-wave
>
<SearchSvg /> Search
<SearchSvg />
</button>
<input
class="header-input pad-sm"
class="header-input pad-sm rounded-sm"
:class="{ showInput: clicked }"
placeholder="Type to search"
:placeholder="currentEmoji"
v-model.trim="query"
id="page-search"
ref="inputRef"
@@ -20,8 +21,8 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import { storeToRefs } from "pinia";
import { ref } from "vue";
import useAlbumStore from "@/stores/pages/album";
import useFolderStore from "@/stores/pages/folder";
@@ -47,15 +48,15 @@ const props = defineProps<{
const inputRef = ref<HTMLElement>();
function handleFocus() {
// if input is not focused, focus it
// if input is focused, blur it
clicked.value = !clicked.value;
if (clicked.value) {
inputRef.value?.focus();
setRandomEmoji();
} else {
inputRef.value?.blur();
resetQuery();
}
}
function getRef() {
@@ -82,17 +83,27 @@ if (source) {
query = source[0];
resetQuery = source[1];
}
const currentEmoji = ref("");
function setRandomEmoji() {
const emojis = ["😹", "😺", "😻", "😸", "😼", "😽", "🙀", "😿", "😾"];
const randomIndex = Math.floor(Math.random() * emojis.length);
currentEmoji.value = emojis[randomIndex];
}
onMounted(() => setRandomEmoji());
</script>
<style lang="scss">
.header-input-wrapper {
&.showInput {
width: 21.5rem;
width: 19rem;
}
display: flex;
flex-direction: row-reverse;
width: 7rem;
width: 5rem;
gap: $small;
transition: all 0.25s;
transition-delay: 0.1s;
@@ -101,18 +112,18 @@ if (source) {
.header-input {
background-color: transparent;
border: none;
color: inherit;
font-size: 1rem;
color: white;
z-index: 200;
font-size: 1rem;
transition: all 0.25s $overshoot;
opacity: 0;
transform: translateY(-3.5rem);
border-radius: 3rem;
padding-left: 1rem;
outline: solid 1px $gray1;
padding-left: 1rem !important;
outline: none;
background-color: $gray3;
&:focus {
outline: solid $darkblue;
background-color: $darkestblue;
}
&.showInput {
@@ -125,6 +136,6 @@ if (source) {
.search-btn {
cursor: pointer;
padding: 0 $small;
padding-right: 1rem;
// padding-right: 1rem;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="nothing rounded" v-if="flag">
<div>
<component :is="icon" />
<h3>{{ title }}</h3>
<p>
{{ description }}
</p>
</div>
</div>
</template>
<script setup lang="ts">
defineProps<{
icon: any;
flag: boolean;
title: string;
description: string;
}>();
</script>
<style lang="scss">
.nothing {
height: 100%;
width: 25rem;
margin: 0 auto;
display: grid;
opacity: 0.5;
p {
word-break: break-word;
}
svg {
scale: 5;
margin-bottom: 2rem;
}
& > * {
margin: auto;
text-align: center;
}
}
</style>

View File

@@ -43,6 +43,7 @@ function handlePlay() {
aspect-ratio: 1;
padding: 0;
background: $black;
border: solid 1px $gray;
svg {
transition: none;

View File

@@ -1,5 +1,9 @@
<template>
<button class="playbtnrect shadow-sm" @click="usePlayFrom(source, useQStore, store)">
<button
v-wave
class="playbtnrect shadow-sm circular btn-active"
@click="usePlayFrom(source, useQStore, store)"
>
<playBtnSvg />
<div class="text">Play</div>
</button>
@@ -34,7 +38,7 @@ defineProps<{
justify-content: center;
transition: all 0.5s ease-in-out;
color: $white;
background: $darkestblue !important;
border-radius: 2rem;
background-color: $pink !important;
border: solid 1px $pink !important;
}
</style>

View File

@@ -33,7 +33,7 @@
<span class="title ellip">
{{ track.title }}
</span>
<MasterFlag v-if="track.bitrate > 1024" />
<MasterFlag :bitrate="track.bitrate" />
</div>
<div class="isSmallArtists" style="display: none">
<ArtistName
@@ -71,20 +71,21 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { onBeforeUnmount, ref, watch } from "vue";
import { paths } from "@/config";
import { Track } from "@/interfaces";
import { formatSeconds } from "@/utils";
import useQueueStore from "@/stores/queue";
import { showTrackContextMenu as showContext } from "@/composables/context";
import { favType } from "@/composables/enums";
import favoriteHandler from "@/composables/favoriteHandler";
import { showTrackContextMenu as showContext } from "@/composables/context";
import { paths } from "@/config";
import { Track } from "@/interfaces";
import useQueueStore from "@/stores/queue";
import { formatSeconds } from "@/utils";
import HeartSvg from "./HeartSvg.vue";
import ArtistName from "./ArtistName.vue";
import MasterFlag from "./MasterFlag.vue";
import OptionSvg from "@/assets/icons/more.svg";
import ArtistName from "./ArtistName.vue";
import HeartSvg from "./HeartSvg.vue";
import MasterFlag from "./MasterFlag.vue";
import { dropSources } from "@/composables/enums";
const imguri = paths.images.thumb.small;
const context_menu_showing = ref(false);
@@ -92,14 +93,25 @@ const queue = useQueueStore();
const props = defineProps<{
track: Track;
index: Number | String;
hide_album?: Boolean;
index: number | string;
hide_album?: boolean;
is_queue_track?: boolean;
droppable?: boolean;
is_last?: boolean;
source: dropSources;
}>();
const is_fav = ref(props.track.is_favorite);
const emit = defineEmits<{
(e: "playThis"): void;
(
e: "trackDropped",
source: dropSources,
track: Track,
newIndex: number,
oldIndex: number
): void;
}>();
function emitUpdate() {
@@ -111,6 +123,10 @@ function showMenu(e: MouseEvent) {
}
function isCurrent() {
if (props.is_queue_track) {
return queue.currentindex == parseInt(props.index as string) - 1;
}
return queue.currenttrackhash == props.track.trackhash;
}
@@ -150,6 +166,40 @@ onBeforeUnmount(() => {
gap: 1rem;
user-select: none;
padding-left: $small;
border: solid 1px transparent;
position: relative;
// .top-drop,
// .bottom-drop {
// display: flex;
// align-items: center;
// z-index: 20;
// position: absolute;
// height: 32px;
// width: 100%;
// left: 0;
// &.active {
// &::before {
// content: "";
// position: absolute;
// height: 1px;
// width: 100%;
// background-color: $red;
// left: 0;
// top: 50%;
// }
// }
// }
// .top-drop {
// top: -16px;
// }
// .bottom-drop {
// bottom: -16px;
// }
.song-title {
.with-flag {
@@ -166,6 +216,7 @@ onBeforeUnmount(() => {
&:hover {
background-color: $gray5;
border: solid 1px $gray4;
.index {
.text {
@@ -249,15 +300,10 @@ onBeforeUnmount(() => {
}
&:hover {
background-color: $darkestblue;
background-color: $gray3;
}
}
.context_menu_showing {
background-color: $gray4;
opacity: 1;
}
.flex {
position: relative;
align-items: center;
@@ -271,12 +317,14 @@ onBeforeUnmount(() => {
width: 3rem;
height: 3rem;
cursor: pointer;
z-index: 20;
}
.now-playing-track-indicator {
position: absolute;
left: $small;
top: $small;
z-index: 20;
}
}
@@ -299,6 +347,7 @@ onBeforeUnmount(() => {
}
.songlist-item.contexton {
background-color: $gray;
background-color: $gray4 !important;
border: solid 1px $gray3;
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<div
class="queue-view-virtual-scroller v-scroll-page"
:class="{ isSmall, isMedium }"
:class="{ isSmall, isMedium, is_queue }"
style="height: 100%"
>
<RecycleScroller
class="scroller"
id="songlist-scroller"
style="height: 100%"
:items="tracks.map((track) => ({ track, id: track.filepath }))"
:items="tracks.map((track) => ({ track, id: Math.random() }))"
:item-size="itemHeight"
key-field="id"
v-slot="{ item, index }"
@@ -16,7 +16,12 @@
<SongItem
:track="item.track"
:index="index + 1"
:is_queue_track="is_queue"
@playThis="handlePlay(index)"
:is_last="index == tracks.length - 1"
:droppable="false"
@trackDropped="dropHandler"
:source="source"
/>
</RecycleScroller>
</div>
@@ -24,13 +29,32 @@
<script setup lang="ts">
import SongItem from "@/components/shared/SongItem.vue";
import { isMedium, isSmall } from "@/stores/content-width";
import { dropSources } from "@/composables/enums";
import { Track } from "@/interfaces";
import { isMedium, isSmall } from "@/stores/content-width";
defineProps<{
tracks: Track[];
is_queue?: boolean;
handlePlay: (index: number) => void;
dropHandler: (
source: dropSources,
track: Track,
newIndex: number,
oldIndex: number
) => void;
source: dropSources;
}>();
const itemHeight = 64;
</script>
</script>
<style lang="scss">
.queue-view-virtual-scroller.is_queue {
.songlist-item.current {
background-color: $darkestblue !important;
border: none;
}
}
</style>

View File

@@ -10,6 +10,9 @@
{ contexton: context_on },
]"
@contextmenu.prevent="showMenu"
v-wave="{
duration: 0.35,
}"
>
<div class="album-art">
<img :src="paths.images.thumb.small + track.image" class="rounded-sm" />
@@ -20,8 +23,10 @@
></div>
</div>
<div class="tags">
<div class="title ellip" v-tooltip>
{{ track.title }}
<div class="title" v-tooltip>
<span class="ellip">
{{ track.title }}
</span>
</div>
<hr />
<div class="artist">
@@ -34,6 +39,7 @@
</div>
<div class="float-buttons flex">
<div
class="fav-icon"
:title="is_fav ? 'Add to favorites' : 'Remove from favorites'"
@click.stop="() => addToFav(track.trackhash)"
>
@@ -54,15 +60,17 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import DelSvg from "@/assets/icons/plus.svg";
import { showTrackContextMenu as showContext } from "@/composables/context";
import { paths } from "@/config";
import { Track } from "@/interfaces";
import useQueueStore from "@/stores/queue";
import ArtistName from "./ArtistName.vue";
import HeartSvg from "./HeartSvg.vue";
import favoriteHandler from "@/composables/favoriteHandler";
import { favType } from "@/composables/enums";
import favoriteHandler from "@/composables/favoriteHandler";
import { showTrackContextMenu as showContext } from "@/composables/context";
import HeartSvg from "./HeartSvg.vue";
import MasterFlag from "./MasterFlag.vue";
import ArtistName from "./ArtistName.vue";
import DelSvg from "@/assets/icons/plus.svg";
const props = defineProps<{
track: Track;
@@ -107,7 +115,7 @@ watch(
</script>
<style lang="scss">
.currentInQueue {
.track-item.currentInQueue {
background-color: $gray4;
}
@@ -130,19 +138,31 @@ watch(
.float-buttons {
opacity: 0;
gap: $small;
.heart-button {
width: 2rem;
height: 2rem;
padding: 0;
border: none;
transition: all 0.25s ease;
transform: scale(1) translateY(-1rem);
&:hover {
background-color: pink;
}
}
.remove-track {
margin-top: $smaller;
transition: all 0.25s ease;
transform: scale(1) translateY(1rem) rotate(45deg);
transform: rotate(45deg);
height: 2rem;
width: 2rem;
display: grid;
place-items: center;
&:hover {
border-radius: 1rem;
background-color: $red;
}
}
&:hover {
@@ -155,12 +175,8 @@ watch(
opacity: 1;
}
.heart-button {
transform: scale(1) translateY(0);
}
.remove-track {
transform: scale(1) translateY(0) rotate(45deg);
transform: translateY(0) rotate(45deg);
}
background-color: $gray5;
@@ -192,6 +208,7 @@ watch(
.artist {
opacity: 0.67;
width: fit-content;
font-weight: bold;
}
}
</style>

View File

@@ -7,10 +7,14 @@
export function isLight(rgb: string): boolean {
if (rgb == null || undefined) return false;
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
try {
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 165;
return brightness > 165;
} catch (error) {
return false;
}
}
interface BtnColor {

View File

@@ -8,6 +8,9 @@ import { ContextSrc } from "./enums";
import { Track } from "@/interfaces";
import trackContext from "@/contexts/track_context";
let prev_track = "";
let prev_watcher = () => {};
/**
* Handles showing the context menu for a track component.
* @param e The MouseEvent for positioning the context menu
@@ -20,16 +23,17 @@ export const showTrackContextMenu = (
flag: Ref<boolean>
) => {
const menu = useContextStore();
const options = () => trackContext(track, useModalStore, useQueueStore);
menu.showContextMenu(e, options, ContextSrc.Track);
flag.value = true;
// watch for context menu visibility and reset flag
menu.$subscribe((mutation, state) => {
if (!state.visible) {
flag.value = false;
}
});
if (prev_track !== track.filepath) {
prev_track = track.filepath || "";
prev_watcher();
// watch for context menu visibility and reset flag
prev_watcher = menu.$subscribe((mutation, state) => {
flag.value = state.visible;
});
}
};

View File

@@ -52,6 +52,7 @@ export enum discographyAlbumTypes {
singles = "Singles",
eps = "EPs",
appearances = "Appearances",
compilations = "Compilations",
}
export enum favType {
@@ -59,3 +60,13 @@ export enum favType {
album = "album",
track = "track",
}
export enum dropSources {
queue = "queue",
playlist = "playlist",
folder = "folder",
album = "album",
search = "search",
artist = "artist",
favorite = "favorite",
}

View File

@@ -14,6 +14,8 @@ export default async function favoriteHandler(
setter: (x?: unknown) => void,
remover: (x?: unknown) => void
) {
if (itemhash == "") return;
const queue = useQueueStore();
const is_current =
type === favType.track && itemhash === queue.currenttrackhash;

View File

@@ -27,11 +27,12 @@ const getArtistData = async (hash: string, limit: number = 5) => {
const getArtistAlbums = async (hash: string, limit = 6, all = false) => {
interface ArtistAlbums {
artistname: string;
albums: Album[];
eps: Album[];
singles: Album[];
appearances: Album[];
artistname: string;
compilations: Album[];
}
const { data, error } = await useAxios({

View File

@@ -0,0 +1,14 @@
import useAxios from "./useAxios";
import { paths } from "@/config";
export async function fetchAlbumColor(
albumhash: string | undefined
): Promise<string> {
const { data } = await useAxios({
url: paths.api.colors.album + `/${albumhash}`,
get: true,
});
return data.color;
}

View File

@@ -55,9 +55,9 @@ export async function createNewPlaylist(playlist_name: string, track?: Track) {
* Fetches all playlists from the server.
* @returns {Promise<Playlist[]>} A promise that resolves to an array of playlists.
*/
export async function getAllPlaylists(): Promise<Playlist[]> {
export async function getAllPlaylists(no_images = false): Promise<Playlist[]> {
const { data, error } = await useAxios({
url: allPlaylistsUrl,
url: allPlaylistsUrl + (no_images ? "?no_images=true" : ""),
get: true,
});

View File

@@ -0,0 +1,66 @@
import useAxios from "../useAxios";
import { paths } from "@/config";
import { NotifType, useNotifStore } from "@/stores/notification";
import { Folder } from "@/interfaces";
const { add_root_dir, get_root_dirs, remove_root_dir } = paths.api.settings;
export async function getRootDirs() {
const { data, error } = await useAxios({
url: get_root_dirs,
get: true,
});
if (error) {
return [];
}
return data.dirs as string[];
}
export async function addRootDirs(new_dirs: string[], removed: string[]) {
const { error, data } = await useAxios({
url: add_root_dir,
props: { new_dirs, removed },
});
if (error) {
useNotifStore().showNotification("Error adding root dirs", NotifType.Error);
return [];
}
useNotifStore().showNotification(
"Root directories configured",
NotifType.Success
);
return data.root_dirs as string[];
}
export async function removeRootDirs(dirs: string[]) {
const { error } = await useAxios({
url: remove_root_dir,
props: { dirs },
});
if (error) {
useNotifStore().showNotification(
"Error removing root dirs",
NotifType.Error
);
}
}
export async function getFolders(folder: string = "$home") {
const { data, error } = await useAxios({
url: paths.api.dir_browser,
props: {
folder,
},
});
if (error) {
return [];
}
return data.folders as Folder[];
}

View File

@@ -1,4 +1,5 @@
import useQStore from "@/stores/queue";
import useModalStore from "@/stores/modal";
let key_down_fired = false;
@@ -11,8 +12,10 @@ function focusPageSearchBox() {
}
}
export default function (queue: typeof useQStore) {
export default function (queue: typeof useQStore, modal: typeof useModalStore) {
const q = queue();
const m = modal();
window.addEventListener("keydown", (e: KeyboardEvent) => {
const target = e.target as HTMLElement;
if (e.altKey) return;
@@ -77,6 +80,14 @@ export default function (queue: typeof useQStore) {
key_down_fired = true;
}
}
case "Escape": {
if (!key_down_fired) {
if (m.visible) {
m.hideModal();
}
}
}
}
});
}

View File

@@ -1,8 +1,24 @@
import { useStorage } from "@vueuse/core";
const development = import.meta.env.DEV;
const dev_url = "http://localhost:1970";
const url = development ? dev_url : "";
const baseApiUrl = development ? dev_url : dev_url;
const baseImgUrl = baseApiUrl + "/img";
export const baseApiUrl = useStorage("baseApiUrl", url, sessionStorage);
const hostname = "swingmusic.netlify.app";
if (window.location.hostname === hostname && baseApiUrl.value === "") {
// is running on netlify and baseApiUrl is not set
baseApiUrl.value = null;
}
const baseImgUrl = baseApiUrl.value + "/img";
export function setBaseApiUrl(url: string) {
baseApiUrl.value = url;
location.reload();
}
const imageRoutes = {
thumb: {
@@ -17,16 +33,16 @@ const imageRoutes = {
raw: "/raw/",
};
const paths = {
export const paths = {
api: {
album: baseApiUrl + "/album",
favorite: baseApiUrl + "/favorite",
favorites: baseApiUrl + "/favorites",
favAlbums: baseApiUrl + "/albums/favorite",
favTracks: baseApiUrl + "/tracks/favorite",
favArtists: baseApiUrl + "/artists/favorite",
isFavorite: baseApiUrl + "/favorites/check",
artist: baseApiUrl + "/artist",
album: baseApiUrl.value + "/album",
favorite: baseApiUrl.value + "/favorite",
favorites: baseApiUrl.value + "/favorites",
favAlbums: baseApiUrl.value + "/albums/favorite",
favTracks: baseApiUrl.value + "/tracks/favorite",
favArtists: baseApiUrl.value + "/artists/favorite",
isFavorite: baseApiUrl.value + "/favorites/check",
artist: baseApiUrl.value + "/artist",
get addFavorite() {
return this.favorite + "/add";
},
@@ -42,9 +58,10 @@ const paths = {
get albumsByArtistUrl() {
return this.album + "/from-artist";
},
folder: baseApiUrl + "/folder",
folder: baseApiUrl.value + "/folder",
dir_browser: baseApiUrl.value + "/folder/dir-browser",
playlist: {
base: baseApiUrl + "/playlist",
base: baseApiUrl.value + "/playlist",
get new() {
return this.base + "/new";
},
@@ -56,7 +73,7 @@ const paths = {
},
},
search: {
base: baseApiUrl + "/search",
base: baseApiUrl.value + "/search",
get tracks() {
return this.base + "/tracks?q=";
},
@@ -70,7 +87,25 @@ const paths = {
return this.base + "/loadmore";
},
},
files: baseApiUrl + "/file",
colors: {
base: baseApiUrl.value + "/colors",
get album() {
return this.base + "/album";
},
},
settings: {
base: baseApiUrl.value + "/settings",
get get_root_dirs() {
return this.base + "/get-root-dirs";
},
get add_root_dir() {
return this.base + "/add-root-dirs";
},
get remove_root_dir() {
return this.base + "/remove-root-dirs";
},
},
files: baseApiUrl.value + "/file",
},
images: {
thumb: {
@@ -85,5 +120,3 @@ const paths = {
raw: baseImgUrl + imageRoutes.raw,
},
};
export { paths };

View File

@@ -29,6 +29,7 @@ export default async (
const single_artist = track.artist.length === 1;
const single_album_artist = track.albumartist.length === 1;
let no_playlists = false;
const goToArtist = (artists: Artist[]) => {
if (artists.length === 1) {
@@ -58,7 +59,11 @@ export default async (
};
let playlists = <Option[]>[];
const p = await getAllPlaylists();
const p = await getAllPlaylists(true);
if (p.length === 0) {
no_playlists = true;
}
playlists = p.map((playlist: Playlist) => {
return <Option>{
@@ -69,7 +74,13 @@ export default async (
};
});
return [new_playlist, separator, ...playlists];
let return_value = [new_playlist, separator, ...playlists];
if (no_playlists) {
return_value.splice(1, 1);
}
return return_value;
}
const add_to_playlist: Option = {
@@ -155,12 +166,6 @@ export default async (
critical: true,
};
const add_to_fav: Option = {
label: "I love this",
action: () => console.log("I love this"),
icon: "heart",
};
const options: Option[] = [
play_next,
add_to_q,
@@ -172,7 +177,6 @@ export default async (
separator,
go_to_artist,
go_to_alb_artist,
// add_to_fav,
// separator,
// del_track,
];

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