mirror of
https://github.com/koel/koel
synced 2024-11-14 00:17:13 +00:00
feat: use a better ESLint setup (#1850)
This commit is contained in:
parent
9cb99af0e4
commit
5f37982641
384 changed files with 4844 additions and 3455 deletions
52
.eslintrc
52
.eslintrc
|
@ -1,52 +0,0 @@
|
||||||
{
|
|
||||||
"parser": "vue-eslint-parser",
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"parserOptions": {
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"ecmaVersion": 2020
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"plugin:vue/vue3-recommended"
|
|
||||||
],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"cypress/fixtures",
|
|
||||||
"cypress/screenshots",
|
|
||||||
"resources/assets/js/tests/__coverage__"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"globals": {
|
|
||||||
"FileReader": "readonly",
|
|
||||||
"defineProps": "readonly",
|
|
||||||
"defineEmits": "readonly",
|
|
||||||
"defineExpose": "readonly",
|
|
||||||
"withDefaults": "readonly"
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"camelcase": 0,
|
|
||||||
"no-multi-str": 0,
|
|
||||||
"no-empty": 0,
|
|
||||||
"quotes": 0,
|
|
||||||
"no-use-before-define": 0,
|
|
||||||
"@typescript-eslint/no-var-requires": 0,
|
|
||||||
"@typescript-eslint/camelcase": 0,
|
|
||||||
"@typescript-eslint/member-delimiter-style": 0,
|
|
||||||
"@typescript-eslint/consistent-type-assertions": 0,
|
|
||||||
"@typescript-eslint/no-inferrable-types": 0,
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/no-non-null-assertion": 0,
|
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
|
||||||
"@typescript-eslint/no-empty-function": 0,
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
||||||
"standard/no-callback-literal": 0,
|
|
||||||
"vue/valid-v-on": 0,
|
|
||||||
"vue/no-side-effects-in-computed-properties": 0,
|
|
||||||
"vue/max-attributes-per-line": 0,
|
|
||||||
"vue/no-v-html": 0,
|
|
||||||
"vue/singleline-html-element-content-newline": 0,
|
|
||||||
"vue/multi-word-component-names": 0
|
|
||||||
}
|
|
||||||
}
|
|
49
.vscode/settings.json
vendored
Normal file
49
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "format/*", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-indent", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spacing", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-spaces", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-order", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-dangle", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*-newline", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*quotes", "severity": "off", "fixable": true },
|
||||||
|
{ "rule": "*semi", "severity": "off", "fixable": true }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
|
}
|
31
eslint.config.js
Normal file
31
eslint.config.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import config from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default config(
|
||||||
|
{
|
||||||
|
formatters: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.ts', '**/*.vue'],
|
||||||
|
rules: {
|
||||||
|
'antfu/top-level-function': 'off',
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'no-case-declarations': 'off',
|
||||||
|
'no-multi-str': 'off',
|
||||||
|
'no-new': 'off',
|
||||||
|
'perfectionist/sort-imports': 'off',
|
||||||
|
'style/arrow-parens': ['error', 'as-needed'],
|
||||||
|
'style/brace-style': ['error', '1tbs'],
|
||||||
|
'style/new-parens': 'off',
|
||||||
|
'node/prefer-global/process': ['error', 'always'],
|
||||||
|
'style/space-before-function-paren': ['error', 'always'],
|
||||||
|
'ts/ban-ts-comment': 'off',
|
||||||
|
'vue/block-order': ['error', { 'order': ['template', 'script', 'style'] }],
|
||||||
|
'vue/no-lone-template': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
).prepend({
|
||||||
|
ignores: [
|
||||||
|
'resources/assets/js/visualizers/**',
|
||||||
|
]
|
||||||
|
})
|
19
package.json
19
package.json
|
@ -39,6 +39,7 @@
|
||||||
"youtube-player": "^3.0.4"
|
"youtube-player": "^3.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^3.7.3",
|
||||||
"@commitlint/cli": "^19.4.0",
|
"@commitlint/cli": "^19.4.0",
|
||||||
"@commitlint/config-conventional": "^19.2.2",
|
"@commitlint/config-conventional": "^19.2.2",
|
||||||
"@faker-js/faker": "^6.2.0",
|
"@faker-js/faker": "^6.2.0",
|
||||||
|
@ -53,8 +54,6 @@
|
||||||
"@types/pusher-js": "^4.2.2",
|
"@types/pusher-js": "^4.2.2",
|
||||||
"@types/three": "^0.144.0",
|
"@types/three": "^0.144.0",
|
||||||
"@types/youtube-player": "^5.5.2",
|
"@types/youtube-player": "^5.5.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.22.0",
|
|
||||||
"@typescript-eslint/parser": "^4.11.1",
|
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vueuse/components": "^10.9.0",
|
"@vueuse/components": "^10.9.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
@ -63,12 +62,8 @@
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^9.5.4",
|
"cypress": "^9.5.4",
|
||||||
"eslint": "^8.14.0",
|
"eslint": "^9.11.1",
|
||||||
"eslint-plugin-import": "^2.20.2",
|
"eslint-plugin-format": "^0.1.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
|
||||||
"eslint-plugin-standard": "^4.0.1",
|
|
||||||
"eslint-plugin-vue": "^8.7.1",
|
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"factoria": "^4.0.0",
|
"factoria": "^4.0.0",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
|
@ -92,7 +87,7 @@
|
||||||
"yarn": "^1.22.22"
|
"yarn": "^1.22.22"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "eslint ./resources/assets/js/**/*.ts --no-error-on-unmatched-pattern && eslint ./cypress/**/*.ts --no-error-on-unmatched-pattern",
|
"lint": "eslint ./resources/assets/js --fix",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"test:unit": "vitest",
|
"test:unit": "vitest",
|
||||||
"test:e2e": "kill-port 8080 && start-test dev http-get://localhost:8080/api/ping 'cypress open'",
|
"test:e2e": "kill-port 8080 && start-test dev http-get://localhost:8080/api/ping 'cypress open'",
|
||||||
|
@ -109,11 +104,11 @@
|
||||||
"**/*.php": [
|
"**/*.php": [
|
||||||
"composer cs"
|
"composer cs"
|
||||||
],
|
],
|
||||||
"resources/assets/**/*.ts": [
|
"resources/assets/js/**/*.{ts,vue}": [
|
||||||
"eslint"
|
"eslint --fix"
|
||||||
],
|
],
|
||||||
"cypress/**/*.ts": [
|
"cypress/**/*.ts": [
|
||||||
"eslint"
|
"eslint --fix"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import isMobile from 'ismobilejs'
|
import isMobile from 'ismobilejs'
|
||||||
import { isObject, mergeWith } from 'lodash'
|
import { isObject, mergeWith } from 'lodash'
|
||||||
import { cleanup, createEvent, fireEvent, render, RenderOptions } from '@testing-library/vue'
|
import type { RenderOptions } from '@testing-library/vue'
|
||||||
|
import { cleanup, createEvent, fireEvent, render } from '@testing-library/vue'
|
||||||
import { afterEach, beforeEach, vi } from 'vitest'
|
import { afterEach, beforeEach, vi } from 'vitest'
|
||||||
import { defineComponent, nextTick } from 'vue'
|
import { defineComponent, nextTick } from 'vue'
|
||||||
import { commonStore, userStore } from '@/stores'
|
import { commonStore, userStore } from '@/stores'
|
||||||
|
@ -11,8 +12,8 @@ import { DialogBoxStub, MessageToasterStub, OverlayStub } from '@/__tests__/stub
|
||||||
import { routes } from '@/config'
|
import { routes } from '@/config'
|
||||||
import Router from '@/router'
|
import Router from '@/router'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'
|
import type { UserEvent } from '@testing-library/user-event/dist/types/setup/setup'
|
||||||
import { EventType } from '@testing-library/dom/types/events'
|
import type { EventType } from '@testing-library/dom/types/events'
|
||||||
|
|
||||||
// A deep-merge function that
|
// A deep-merge function that
|
||||||
// - supports symbols as keys (_.merge doesn't)
|
// - supports symbols as keys (_.merge doesn't)
|
||||||
|
@ -20,7 +21,9 @@ import { EventType } from '@testing-library/dom/types/events'
|
||||||
// Credit: https://stackoverflow.com/a/60598589/794641
|
// Credit: https://stackoverflow.com/a/60598589/794641
|
||||||
const deepMerge = (first: object, second: object) => {
|
const deepMerge = (first: object, second: object) => {
|
||||||
return mergeWith(first, second, (a, b) => {
|
return mergeWith(first, second, (a, b) => {
|
||||||
if (!isObject(b)) return b
|
if (!isObject(b)) {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return Array.isArray(a) ? [...a, ...b] : { ...a, ...b }
|
return Array.isArray(a) ? [...a, ...b] : { ...a, ...b }
|
||||||
|
@ -28,7 +31,9 @@ const deepMerge = (first: object, second: object) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPropIfNotExists = (obj: object | null, prop: any, value: any) => {
|
const setPropIfNotExists = (obj: object | null, prop: any, value: any) => {
|
||||||
if (!obj) return
|
if (!obj) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
if (!Object.prototype.hasOwnProperty.call(obj, prop)) {
|
||||||
obj[prop] = value
|
obj[prop] = value
|
||||||
|
@ -46,7 +51,7 @@ export default abstract class UnitTestCase {
|
||||||
this.user = userEvent.setup({ delay: null }) // @see https://github.com/testing-library/user-event/issues/833
|
this.user = userEvent.setup({ delay: null }) // @see https://github.com/testing-library/user-event/issues/833
|
||||||
|
|
||||||
this.setReadOnlyProperty(navigator, 'clipboard', {
|
this.setReadOnlyProperty(navigator, 'clipboard', {
|
||||||
writeText: vi.fn()
|
writeText: vi.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
this.beforeEach()
|
this.beforeEach()
|
||||||
|
@ -113,12 +118,12 @@ export default abstract class UnitTestCase {
|
||||||
'koel-focus': {},
|
'koel-focus': {},
|
||||||
'koel-tooltip': {},
|
'koel-tooltip': {},
|
||||||
'koel-hide-broken-icon': {},
|
'koel-hide-broken-icon': {},
|
||||||
'koel-overflow-fade': {}
|
'koel-overflow-fade': {},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Icon: this.stub('Icon')
|
Icon: this.stub('Icon'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}, this.supplyRequiredProvides(options)))
|
}, this.supplyRequiredProvides(options)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +153,7 @@ export default abstract class UnitTestCase {
|
||||||
|
|
||||||
protected stub (testId = 'stub') {
|
protected stub (testId = 'stub') {
|
||||||
return defineComponent({
|
return defineComponent({
|
||||||
template: `<br data-testid="${testId}"/>`
|
template: `<br data-testid="${testId}"/>`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -162,8 +167,8 @@ export default abstract class UnitTestCase {
|
||||||
return Object.defineProperties(obj, {
|
return Object.defineProperties(obj, {
|
||||||
[prop]: {
|
[prop]: {
|
||||||
value,
|
value,
|
||||||
configurable: true
|
configurable: true,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +177,7 @@ export default abstract class UnitTestCase {
|
||||||
await this.user.type(element, value)
|
await this.user.type(element, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async trigger (element: HTMLElement, key: EventType | string, options?: {}) {
|
protected async trigger (element: HTMLElement, key: EventType | string, options?: object = {}) {
|
||||||
await fireEvent(element, createEvent[key](element, options))
|
await fireEvent(element, createEvent[key](element, options))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Album => {
|
export default (faker: Faker): Album => {
|
||||||
return {
|
return {
|
||||||
|
@ -8,7 +8,7 @@ export default (faker: Faker): Album => {
|
||||||
id: faker.datatype.number({ min: 2 }), // avoid Unknown Album by default
|
id: faker.datatype.number({ min: 2 }), // avoid Unknown Album by default
|
||||||
name: faker.lorem.sentence(),
|
name: faker.lorem.sentence(),
|
||||||
cover: faker.image.imageUrl(),
|
cover: faker.image.imageUrl(),
|
||||||
created_at: faker.date.past().toISOString()
|
created_at: faker.date.past().toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,6 @@ export const states: Record<string, Omit<Partial<Album>, 'type'>> = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Unknown Album',
|
name: 'Unknown Album',
|
||||||
artist_id: 1,
|
artist_id: 1,
|
||||||
artist_name: 'Unknown Artist'
|
artist_name: 'Unknown Artist',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
import factory from 'factoria'
|
import factory from 'factoria'
|
||||||
|
|
||||||
export default (faker: Faker): AlbumInfo => ({
|
export default (faker: Faker): AlbumInfo => ({
|
||||||
cover: faker.image.imageUrl(),
|
cover: faker.image.imageUrl(),
|
||||||
wiki: {
|
wiki: {
|
||||||
summary: faker.lorem.sentence(),
|
summary: faker.lorem.sentence(),
|
||||||
full: faker.lorem.sentences(4)
|
full: faker.lorem.sentences(4),
|
||||||
},
|
},
|
||||||
tracks: factory('album-track', 8),
|
tracks: factory('album-track', 8),
|
||||||
url: faker.internet.url()
|
url: faker.internet.url(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): AlbumTrack => ({
|
export default (faker: Faker): AlbumTrack => ({
|
||||||
title: faker.lorem.sentence(),
|
title: faker.lorem.sentence(),
|
||||||
length: faker.datatype.number({ min: 180, max: 1_800 })
|
length: faker.datatype.number({ min: 180, max: 1_800 }),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Artist => {
|
export default (faker: Faker): Artist => {
|
||||||
return {
|
return {
|
||||||
|
@ -6,17 +6,17 @@ export default (faker: Faker): Artist => {
|
||||||
id: faker.datatype.number({ min: 3 }), // avoid Unknown and Various Artist by default
|
id: faker.datatype.number({ min: 3 }), // avoid Unknown and Various Artist by default
|
||||||
name: faker.name.findName(),
|
name: faker.name.findName(),
|
||||||
image: 'foo.jpg',
|
image: 'foo.jpg',
|
||||||
created_at: faker.date.past().toISOString()
|
created_at: faker.date.past().toISOString(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const states: Record<string, Omit<Partial<Artist>, 'type'>> = {
|
export const states: Record<string, Omit<Partial<Artist>, 'type'>> = {
|
||||||
unknown: {
|
unknown: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Unknown Artist'
|
name: 'Unknown Artist',
|
||||||
},
|
},
|
||||||
various: {
|
various: {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'Various Artists'
|
name: 'Various Artists',
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): ArtistInfo => ({
|
export default (faker: Faker): ArtistInfo => ({
|
||||||
image: faker.image.imageUrl(),
|
image: faker.image.imageUrl(),
|
||||||
bio: {
|
bio: {
|
||||||
summary: faker.lorem.sentence(),
|
summary: faker.lorem.sentence(),
|
||||||
full: faker.lorem.sentences(4)
|
full: faker.lorem.sentences(4),
|
||||||
},
|
},
|
||||||
url: faker.internet.url()
|
url: faker.internet.url(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Episode => {
|
export default (faker: Faker): Episode => {
|
||||||
return {
|
return {
|
||||||
|
@ -15,6 +15,6 @@ export default (faker: Faker): Episode => {
|
||||||
episode_image: faker.image.imageUrl(),
|
episode_image: faker.image.imageUrl(),
|
||||||
podcast_id: faker.datatype.uuid(),
|
podcast_id: faker.datatype.uuid(),
|
||||||
podcast_title: faker.lorem.sentence(),
|
podcast_title: faker.lorem.sentence(),
|
||||||
podcast_author: faker.name.findName()
|
podcast_author: faker.name.findName(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
import { genres } from '@/config'
|
import { genres } from '@/config'
|
||||||
|
|
||||||
export default (faker: Faker): Genre => {
|
export default (faker: Faker): Genre => {
|
||||||
|
@ -6,6 +6,6 @@ export default (faker: Faker): Genre => {
|
||||||
type: 'genres',
|
type: 'genres',
|
||||||
name: faker.helpers.arrayElement(genres),
|
name: faker.helpers.arrayElement(genres),
|
||||||
song_count: faker.datatype.number({ min: 1, max: 1_000 }),
|
song_count: faker.datatype.number({ min: 1, max: 1_000 }),
|
||||||
length: faker.datatype.number({ min: 300, max: 300_000 })
|
length: faker.datatype.number({ min: 300, max: 300_000 }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import factoria, { Factoria } from 'factoria'
|
import type { Factoria } from 'factoria'
|
||||||
|
import factoria from 'factoria'
|
||||||
import artistFactory, { states as artistStates } from '@/__tests__/factory/artistFactory'
|
import artistFactory, { states as artistStates } from '@/__tests__/factory/artistFactory'
|
||||||
import songFactory, { states as songStates } from '@/__tests__/factory/songFactory'
|
import songFactory, { states as songStates } from '@/__tests__/factory/songFactory'
|
||||||
import albumFactory, { states as albumStates } from '@/__tests__/factory/albumFactory'
|
import albumFactory, { states as albumStates } from '@/__tests__/factory/albumFactory'
|
||||||
|
@ -16,26 +17,26 @@ import genreFactory from '@/__tests__/factory/genreFactory'
|
||||||
import playlistCollaboratorFactory from '@/__tests__/factory/playlistCollaboratorFactory'
|
import playlistCollaboratorFactory from '@/__tests__/factory/playlistCollaboratorFactory'
|
||||||
import episodeFactory from '@/__tests__/factory/episodeFactory'
|
import episodeFactory from '@/__tests__/factory/episodeFactory'
|
||||||
import podcastFactory from '@/__tests__/factory/podcastFactory'
|
import podcastFactory from '@/__tests__/factory/podcastFactory'
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
type ModelToTypeMap = {
|
interface ModelToTypeMap {
|
||||||
artist: Artist
|
'artist': Artist
|
||||||
'artist-info': ArtistInfo
|
'artist-info': ArtistInfo
|
||||||
album: Album
|
'album': Album
|
||||||
'album-track': AlbumTrack
|
'album-track': AlbumTrack
|
||||||
'album-info': AlbumInfo
|
'album-info': AlbumInfo
|
||||||
song: Song
|
'song': Song
|
||||||
interaction: Interaction
|
'interaction': Interaction
|
||||||
genre: Genre
|
'genre': Genre
|
||||||
video: YouTubeVideo
|
'video': YouTubeVideo
|
||||||
'smart-playlist-rule': SmartPlaylistRule
|
'smart-playlist-rule': SmartPlaylistRule
|
||||||
'smart-playlist-rule-group': SmartPlaylistRuleGroup
|
'smart-playlist-rule-group': SmartPlaylistRuleGroup
|
||||||
playlist: Playlist
|
'playlist': Playlist
|
||||||
'playlist-folder': PlaylistFolder
|
'playlist-folder': PlaylistFolder
|
||||||
user: User
|
'user': User
|
||||||
'playlist-collaborator': PlaylistCollaborator
|
'playlist-collaborator': PlaylistCollaborator
|
||||||
episode: Episode
|
'episode': Episode
|
||||||
podcast: Podcast
|
'podcast': Podcast
|
||||||
}
|
}
|
||||||
|
|
||||||
type Model = keyof ModelToTypeMap
|
type Model = keyof ModelToTypeMap
|
||||||
|
@ -44,30 +45,30 @@ type Overrides<M extends Model> = Factoria.Overrides<ModelToTypeMap[M]>
|
||||||
const define = <M extends Model>(
|
const define = <M extends Model>(
|
||||||
model: M,
|
model: M,
|
||||||
handle: (faker: Faker) => Overrides<M>,
|
handle: (faker: Faker) => Overrides<M>,
|
||||||
states?: Record<string, Factoria.StateDefinition>
|
states?: Record<string, Factoria.StateDefinition>,
|
||||||
) => factoria.define(model, handle, states)
|
) => factoria.define(model, handle, states)
|
||||||
|
|
||||||
function factory <M extends Model>(
|
function factory<M extends Model> (
|
||||||
model: M,
|
model: M,
|
||||||
overrides?: Overrides<M>
|
overrides?: Overrides<M>
|
||||||
): ModelToTypeMap[M]
|
): ModelToTypeMap[M]
|
||||||
|
|
||||||
function factory <M extends Model>(
|
function factory<M extends Model> (
|
||||||
model: M,
|
model: M,
|
||||||
count: 1,
|
count: 1,
|
||||||
overrides?: Overrides<M>
|
overrides?: Overrides<M>
|
||||||
): ModelToTypeMap[M]
|
): ModelToTypeMap[M]
|
||||||
|
|
||||||
function factory <M extends Model>(
|
function factory<M extends Model> (
|
||||||
model: M,
|
model: M,
|
||||||
count: number,
|
count: number,
|
||||||
overrides?: Overrides<M>
|
overrides?: Overrides<M>
|
||||||
): ModelToTypeMap[M][]
|
): ModelToTypeMap[M][]
|
||||||
|
|
||||||
function factory <M extends Model>(
|
function factory<M extends Model> (
|
||||||
model: M,
|
model: M,
|
||||||
count: number|Overrides<M> = 1,
|
count: number | Overrides<M> = 1,
|
||||||
overrides?: Overrides<M>
|
overrides?: Overrides<M>,
|
||||||
) {
|
) {
|
||||||
return typeof count === 'number'
|
return typeof count === 'number'
|
||||||
? count === 1 ? factoria(model, overrides) : factoria(model, count, overrides)
|
? count === 1 ? factoria(model, overrides) : factoria(model, count, overrides)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Interaction => ({
|
export default (faker: Faker): Interaction => ({
|
||||||
type: 'interactions',
|
type: 'interactions',
|
||||||
id: faker.datatype.number({ min: 1 }),
|
id: faker.datatype.number({ min: 1 }),
|
||||||
song_id: faker.datatype.uuid(),
|
song_id: faker.datatype.uuid(),
|
||||||
liked: faker.datatype.boolean(),
|
liked: faker.datatype.boolean(),
|
||||||
play_count: faker.datatype.number({ min: 1 })
|
play_count: faker.datatype.number({ min: 1 }),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): PlaylistCollaborator => ({
|
export default (faker: Faker): PlaylistCollaborator => ({
|
||||||
type: 'playlist-collaborators',
|
type: 'playlist-collaborators',
|
||||||
id: faker.datatype.number(),
|
id: faker.datatype.number(),
|
||||||
name: faker.name.findName(),
|
name: faker.name.findName(),
|
||||||
avatar: 'https://gravatar.com/foo'
|
avatar: 'https://gravatar.com/foo',
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import factory from 'factoria'
|
import factory from 'factoria'
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Playlist => ({
|
export default (faker: Faker): Playlist => ({
|
||||||
type: 'playlists',
|
type: 'playlists',
|
||||||
|
@ -18,10 +18,10 @@ export const states: Record<string, (faker: Faker) => Omit<Partial<Playlist>, 't
|
||||||
smart: _ => ({
|
smart: _ => ({
|
||||||
is_smart: true,
|
is_smart: true,
|
||||||
rules: [
|
rules: [
|
||||||
factory('smart-playlist-rule-group')
|
factory('smart-playlist-rule-group'),
|
||||||
]
|
],
|
||||||
}),
|
}),
|
||||||
orphan: _ => ({
|
orphan: _ => ({
|
||||||
folder_id: null
|
folder_id: null,
|
||||||
})
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): PlaylistFolder => ({
|
export default (faker: Faker): PlaylistFolder => ({
|
||||||
type: 'playlist-folders',
|
type: 'playlist-folders',
|
||||||
id: faker.datatype.uuid(),
|
id: faker.datatype.uuid(),
|
||||||
name: faker.random.word()
|
name: faker.random.word(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): Podcast => {
|
export default (faker: Faker): Podcast => {
|
||||||
return {
|
return {
|
||||||
|
@ -14,7 +14,7 @@ export default (faker: Faker): Podcast => {
|
||||||
created_at: faker.date.past().toISOString(),
|
created_at: faker.date.past().toISOString(),
|
||||||
state: {
|
state: {
|
||||||
current_episode: null,
|
current_episode: null,
|
||||||
progresses: {}
|
progresses: {},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
import models from '@/config/smart-playlist/models'
|
import models from '@/config/smart-playlist/models'
|
||||||
|
|
||||||
export default (faker: Faker): SmartPlaylistRule => ({
|
export default (faker: Faker): SmartPlaylistRule => ({
|
||||||
id: faker.datatype.uuid(),
|
id: faker.datatype.uuid(),
|
||||||
model: faker.helpers.arrayElement(models),
|
model: faker.helpers.arrayElement(models),
|
||||||
operator: faker.helpers.arrayElement<SmartPlaylistOperator['operator']>(['is', 'contains', 'isNot']),
|
operator: faker.helpers.arrayElement<SmartPlaylistOperator['operator']>(['is', 'contains', 'isNot']),
|
||||||
value: [faker.random.word()]
|
value: [faker.random.word()],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
import factory from 'factoria'
|
import factory from 'factoria'
|
||||||
|
|
||||||
export default (faker: Faker): SmartPlaylistRuleGroup => ({
|
export default (faker: Faker): SmartPlaylistRuleGroup => ({
|
||||||
id: faker.datatype.uuid(),
|
id: faker.datatype.uuid(),
|
||||||
rules: factory('smart-playlist-rule', 3)
|
rules: factory('smart-playlist-rule', 3),
|
||||||
})
|
})
|
||||||
|
|
|
@ -27,12 +27,12 @@ const generate = (partOfCompilation = false): Song => {
|
||||||
liked: faker.datatype.boolean(),
|
liked: faker.datatype.boolean(),
|
||||||
is_public: faker.datatype.boolean(),
|
is_public: faker.datatype.boolean(),
|
||||||
created_at: faker.date.past().toISOString(),
|
created_at: faker.date.past().toISOString(),
|
||||||
playback_state: 'Stopped'
|
playback_state: 'Stopped',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (): Song => generate()
|
export default (): Song => generate()
|
||||||
|
|
||||||
export const states: Record<string, Partial<Song>> = {
|
export const states: Record<string, Partial<Song>> = {
|
||||||
partOfCompilation: generate(true)
|
partOfCompilation: generate(true),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): User => ({
|
export default (faker: Faker): User => ({
|
||||||
type: 'users',
|
type: 'users',
|
||||||
|
@ -11,14 +11,14 @@ export default (faker: Faker): User => ({
|
||||||
avatar: 'https://gravatar.com/foo',
|
avatar: 'https://gravatar.com/foo',
|
||||||
preferences: undefined,
|
preferences: undefined,
|
||||||
sso_provider: null,
|
sso_provider: null,
|
||||||
sso_id: null
|
sso_id: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const states: Record<string, Omit<Partial<User>, 'type'>> = {
|
export const states: Record<string, Omit<Partial<User>, 'type'>> = {
|
||||||
admin: {
|
admin: {
|
||||||
is_admin: true
|
is_admin: true,
|
||||||
},
|
},
|
||||||
prospect: {
|
prospect: {
|
||||||
is_prospect: true
|
is_prospect: true,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { Faker } from '@faker-js/faker'
|
import type { Faker } from '@faker-js/faker'
|
||||||
|
|
||||||
export default (faker: Faker): YouTubeVideo => ({
|
export default (faker: Faker): YouTubeVideo => ({
|
||||||
id: {
|
id: {
|
||||||
videoId: faker.random.alphaNumeric(16)
|
videoId: faker.random.alphaNumeric(16),
|
||||||
},
|
},
|
||||||
snippet: {
|
snippet: {
|
||||||
title: faker.lorem.sentence(),
|
title: faker.lorem.sentence(),
|
||||||
description: faker.lorem.paragraph(),
|
description: faker.lorem.paragraph(),
|
||||||
thumbnails: {
|
thumbnails: {
|
||||||
default: {
|
default: {
|
||||||
url: faker.image.imageUrl()
|
url: faker.image.imageUrl(),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,18 +19,17 @@ declare global {
|
||||||
|
|
||||||
expect.addSnapshotSerializer(vueSnapshotSerializer)
|
expect.addSnapshotSerializer(vueSnapshotSerializer)
|
||||||
|
|
||||||
global.ResizeObserver = global.ResizeObserver ||
|
globalThis.ResizeObserver = globalThis.ResizeObserver
|
||||||
vi.fn().mockImplementation(() => ({
|
|| vi.fn().mockImplementation(() => ({
|
||||||
disconnect: vi.fn(),
|
disconnect: vi.fn(),
|
||||||
observe: vi.fn(),
|
observe: vi.fn(),
|
||||||
unobserve: vi.fn()
|
unobserve: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
globalThis.LemonSqueezy = {
|
||||||
global.LemonSqueezy = {
|
|
||||||
Url: {
|
Url: {
|
||||||
Open: vi.fn()
|
Open: vi.fn(),
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
HTMLMediaElement.prototype.load = vi.fn()
|
HTMLMediaElement.prototype.load = vi.fn()
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { Ref, ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
import { noop } from '@/utils'
|
import { noop } from '@/utils'
|
||||||
|
|
||||||
import MessageToaster from '@/components/ui/message-toaster/MessageToaster.vue'
|
import type MessageToaster from '@/components/ui/message-toaster/MessageToaster.vue'
|
||||||
import DialogBox from '@/components/ui/DialogBox.vue'
|
import type DialogBox from '@/components/ui/DialogBox.vue'
|
||||||
import Overlay from '@/components/ui/Overlay.vue'
|
import type Overlay from '@/components/ui/Overlay.vue'
|
||||||
|
|
||||||
export const MessageToasterStub = ref({
|
export const MessageToasterStub = ref({
|
||||||
info: noop,
|
info: noop,
|
||||||
success: noop,
|
success: noop,
|
||||||
warning: noop,
|
warning: noop,
|
||||||
error: noop
|
error: noop,
|
||||||
}) as unknown as Ref<InstanceType<typeof MessageToaster>>
|
}) as unknown as Ref<InstanceType<typeof MessageToaster>>
|
||||||
|
|
||||||
export const DialogBoxStub = ref({
|
export const DialogBoxStub = ref({
|
||||||
|
@ -17,10 +18,10 @@ export const DialogBoxStub = ref({
|
||||||
success: noop,
|
success: noop,
|
||||||
warning: noop,
|
warning: noop,
|
||||||
error: noop,
|
error: noop,
|
||||||
confirm: noop
|
confirm: noop,
|
||||||
}) as unknown as Ref<InstanceType<typeof DialogBox>>
|
}) as unknown as Ref<InstanceType<typeof DialogBox>>
|
||||||
|
|
||||||
export const OverlayStub = ref({
|
export const OverlayStub = ref({
|
||||||
show: noop,
|
show: noop,
|
||||||
hide: noop
|
hide: noop,
|
||||||
}) as unknown as Ref<InstanceType<typeof Overlay>>
|
}) as unknown as Ref<InstanceType<typeof Overlay>>
|
||||||
|
|
|
@ -56,18 +56,18 @@ new class extends UnitTestCase {
|
||||||
id: 42,
|
id: 42,
|
||||||
name: 'IV',
|
name: 'IV',
|
||||||
artist_id: 17,
|
artist_id: 17,
|
||||||
artist_name: 'Led Zeppelin'
|
artist_name: 'Led Zeppelin',
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.render(AlbumCard, {
|
return this.render(AlbumCard, {
|
||||||
props: {
|
props: {
|
||||||
album
|
album,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
AlbumArtistThumbnail: this.stub('thumbnail'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,10 @@ import { useDraggable, useRouter } from '@/composables'
|
||||||
|
|
||||||
import BaseCard from '@/components/ui/album-artist/AlbumOrArtistCard.vue'
|
import BaseCard from '@/components/ui/album-artist/AlbumOrArtistCard.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ album: Album, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
||||||
const { go } = useRouter()
|
const { go } = useRouter()
|
||||||
const { startDragging } = useDraggable('album')
|
const { startDragging } = useDraggable('album')
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ album: Album, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
|
||||||
const { album, layout } = toRefs(props)
|
const { album, layout } = toRefs(props)
|
||||||
|
|
||||||
// We're not checking for supports_batch_downloading here, as the number of songs on the album is not yet known.
|
// We're not checking for supports_batch_downloading here, as the number of songs on the album is not yet known.
|
||||||
|
|
|
@ -85,7 +85,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
private async renderComponent (_album?: Album) {
|
private async renderComponent (_album?: Album) {
|
||||||
album = _album || factory('album', {
|
album = _album || factory('album', {
|
||||||
name: 'IV'
|
name: 'IV',
|
||||||
})
|
})
|
||||||
|
|
||||||
const rendered = this.render(AlbumContextMenu)
|
const rendered = this.render(AlbumContextMenu)
|
||||||
|
|
|
@ -10,7 +10,7 @@ let album: Album
|
||||||
|
|
||||||
new class extends UnitTestCase {
|
new class extends UnitTestCase {
|
||||||
protected test () {
|
protected test () {
|
||||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async mode => {
|
||||||
await this.renderComponent(mode)
|
await this.renderComponent(mode)
|
||||||
|
|
||||||
screen.getByTestId('album-info-tracks')
|
screen.getByTestId('album-info-tracks')
|
||||||
|
@ -38,14 +38,14 @@ new class extends UnitTestCase {
|
||||||
const rendered = this.render(AlbumInfoComponent, {
|
const rendered = this.render(AlbumInfoComponent, {
|
||||||
props: {
|
props: {
|
||||||
album,
|
album,
|
||||||
mode
|
mode,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
TrackList: this.stub(),
|
TrackList: this.stub(),
|
||||||
AlbumThumbnail: this.stub('thumbnail')
|
AlbumThumbnail: this.stub('thumbnail'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.tick(1)
|
await this.tick(1)
|
||||||
|
|
|
@ -40,9 +40,10 @@ import AlbumThumbnail from '@/components/ui/album-artist/AlbumOrArtistThumbnail.
|
||||||
import AlbumArtistInfo from '@/components/ui/album-artist/AlbumOrArtistInfo.vue'
|
import AlbumArtistInfo from '@/components/ui/album-artist/AlbumOrArtistInfo.vue'
|
||||||
import ExpandableContentBlock from '@/components/ui/album-artist/ExpandableContentBlock.vue'
|
import ExpandableContentBlock from '@/components/ui/album-artist/ExpandableContentBlock.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ album: Album, mode?: MediaInfoDisplayMode }>(), { mode: 'aside' })
|
||||||
|
|
||||||
const TrackList = defineAsyncComponent(() => import('@/components/album/AlbumTrackList.vue'))
|
const TrackList = defineAsyncComponent(() => import('@/components/album/AlbumTrackList.vue'))
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ album: Album, mode?: MediaInfoDisplayMode }>(), { mode: 'aside' })
|
|
||||||
const { album, mode } = toRefs(props)
|
const { album, mode } = toRefs(props)
|
||||||
|
|
||||||
const { useLastfm, useSpotify } = useThirdPartyServices()
|
const { useLastfm, useSpotify } = useThirdPartyServices()
|
||||||
|
|
|
@ -14,8 +14,8 @@ new class extends UnitTestCase {
|
||||||
this.render(AlbumTrackList, {
|
this.render(AlbumTrackList, {
|
||||||
props: {
|
props: {
|
||||||
album,
|
album,
|
||||||
tracks: factory('album-track', 3)
|
tracks: factory('album-track', 3),
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.tick()
|
await this.tick()
|
||||||
|
|
|
@ -30,7 +30,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
const track = factory('album-track', {
|
const track = factory('album-track', {
|
||||||
title: 'Fahrstuhl to Heaven',
|
title: 'Fahrstuhl to Heaven',
|
||||||
length: 280
|
length: 280,
|
||||||
})
|
})
|
||||||
|
|
||||||
const matchMock = this.mock(songStore, 'match', matchedSong)
|
const matchMock = this.mock(songStore, 'match', matchedSong)
|
||||||
|
@ -38,13 +38,13 @@ new class extends UnitTestCase {
|
||||||
const rendered = this.render(AlbumTrackListItem, {
|
const rendered = this.render(AlbumTrackListItem, {
|
||||||
props: {
|
props: {
|
||||||
album,
|
album,
|
||||||
track
|
track,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>PlayablesKey]: ref(songsToMatchAgainst)
|
[<symbol>PlayablesKey]: ref(songsToMatchAgainst),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(matchMock).toHaveBeenCalledWith('Fahrstuhl to Heaven', songsToMatchAgainst)
|
expect(matchMock).toHaveBeenCalledWith('Fahrstuhl to Heaven', songsToMatchAgainst)
|
||||||
|
|
|
@ -13,16 +13,18 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, defineAsyncComponent, Ref, toRefs } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, defineAsyncComponent, toRefs } from 'vue'
|
||||||
import { songStore } from '@/stores'
|
import { songStore } from '@/stores'
|
||||||
import { authService, playbackService } from '@/services'
|
import { authService, playbackService } from '@/services'
|
||||||
import { useThirdPartyServices } from '@/composables'
|
import { useThirdPartyServices } from '@/composables'
|
||||||
import { requireInjection, secondsToHis } from '@/utils'
|
import { requireInjection, secondsToHis } from '@/utils'
|
||||||
import { PlayablesKey } from '@/symbols'
|
import { PlayablesKey } from '@/symbols'
|
||||||
|
|
||||||
|
const props = defineProps<{ album: Album, track: AlbumTrack }>()
|
||||||
|
|
||||||
const AppleMusicButton = defineAsyncComponent(() => import('@/components/ui/AppleMusicButton.vue'))
|
const AppleMusicButton = defineAsyncComponent(() => import('@/components/ui/AppleMusicButton.vue'))
|
||||||
|
|
||||||
const props = defineProps<{ album: Album, track: AlbumTrack }>()
|
|
||||||
const { album, track } = toRefs(props)
|
const { album, track } = toRefs(props)
|
||||||
|
|
||||||
const { useAppleMusic } = useThirdPartyServices()
|
const { useAppleMusic } = useThirdPartyServices()
|
||||||
|
@ -44,7 +46,8 @@ const play = () => matchedSong.value && playbackService.play(matchedSong.value)
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
.track-list-item {
|
.track-list-item {
|
||||||
&:focus, &.active {
|
&:focus,
|
||||||
|
&.active {
|
||||||
span.title {
|
span.title {
|
||||||
@apply text-k-highlight;
|
@apply text-k-highlight;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ new class extends UnitTestCase {
|
||||||
super.beforeEach(() => {
|
super.beforeEach(() => {
|
||||||
artist = factory('artist', {
|
artist = factory('artist', {
|
||||||
id: 42,
|
id: 42,
|
||||||
name: 'Led Zeppelin'
|
name: 'Led Zeppelin',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -63,13 +63,13 @@ new class extends UnitTestCase {
|
||||||
private renderComponent () {
|
private renderComponent () {
|
||||||
return this.render(ArtistCard, {
|
return this.render(ArtistCard, {
|
||||||
props: {
|
props: {
|
||||||
artist
|
artist,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
AlbumArtistThumbnail: this.stub('thumbnail')
|
AlbumArtistThumbnail: this.stub('thumbnail'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,10 +31,10 @@ import { useDraggable, useRouter } from '@/composables'
|
||||||
|
|
||||||
import BaseCard from '@/components/ui/album-artist/AlbumOrArtistCard.vue'
|
import BaseCard from '@/components/ui/album-artist/AlbumOrArtistCard.vue'
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{ artist: Artist, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
||||||
const { go } = useRouter()
|
const { go } = useRouter()
|
||||||
const { startDragging } = useDraggable('artist')
|
const { startDragging } = useDraggable('artist')
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ artist: Artist, layout?: ArtistAlbumCardLayout }>(), { layout: 'full' })
|
|
||||||
const { artist, layout } = toRefs(props)
|
const { artist, layout } = toRefs(props)
|
||||||
|
|
||||||
// We're not checking for supports_batch_downloading here, as the number of songs by the artist is not yet known.
|
// We're not checking for supports_batch_downloading here, as the number of songs by the artist is not yet known.
|
||||||
|
|
|
@ -82,7 +82,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
private async renderComponent (_artist?: Artist) {
|
private async renderComponent (_artist?: Artist) {
|
||||||
artist = _artist || factory('artist', {
|
artist = _artist || factory('artist', {
|
||||||
name: 'Accept'
|
name: 'Accept',
|
||||||
})
|
})
|
||||||
|
|
||||||
const rendered = this.render(ArtistContextMenu)
|
const rendered = this.render(ArtistContextMenu)
|
||||||
|
|
|
@ -30,7 +30,7 @@ const allowDownload = toRef(commonStore.state, 'allows_download')
|
||||||
|
|
||||||
const isStandardArtist = computed(() =>
|
const isStandardArtist = computed(() =>
|
||||||
!artistStore.isUnknown(artist.value!)
|
!artistStore.isUnknown(artist.value!)
|
||||||
&& !artistStore.isVarious(artist.value!)
|
&& !artistStore.isVarious(artist.value!),
|
||||||
)
|
)
|
||||||
|
|
||||||
const play = () => trigger(async () => {
|
const play = () => trigger(async () => {
|
||||||
|
|
|
@ -10,7 +10,7 @@ let artist: Artist
|
||||||
|
|
||||||
new class extends UnitTestCase {
|
new class extends UnitTestCase {
|
||||||
protected test () {
|
protected test () {
|
||||||
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async (mode) => {
|
it.each<[MediaInfoDisplayMode]>([['aside'], ['full']])('renders in %s mode', async mode => {
|
||||||
await this.renderComponent(mode)
|
await this.renderComponent(mode)
|
||||||
|
|
||||||
if (mode === 'aside') {
|
if (mode === 'aside') {
|
||||||
|
@ -33,13 +33,13 @@ new class extends UnitTestCase {
|
||||||
const rendered = this.render(ArtistInfoComponent, {
|
const rendered = this.render(ArtistInfoComponent, {
|
||||||
props: {
|
props: {
|
||||||
artist,
|
artist,
|
||||||
mode
|
mode,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
ArtistThumbnail: this.stub('thumbnail')
|
ArtistThumbnail: this.stub('thumbnail'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.tick(1)
|
await this.tick(1)
|
||||||
|
|
|
@ -30,10 +30,10 @@ import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'cancel'): void }>()
|
||||||
const { handleHttpError } = useErrorHandler()
|
const { handleHttpError } = useErrorHandler()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'cancel'): void }>()
|
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { screen, waitFor } from '@testing-library/vue'
|
import { screen, waitFor } from '@testing-library/vue'
|
||||||
import { expect, it, Mock } from 'vitest'
|
import type { Mock } from 'vitest'
|
||||||
|
import { expect, it } from 'vitest'
|
||||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
import { authService } from '@/services'
|
import { authService } from '@/services'
|
||||||
import { logger } from '@/utils'
|
import { logger } from '@/utils'
|
||||||
|
@ -45,9 +46,9 @@ new class extends UnitTestCase {
|
||||||
const { html } = this.render(LoginFrom, {
|
const { html } = this.render(LoginFrom, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
GoogleLoginButton: this.stub('google-login-button')
|
GoogleLoginButton: this.stub('google-login-button'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(html()).toMatchSnapshot()
|
expect(html()).toMatchSnapshot()
|
||||||
|
|
|
@ -51,9 +51,11 @@ import GoogleLoginButton from '@/components/auth/sso/GoogleLoginButton.vue'
|
||||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'loggedin'): void }>()
|
||||||
|
|
||||||
const DEMO_ACCOUNT = {
|
const DEMO_ACCOUNT = {
|
||||||
email: 'demo@koel.dev',
|
email: 'demo@koel.dev',
|
||||||
password: 'demo'
|
password: 'demo',
|
||||||
}
|
}
|
||||||
|
|
||||||
const canResetPassword = window.MAILER_CONFIGURED && !window.IS_DEMO
|
const canResetPassword = window.MAILER_CONFIGURED && !window.IS_DEMO
|
||||||
|
@ -66,8 +68,6 @@ const showingForgotPasswordForm = ref(false)
|
||||||
|
|
||||||
const showForgotPasswordForm = () => (showingForgotPasswordForm.value = true)
|
const showForgotPasswordForm = () => (showingForgotPasswordForm.value = true)
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'loggedin'): void }>()
|
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
try {
|
try {
|
||||||
await authService.login(email.value, password.value)
|
await authService.login(email.value, password.value)
|
||||||
|
@ -103,10 +103,12 @@ const onSSOSuccess = (token: CompositeToken) => {
|
||||||
* You like to - move it!
|
* You like to - move it!
|
||||||
*/
|
*/
|
||||||
@keyframes shake {
|
@keyframes shake {
|
||||||
8%, 41% {
|
8%,
|
||||||
|
41% {
|
||||||
transform: translateX(-10px);
|
transform: translateX(-10px);
|
||||||
}
|
}
|
||||||
25%, 58% {
|
25%,
|
||||||
|
58% {
|
||||||
transform: translateX(10px);
|
transform: translateX(10px);
|
||||||
}
|
}
|
||||||
75% {
|
75% {
|
||||||
|
@ -115,7 +117,8 @@ const onSSOSuccess = (token: CompositeToken) => {
|
||||||
92% {
|
92% {
|
||||||
transform: translateX(5px);
|
transform: translateX(5px);
|
||||||
}
|
}
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +126,7 @@ const onSSOSuccess = (token: CompositeToken) => {
|
||||||
form {
|
form {
|
||||||
&.error {
|
&.error {
|
||||||
@apply border-red-500;
|
@apply border-red-500;
|
||||||
animation: shake .5s;
|
animation: shake 0.5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
await this.router.activateRoute({
|
await this.router.activateRoute({
|
||||||
path: '_',
|
path: '_',
|
||||||
screen: 'Password.Reset'
|
screen: 'Password.Reset',
|
||||||
}, { payload: 'Zm9vQGJhci5jb218bXktdG9rZW4=' })
|
}, { payload: 'Zm9vQGJhci5jb218bXktdG9rZW4=' })
|
||||||
|
|
||||||
this.render(ResetPasswordForm)
|
this.render(ResetPasswordForm)
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { authService } from '@/services'
|
import { authService } from '@/services'
|
||||||
import { base64Decode } from '@/utils'
|
import { base64Decode, logger } from '@/utils'
|
||||||
import { useErrorHandler, useMessageToaster, useRouter } from '@/composables'
|
import { useErrorHandler, useMessageToaster, useRouter } from '@/composables'
|
||||||
|
|
||||||
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
import PasswordField from '@/components/ui/form/PasswordField.vue'
|
||||||
|
@ -39,6 +39,7 @@ const validPayload = computed(() => email.value && token.value)
|
||||||
try {
|
try {
|
||||||
[email.value, token.value] = base64Decode(decodeURIComponent(getRouteParam('payload')!)).split('|')
|
[email.value, token.value] = base64Decode(decodeURIComponent(getRouteParam('payload')!)).split('|')
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
|
logger.error(error)
|
||||||
toastError('Invalid reset password link.')
|
toastError('Invalid reset password link.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,15 +12,15 @@ new class extends UnitTestCase {
|
||||||
.mockResolvedValue(factory.states('prospect')('user'))
|
.mockResolvedValue(factory.states('prospect')('user'))
|
||||||
|
|
||||||
const acceptMock = this.mock(invitationService, 'accept').mockResolvedValue({
|
const acceptMock = this.mock(invitationService, 'accept').mockResolvedValue({
|
||||||
token: 'my-api-token',
|
'token': 'my-api-token',
|
||||||
'audio-token': 'my-audio-token'
|
'audio-token': 'my-audio-token',
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.router.activateRoute({
|
await this.router.activateRoute({
|
||||||
path: '_',
|
path: '_',
|
||||||
screen: 'Invitation.Accept'
|
screen: 'Invitation.Accept',
|
||||||
}, {
|
}, {
|
||||||
token: 'my-token'
|
token: 'my-token',
|
||||||
})
|
})
|
||||||
|
|
||||||
this.render(AcceptInvitation)
|
this.render(AcceptInvitation)
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
<Btn :disabled="loading" class="!rounded-l-none" type="submit">Activate</Btn>
|
<Btn :disabled="loading" class="!rounded-l-none" type="submit">Activate</Btn>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { plusService } from '@/services'
|
import { plusService } from '@/services'
|
||||||
|
|
|
@ -13,8 +13,8 @@ new class extends UnitTestCase {
|
||||||
screen.getByTestId('buttons')
|
screen.getByTestId('buttons')
|
||||||
expect(screen.queryByTestId('activateForm')).toBeNull()
|
expect(screen.queryByTestId('activateForm')).toBeNull()
|
||||||
await this.user.click(screen.getByText('Purchase Koel Plus'))
|
await this.user.click(screen.getByText('Purchase Koel Plus'))
|
||||||
expect(global.LemonSqueezy.Url.Open).toHaveBeenCalledWith(
|
expect(globalThis.LemonSqueezy.Url.Open).toHaveBeenCalledWith(
|
||||||
'https://store.koel.dev/checkout/buy/42?embed=1&media=0&desc=0'
|
'https://store.koel.dev/checkout/buy/42?embed=1&media=0&desc=0',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,10 @@ import { useKoelPlus } from '@/composables'
|
||||||
import Btn from '@/components/ui/form/Btn.vue'
|
import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import ActivateLicenseForm from '@/components/koel-plus/ActivateLicenseForm.vue'
|
import ActivateLicenseForm from '@/components/koel-plus/ActivateLicenseForm.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
|
|
||||||
const { checkoutUrl } = useKoelPlus()
|
const { checkoutUrl } = useKoelPlus()
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const showingActivateLicenseForm = ref(false)
|
const showingActivateLicenseForm = ref(false)
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { it } from 'vitest'
|
||||||
import { screen, waitFor } from '@testing-library/vue'
|
import { screen, waitFor } from '@testing-library/vue'
|
||||||
import factory from '@/__tests__/factory'
|
import factory from '@/__tests__/factory'
|
||||||
import { eventBus } from '@/utils'
|
import { eventBus } from '@/utils'
|
||||||
import { Events } from '@/config'
|
import type { Events } from '@/config'
|
||||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
import ModalWrapper from './ModalWrapper.vue'
|
import ModalWrapper from './ModalWrapper.vue'
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ new class extends UnitTestCase {
|
||||||
['about-koel', 'MODAL_SHOW_ABOUT_KOEL', undefined],
|
['about-koel', 'MODAL_SHOW_ABOUT_KOEL', undefined],
|
||||||
['koel-plus', 'MODAL_SHOW_KOEL_PLUS', undefined],
|
['koel-plus', 'MODAL_SHOW_KOEL_PLUS', undefined],
|
||||||
['equalizer', 'MODAL_SHOW_EQUALIZER', undefined],
|
['equalizer', 'MODAL_SHOW_EQUALIZER', undefined],
|
||||||
['add-podcast-form', 'MODAL_SHOW_ADD_PODCAST_FORM', undefined]
|
['add-podcast-form', 'MODAL_SHOW_ADD_PODCAST_FORM', undefined],
|
||||||
])('shows %s modal', async (modalName, eventName, eventParams?: any) => {
|
])('shows %s modal', async (modalName, eventName, eventParams?: any) => {
|
||||||
this.render(ModalWrapper, {
|
this.render(ModalWrapper, {
|
||||||
global: {
|
global: {
|
||||||
|
@ -42,9 +42,9 @@ new class extends UnitTestCase {
|
||||||
Equalizer: this.stub('equalizer'),
|
Equalizer: this.stub('equalizer'),
|
||||||
InviteUserForm: this.stub('invite-user-form'),
|
InviteUserForm: this.stub('invite-user-form'),
|
||||||
KoelPlus: this.stub('koel-plus'),
|
KoelPlus: this.stub('koel-plus'),
|
||||||
PlaylistCollaborationModal: this.stub('playlist-collaboration')
|
PlaylistCollaborationModal: this.stub('playlist-collaboration'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
eventBus.emit(eventName, eventParams)
|
eventBus.emit(eventName, eventParams)
|
||||||
|
|
|
@ -28,7 +28,7 @@ const modalNameToComponentMap = {
|
||||||
'equalizer': defineAsyncComponent(() => import('@/components/ui/equalizer/Equalizer.vue')),
|
'equalizer': defineAsyncComponent(() => import('@/components/ui/equalizer/Equalizer.vue')),
|
||||||
'invite-user-form': defineAsyncComponent(() => import('@/components/user/InviteUserForm.vue')),
|
'invite-user-form': defineAsyncComponent(() => import('@/components/user/InviteUserForm.vue')),
|
||||||
'koel-plus': defineAsyncComponent(() => import('@/components/koel-plus/KoelPlusModal.vue')),
|
'koel-plus': defineAsyncComponent(() => import('@/components/koel-plus/KoelPlusModal.vue')),
|
||||||
'playlist-collaboration': defineAsyncComponent(() => import('@/components/playlist/PlaylistCollaborationModal.vue'))
|
'playlist-collaboration': defineAsyncComponent(() => import('@/components/playlist/PlaylistCollaborationModal.vue')),
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalName = keyof typeof modalNameToComponentMap
|
type ModalName = keyof typeof modalNameToComponentMap
|
||||||
|
@ -53,7 +53,7 @@ eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'
|
||||||
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', (folder, playables?) => {
|
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', (folder, playables?) => {
|
||||||
context.value = {
|
context.value = {
|
||||||
folder,
|
folder,
|
||||||
playables: playables ? arrayify(playables) : []
|
playables: playables ? arrayify(playables) : [],
|
||||||
}
|
}
|
||||||
|
|
||||||
activeModalName.value = 'create-playlist-form'
|
activeModalName.value = 'create-playlist-form'
|
||||||
|
@ -74,7 +74,7 @@ eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'
|
||||||
.on('MODAL_SHOW_EDIT_SONG_FORM', (songs, initialTab: EditSongFormTabName = 'details') => {
|
.on('MODAL_SHOW_EDIT_SONG_FORM', (songs, initialTab: EditSongFormTabName = 'details') => {
|
||||||
context.value = {
|
context.value = {
|
||||||
initialTab,
|
initialTab,
|
||||||
songs: arrayify(songs)
|
songs: arrayify(songs),
|
||||||
}
|
}
|
||||||
|
|
||||||
activeModalName.value = 'edit-song-form'
|
activeModalName.value = 'edit-song-form'
|
||||||
|
@ -95,10 +95,13 @@ eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
dialog {
|
dialog {
|
||||||
:deep(form), :deep(>div) {
|
:deep(form),
|
||||||
|
:deep(> div) {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
|
|
||||||
> header, > main, > footer {
|
> header,
|
||||||
|
> main,
|
||||||
|
> footer {
|
||||||
@apply px-6 py-5;
|
@apply px-6 py-5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,9 @@ new class extends UnitTestCase {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
Equalizer: this.stub('Equalizer'),
|
Equalizer: this.stub('Equalizer'),
|
||||||
Volume: this.stub('Volume')
|
Volume: this.stub('Volume'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { faBolt, faCompress, faExpand, faSliders } from '@fortawesome/free-solid-svg-icons'
|
import { faBolt, faCompress, faExpand, faSliders } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { eventBus, isAudioContextSupported as useEqualizer, isFullscreenSupported } from '@/utils'
|
import { eventBus, isFullscreenSupported, isAudioContextSupported as useEqualizer } from '@/utils'
|
||||||
import { useRouter } from '@/composables'
|
import { useRouter } from '@/composables'
|
||||||
|
|
||||||
import VolumeSlider from '@/components/ui/VolumeSlider.vue'
|
import VolumeSlider from '@/components/ui/VolumeSlider.vue'
|
||||||
|
|
|
@ -40,19 +40,19 @@ new class extends UnitTestCase {
|
||||||
artist_id: 3,
|
artist_id: 3,
|
||||||
album_name: 'Led Zeppelin IV',
|
album_name: 'Led Zeppelin IV',
|
||||||
album_id: 4,
|
album_id: 4,
|
||||||
liked: true
|
liked: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.render(Component, {
|
return this.render(Component, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
PlayButton: this.stub('PlayButton')
|
PlayButton: this.stub('PlayButton'),
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>CurrentPlayableKey]: ref(playable)
|
[<symbol>CurrentPlayableKey]: ref(playable),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ new class extends UnitTestCase {
|
||||||
it('goes back if current screen is Queue', async () => {
|
it('goes back if current screen is Queue', async () => {
|
||||||
this.router.$currentRoute.value = {
|
this.router.$currentRoute.value = {
|
||||||
screen: 'Queue',
|
screen: 'Queue',
|
||||||
path: '/queue'
|
path: '/queue',
|
||||||
}
|
}
|
||||||
|
|
||||||
const goMock = this.mock(Router, 'go')
|
const goMock = this.mock(Router, 'go')
|
||||||
|
|
|
@ -26,7 +26,7 @@ const { go, isCurrentScreen } = useRouter()
|
||||||
const { toastWarning, toastSuccess } = useMessageToaster()
|
const { toastWarning, toastSuccess } = useMessageToaster()
|
||||||
|
|
||||||
const { acceptsDrop, resolveDroppedItems } = useDroppable(
|
const { acceptsDrop, resolveDroppedItems } = useDroppable(
|
||||||
['playables', 'album', 'artist', 'playlist', 'playlist-folder']
|
['playables', 'album', 'artist', 'playlist', 'playlist-folder'],
|
||||||
)
|
)
|
||||||
|
|
||||||
const droppable = ref(false)
|
const droppable = ref(false)
|
||||||
|
|
|
@ -15,15 +15,15 @@ new class extends UnitTestCase {
|
||||||
album_cover: 'https://via.placeholder.com/150',
|
album_cover: 'https://via.placeholder.com/150',
|
||||||
playback_state: 'Playing',
|
playback_state: 'Playing',
|
||||||
artist_id: 10,
|
artist_id: 10,
|
||||||
artist_name: 'Led Zeppelin'
|
artist_name: 'Led Zeppelin',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(this.render(FooterSongInfo, {
|
expect(this.render(FooterSongInfo, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>CurrentPlayableKey]: ref(song)
|
[<symbol>CurrentPlayableKey]: ref(song),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}).html()).toMatchSnapshot()
|
}).html()).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,17 +29,23 @@ const { startDragging } = useDraggable('playables')
|
||||||
const song = requireInjection(CurrentPlayableKey, ref())
|
const song = requireInjection(CurrentPlayableKey, ref())
|
||||||
|
|
||||||
const cover = computed(() => {
|
const cover = computed(() => {
|
||||||
if (!song.value) return defaultCover
|
if (!song.value) {
|
||||||
|
return defaultCover
|
||||||
|
}
|
||||||
return getPlayableProp(song.value, 'album_cover', 'episode_image')
|
return getPlayableProp(song.value, 'album_cover', 'episode_image')
|
||||||
})
|
})
|
||||||
|
|
||||||
const artistOrPodcastUri = computed(() => {
|
const artistOrPodcastUri = computed(() => {
|
||||||
if (!song.value) return ''
|
if (!song.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
return isSong(song.value) ? `#/artist/${song.value?.artist_id}` : `#/podcasts/${song.value.podcast_id}`
|
return isSong(song.value) ? `#/artist/${song.value?.artist_id}` : `#/podcasts/${song.value.podcast_id}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const artistOrPodcastName = computed(() => {
|
const artistOrPodcastName = computed(() => {
|
||||||
if (!song.value) return ''
|
if (!song.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
return getPlayableProp(song.value, 'artist_name', 'podcast_title')
|
return getPlayableProp(song.value, 'artist_name', 'podcast_title')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,16 @@ const root = ref<HTMLElement>()
|
||||||
const artist = ref<Artist>()
|
const artist = ref<Artist>()
|
||||||
|
|
||||||
const requestContextMenu = (event: MouseEvent) => {
|
const requestContextMenu = (event: MouseEvent) => {
|
||||||
if (document.fullscreenElement) return
|
if (document.fullscreenElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
playable.value && eventBus.emit('PLAYABLE_CONTEXT_MENU_REQUESTED', event, playable.value)
|
playable.value && eventBus.emit('PLAYABLE_CONTEXT_MENU_REQUESTED', event, playable.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(playable, async () => {
|
watch(playable, async () => {
|
||||||
if (!playable.value) return
|
if (!playable.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (isSong(playable.value)) {
|
if (isSong(playable.value)) {
|
||||||
artist.value = await artistStore.resolve(playable.value.artist_id)
|
artist.value = await artistStore.resolve(playable.value.artist_id)
|
||||||
|
@ -51,7 +55,9 @@ watch(playable, async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const appBackgroundImage = computed(() => {
|
const appBackgroundImage = computed(() => {
|
||||||
if (!playable.value || !isSong(playable.value)) return 'none'
|
if (!playable.value || !isSong(playable.value)) {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
|
||||||
const src = artist.value?.image ?? playable.value.album_cover
|
const src = artist.value?.image ?? playable.value.album_cover
|
||||||
return src ? `url(${src})` : 'none'
|
return src ? `url(${src})` : 'none'
|
||||||
|
@ -71,7 +77,9 @@ const initPlaybackRelatedServices = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(preferenceStore.initialized, async initialized => {
|
watch(preferenceStore.initialized, async initialized => {
|
||||||
if (!initialized) return
|
if (!initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
await initPlaybackRelatedServices()
|
await initPlaybackRelatedServices()
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
@ -80,7 +88,9 @@ const setupControlHidingTimer = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const showControls = throttle(() => {
|
const showControls = throttle(() => {
|
||||||
if (!document.fullscreenElement) return
|
if (!document.fullscreenElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
root.value?.classList.remove('hide-controls')
|
root.value?.classList.remove('hide-controls')
|
||||||
window.clearTimeout(hideControlsTimeout)
|
window.clearTimeout(hideControlsTimeout)
|
||||||
|
@ -103,7 +113,7 @@ eventBus.on('FULLSCREEN_TOGGLE', () => toggleFullscreen())
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
footer {
|
footer {
|
||||||
box-shadow: 0 0 30px 20px rgba(0, 0, 0, .2);
|
box-shadow: 0 0 30px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
.fullscreen-backdrop {
|
.fullscreen-backdrop {
|
||||||
background-image: v-bind(appBackgroundImage);
|
background-image: v-bind(appBackgroundImage);
|
||||||
|
@ -119,17 +129,20 @@ footer {
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
@apply z-[3]
|
@apply z-[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@apply bg-black bg-repeat absolute top-0 left-0 opacity-50 z-[1] pointer-events-none -m-[20rem];
|
@apply bg-black bg-repeat absolute top-0 left-0 opacity-50 z-[1] pointer-events-none -m-[20rem];
|
||||||
content: '';
|
content: '';
|
||||||
background-image: linear-gradient(135deg, #111 25%, transparent 25%),
|
background-image: linear-gradient(135deg, #111 25%, transparent 25%),
|
||||||
linear-gradient(225deg, #111 25%, transparent 25%),
|
linear-gradient(225deg, #111 25%, transparent 25%), linear-gradient(45deg, #111 25%, transparent 25%),
|
||||||
linear-gradient(45deg, #111 25%, transparent 25%),
|
linear-gradient(315deg, #111 25%, rgba(255, 255, 255, 0) 25%);
|
||||||
linear-gradient(315deg, #111 25%, rgba(255, 255, 255, 0) 25%);
|
background-position:
|
||||||
background-position: 6px 0, 6px 0, 0 0, 0 0;
|
6px 0,
|
||||||
|
6px 0,
|
||||||
|
0 0,
|
||||||
|
0 0;
|
||||||
background-size: 6px 6px;
|
background-size: 6px 6px;
|
||||||
width: calc(100% + 40rem);
|
width: calc(100% + 40rem);
|
||||||
height: calc(100% + 40rem);
|
height: calc(100% + 40rem);
|
||||||
|
|
|
@ -31,7 +31,7 @@ new class extends UnitTestCase {
|
||||||
return this.render(MainContent, {
|
return this.render(MainContent, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>CurrentPlayableKey]: ref(factory('song'))
|
[<symbol>CurrentPlayableKey]: ref(factory('song')),
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
AlbumArtOverlay,
|
AlbumArtOverlay,
|
||||||
|
@ -45,9 +45,9 @@ new class extends UnitTestCase {
|
||||||
SearchExcerptsScreen: this.stub('search-excerpts-screen'),
|
SearchExcerptsScreen: this.stub('search-excerpts-screen'),
|
||||||
GenreScreen: this.stub('genre-screen'),
|
GenreScreen: this.stub('genre-screen'),
|
||||||
HomeScreen: this.stub(), // so that home overview requests are not made
|
HomeScreen: this.stub(), // so that home overview requests are not made
|
||||||
Visualizer: this.stub('visualizer')
|
Visualizer: this.stub('visualizer'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<ArtistScreen v-if="screen === 'Artist'" />
|
<ArtistScreen v-if="screen === 'Artist'" />
|
||||||
<SettingsScreen v-if="screen === 'Settings'" />
|
<SettingsScreen v-if="screen === 'Settings'" />
|
||||||
<ProfileScreen v-if="screen === 'Profile'" />
|
<ProfileScreen v-if="screen === 'Profile'" />
|
||||||
<PodcastScreen v-if="screen ==='Podcast'" />
|
<PodcastScreen v-if="screen === 'Podcast'" />
|
||||||
<EpisodeScreen v-if="screen === 'Episode'" />
|
<EpisodeScreen v-if="screen === 'Episode'" />
|
||||||
<UserListScreen v-if="screen === 'Users'" />
|
<UserListScreen v-if="screen === 'Users'" />
|
||||||
<YouTubeScreen v-if="useYouTube" v-show="screen === 'YouTube'" />
|
<YouTubeScreen v-if="useYouTube" v-show="screen === 'YouTube'" />
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { ref, Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { expect, it, Mock } from 'vitest'
|
import { ref } from 'vue'
|
||||||
import { RenderResult, screen, waitFor } from '@testing-library/vue'
|
import type { Mock } from 'vitest'
|
||||||
|
import { expect, it } from 'vitest'
|
||||||
|
import type { RenderResult } from '@testing-library/vue'
|
||||||
|
import { screen, waitFor } from '@testing-library/vue'
|
||||||
import factory from '@/__tests__/factory'
|
import factory from '@/__tests__/factory'
|
||||||
import { albumStore, artistStore, commonStore, preferenceStore } from '@/stores'
|
import { albumStore, artistStore, commonStore, preferenceStore } from '@/stores'
|
||||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
|
@ -80,12 +83,12 @@ new class extends UnitTestCase {
|
||||||
AlbumInfo: this.stub('album-info'),
|
AlbumInfo: this.stub('album-info'),
|
||||||
ArtistInfo: this.stub('artist-info'),
|
ArtistInfo: this.stub('artist-info'),
|
||||||
YouTubeVideoList: this.stub('youtube-video-list'),
|
YouTubeVideoList: this.stub('youtube-video-list'),
|
||||||
ExtraPanelTabHeader: this.stub()
|
ExtraPanelTabHeader: this.stub(),
|
||||||
},
|
},
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>CurrentPlayableKey]: songRef
|
[<symbol>CurrentPlayableKey]: songRef,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return [rendered, resolveArtistMock, resolveAlbumMock]
|
return [rendered, resolveArtistMock, resolveAlbumMock]
|
||||||
|
|
|
@ -73,7 +73,8 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import isMobile from 'ismobilejs'
|
import isMobile from 'ismobilejs'
|
||||||
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
import { faBars } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { computed, defineAsyncComponent, onMounted, ref, Ref, watch } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
|
||||||
import { albumStore, artistStore, preferenceStore } from '@/stores'
|
import { albumStore, artistStore, preferenceStore } from '@/stores'
|
||||||
import { useErrorHandler, useThirdPartyServices } from '@/composables'
|
import { useErrorHandler, useThirdPartyServices } from '@/composables'
|
||||||
import { eventBus, isSong, requireInjection } from '@/utils'
|
import { eventBus, isSong, requireInjection } from '@/utils'
|
||||||
|
@ -115,7 +116,9 @@ const fetchSongInfo = async (song: Song) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(playable, song => {
|
watch(playable, song => {
|
||||||
if (!song || !isSong(song)) return
|
if (!song || !isSong(song)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
fetchSongInfo(song)
|
fetchSongInfo(song)
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
@ -134,7 +137,7 @@ onMounted(() => isMobile.any || (activeTab.value = preferenceStore.active_extra_
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.btn-group {
|
.btn-group {
|
||||||
@apply flex md:flex-col justify-between items-center gap-1 md:gap-3
|
@apply flex md:flex-col justify-between items-center gap-1 md:gap-3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -46,7 +46,7 @@ import { useThirdPartyServices } from '@/composables'
|
||||||
import ExtraDrawerButton from '@/components/layout/main-wrapper/extra-drawer/ExtraDrawerButton.vue'
|
import ExtraDrawerButton from '@/components/layout/main-wrapper/extra-drawer/ExtraDrawerButton.vue'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ modelValue?: ExtraPanelTab | null }>(), {
|
const props = withDefaults(defineProps<{ modelValue?: ExtraPanelTab | null }>(), {
|
||||||
modelValue: null
|
modelValue: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'update:modelValue', value: ExtraPanelTab | null): void }>()
|
const emit = defineEmits<{ (e: 'update:modelValue', value: ExtraPanelTab | null): void }>()
|
||||||
|
@ -55,7 +55,7 @@ const { useYouTube } = useThirdPartyServices()
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => emit('update:modelValue', value)
|
set: value => emit('update:modelValue', value),
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleTab = (tab: ExtraPanelTab) => (value.value = value.value === tab ? null : tab)
|
const toggleTab = (tab: ExtraPanelTab) => (value.value = value.value === tab ? null : tab)
|
||||||
|
|
|
@ -67,7 +67,9 @@ const toggle = () => (opened.value = !opened.value)
|
||||||
const onDragStart = (event: DragEvent) => startDragging(event, folder.value)
|
const onDragStart = (event: DragEvent) => startDragging(event, folder.value)
|
||||||
|
|
||||||
const onDragOver = (event: DragEvent) => {
|
const onDragOver = (event: DragEvent) => {
|
||||||
if (!acceptsDrop(event)) return false
|
if (!acceptsDrop(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
droppable.value = true
|
droppable.value = true
|
||||||
|
@ -79,12 +81,16 @@ const onDragLeave = () => (droppable.value = false)
|
||||||
const onDrop = async (event: DragEvent) => {
|
const onDrop = async (event: DragEvent) => {
|
||||||
droppable.value = false
|
droppable.value = false
|
||||||
|
|
||||||
if (!acceptsDrop(event)) return false
|
if (!acceptsDrop(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
const playlist = await resolveDroppedValue<Playlist>(event)
|
const playlist = await resolveDroppedValue<Playlist>(event)
|
||||||
if (!playlist || playlist.folder_id === folder.value.id) return
|
if (!playlist || playlist.folder_id === folder.value.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
await playlistFolderStore.addPlaylistToFolder(folder.value, playlist)
|
await playlistFolderStore.addPlaylistToFolder(folder.value, playlist)
|
||||||
}
|
}
|
||||||
|
@ -92,7 +98,9 @@ const onDrop = async (event: DragEvent) => {
|
||||||
const onDragLeaveHatch = () => (droppableOnHatch.value = false)
|
const onDragLeaveHatch = () => (droppableOnHatch.value = false)
|
||||||
|
|
||||||
const onDragOverHatch = (event: DragEvent) => {
|
const onDragOverHatch = (event: DragEvent) => {
|
||||||
if (!acceptsDrop(event)) return false
|
if (!acceptsDrop(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
droppableOnHatch.value = true
|
droppableOnHatch.value = true
|
||||||
|
@ -105,7 +113,9 @@ const onDropOnHatch = async (event: DragEvent) => {
|
||||||
const playlist = (await resolveDroppedValue<Playlist>(event))!
|
const playlist = (await resolveDroppedValue<Playlist>(event))!
|
||||||
|
|
||||||
// if the playlist isn't in the folder, don't do anything. The folder will handle the drop.
|
// if the playlist isn't in the folder, don't do anything. The folder will handle the drop.
|
||||||
if (playlist.folder_id !== folder.value.id) return
|
if (playlist.folder_id !== folder.value.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// otherwise, the user is trying to remove the playlist from the folder.
|
// otherwise, the user is trying to remove the playlist from the folder.
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -115,7 +125,7 @@ const onDropOnHatch = async (event: DragEvent) => {
|
||||||
const onContextMenu = (event: MouseEvent) => eventBus.emit(
|
const onContextMenu = (event: MouseEvent) => eventBus.emit(
|
||||||
'PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED',
|
'PLAYLIST_FOLDER_CONTEXT_MENU_REQUESTED',
|
||||||
event,
|
event,
|
||||||
folder.value
|
folder.value,
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,8 @@ new class extends UnitTestCase {
|
||||||
renderComponent (list: PlaylistLike) {
|
renderComponent (list: PlaylistLike) {
|
||||||
this.render(PlaylistSidebarItem, {
|
this.render(PlaylistSidebarItem, {
|
||||||
props: {
|
props: {
|
||||||
list
|
list,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,10 +26,10 @@ new class extends UnitTestCase {
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each<FavoriteList['name'] | RecentlyPlayedList['name']>(['Favorites', 'Recently Played'])
|
it.each<FavoriteList['name'] | RecentlyPlayedList['name']>(['Favorites', 'Recently Played'])
|
||||||
('does not request context menu if not playlist', async (name) => {
|
('does not request context menu if not playlist', async name => { // eslint-disable-line no-unexpected-multiline
|
||||||
const list: FavoriteList | RecentlyPlayedList = {
|
const list: FavoriteList | RecentlyPlayedList = {
|
||||||
name,
|
name,
|
||||||
songs: []
|
songs: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitMock = this.mock(eventBus, 'emit')
|
const emitMock = this.mock(eventBus, 'emit')
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { useDraggable, useDroppable, usePlaylistManagement, useRouter } from '@/
|
||||||
|
|
||||||
import SidebarItem from '@/components/layout/main-wrapper/sidebar/SidebarItem.vue'
|
import SidebarItem from '@/components/layout/main-wrapper/sidebar/SidebarItem.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ list: PlaylistLike }>()
|
||||||
const { onRouteChanged } = useRouter()
|
const { onRouteChanged } = useRouter()
|
||||||
const { startDragging } = useDraggable('playlist')
|
const { startDragging } = useDraggable('playlist')
|
||||||
const { acceptsDrop, resolveDroppedItems } = useDroppable(['playables', 'album', 'artist'])
|
const { acceptsDrop, resolveDroppedItems } = useDroppable(['playables', 'album', 'artist'])
|
||||||
|
@ -39,7 +40,6 @@ const droppable = ref(false)
|
||||||
|
|
||||||
const { addToPlaylist } = usePlaylistManagement()
|
const { addToPlaylist } = usePlaylistManagement()
|
||||||
|
|
||||||
const props = defineProps<{ list: PlaylistLike }>()
|
|
||||||
const { list } = toRefs(props)
|
const { list } = toRefs(props)
|
||||||
|
|
||||||
const isPlaylist = (list: PlaylistLike): list is Playlist => 'id' in list
|
const isPlaylist = (list: PlaylistLike): list is Playlist => 'id' in list
|
||||||
|
@ -49,16 +49,26 @@ const isRecentlyPlayedList = (list: PlaylistLike): list is RecentlyPlayedList =>
|
||||||
const current = ref(false)
|
const current = ref(false)
|
||||||
|
|
||||||
const url = computed(() => {
|
const url = computed(() => {
|
||||||
if (isPlaylist(list.value)) return `#/playlist/${list.value.id}`
|
if (isPlaylist(list.value)) {
|
||||||
if (isFavoriteList(list.value)) return '#/favorites'
|
return `#/playlist/${list.value.id}`
|
||||||
if (isRecentlyPlayedList(list.value)) return '#/recently-played'
|
}
|
||||||
|
if (isFavoriteList(list.value)) {
|
||||||
|
return '#/favorites'
|
||||||
|
}
|
||||||
|
if (isRecentlyPlayedList(list.value)) {
|
||||||
|
return '#/recently-played'
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Invalid playlist-like type.')
|
throw new Error('Invalid playlist-like type.')
|
||||||
})
|
})
|
||||||
|
|
||||||
const contentEditable = computed(() => {
|
const contentEditable = computed(() => {
|
||||||
if (isRecentlyPlayedList(list.value)) return false
|
if (isRecentlyPlayedList(list.value)) {
|
||||||
if (isFavoriteList(list.value)) return true
|
return false
|
||||||
|
}
|
||||||
|
if (isFavoriteList(list.value)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return !list.value.is_smart
|
return !list.value.is_smart
|
||||||
})
|
})
|
||||||
|
@ -73,8 +83,12 @@ const onContextMenu = (event: MouseEvent) => {
|
||||||
const onDragStart = (event: DragEvent) => isPlaylist(list.value) && startDragging(event, list.value)
|
const onDragStart = (event: DragEvent) => isPlaylist(list.value) && startDragging(event, list.value)
|
||||||
|
|
||||||
const onDragOver = (event: DragEvent) => {
|
const onDragOver = (event: DragEvent) => {
|
||||||
if (!contentEditable.value) return false
|
if (!contentEditable.value) {
|
||||||
if (!acceptsDrop(event)) return false
|
return false
|
||||||
|
}
|
||||||
|
if (!acceptsDrop(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
droppable.value = true
|
droppable.value = true
|
||||||
|
@ -87,12 +101,18 @@ const onDragLeave = () => (droppable.value = false)
|
||||||
const onDrop = async (event: DragEvent) => {
|
const onDrop = async (event: DragEvent) => {
|
||||||
droppable.value = false
|
droppable.value = false
|
||||||
|
|
||||||
if (!contentEditable.value) return false
|
if (!contentEditable.value) {
|
||||||
if (!acceptsDrop(event)) return false
|
return false
|
||||||
|
}
|
||||||
|
if (!acceptsDrop(event)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const playables = await resolveDroppedItems(event)
|
const playables = await resolveDroppedItems(event)
|
||||||
|
|
||||||
if (!playables?.length) return false
|
if (!playables?.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (isFavoriteList(list.value)) {
|
if (isFavoriteList(list.value)) {
|
||||||
await favoriteStore.like(playables)
|
await favoriteStore.like(playables)
|
||||||
|
|
|
@ -11,7 +11,7 @@ const standardItems = [
|
||||||
'Artists',
|
'Artists',
|
||||||
'Genres',
|
'Genres',
|
||||||
'Favorites',
|
'Favorites',
|
||||||
'Recently Played'
|
'Recently Played',
|
||||||
]
|
]
|
||||||
|
|
||||||
const adminItems = [...standardItems, 'Users', 'Upload', 'Settings']
|
const adminItems = [...standardItems, 'Users', 'Upload', 'Settings']
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<nav
|
<nav
|
||||||
:class="{ collapsed: !expanded, 'tmp-showing': tmpShowing, showing: mobileShowing }"
|
:class="{ 'collapsed': !expanded, 'tmp-showing': tmpShowing, 'showing': mobileShowing }"
|
||||||
class="group left-0 top-0 flex flex-col fixed h-full w-full md:relative md:w-k-sidebar-width z-[999] md:z-10"
|
class="group left-0 top-0 flex flex-col fixed h-full w-full md:relative md:w-k-sidebar-width z-[999] md:z-10"
|
||||||
@mouseenter="onMouseEnter"
|
@mouseenter="onMouseEnter"
|
||||||
@mouseleave="onMouseLeave"
|
@mouseleave="onMouseLeave"
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<SidebarToggleButton
|
<SidebarToggleButton
|
||||||
class="opacity-0 no-hover:hidden group-hover:opacity-100 transition"
|
|
||||||
v-model="expanded"
|
v-model="expanded"
|
||||||
|
class="opacity-0 no-hover:hidden group-hover:opacity-100 transition"
|
||||||
:class="expanded || 'opacity-100'"
|
:class="expanded || 'opacity-100'"
|
||||||
/>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -68,10 +68,14 @@ let tmpShowingHandler: number | undefined
|
||||||
const tmpShowing = ref(false)
|
const tmpShowing = ref(false)
|
||||||
|
|
||||||
const onMouseEnter = () => {
|
const onMouseEnter = () => {
|
||||||
if (expanded.value) return;
|
if (expanded.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tmpShowingHandler = window.setTimeout(() => {
|
tmpShowingHandler = window.setTimeout(() => {
|
||||||
if (expanded.value) return
|
if (expanded.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
tmpShowing.value = true
|
tmpShowing.value = true
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
@ -132,7 +136,7 @@ nav {
|
||||||
@mixin themed-background;
|
@mixin themed-background;
|
||||||
|
|
||||||
transform: translateX(-100vw);
|
transform: translateX(-100vw);
|
||||||
transition: transform .2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
|
|
||||||
&.showing {
|
&.showing {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
|
|
|
@ -14,7 +14,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
await this.router.activateRoute({
|
await this.router.activateRoute({
|
||||||
screen: 'Home',
|
screen: 'Home',
|
||||||
path: '_'
|
path: '_',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(screen.getByTestId('sidebar-item').classList.contains('current')).toBe(true)
|
expect(screen.getByTestId('sidebar-item').classList.contains('current')).toBe(true)
|
||||||
|
@ -34,11 +34,11 @@ new class extends UnitTestCase {
|
||||||
props: {
|
props: {
|
||||||
icon: faHome,
|
icon: faHome,
|
||||||
href: '#',
|
href: '#',
|
||||||
screen: 'Home'
|
screen: 'Home',
|
||||||
},
|
},
|
||||||
slots: {
|
slots: {
|
||||||
default: 'Home'
|
default: 'Home',
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,9 +27,9 @@ import { ref } from 'vue'
|
||||||
import { useRouter } from '@/composables'
|
import { useRouter } from '@/composables'
|
||||||
import { eventBus } from '@/utils'
|
import { eventBus } from '@/utils'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{ href?: string | undefined; screen?: ScreenName | undefined }>(), {
|
const props = withDefaults(defineProps<{ href?: string | undefined, screen?: ScreenName | undefined }>(), {
|
||||||
href: undefined,
|
href: undefined,
|
||||||
screen: undefined
|
screen: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const current = ref(false)
|
const current = ref(false)
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { faTools, faUpload, faUsers } from '@fortawesome/free-solid-svg-icons'
|
import { faTools, faUpload, faUsers } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { useAuthorization, useUpload } from '@/composables'
|
import { useAuthorization, useUpload } from '@/composables'
|
||||||
|
|
|
@ -13,7 +13,7 @@ new class extends UnitTestCase {
|
||||||
playlistStore.state.playlists = [
|
playlistStore.state.playlists = [
|
||||||
factory.states('orphan')('playlist', { name: 'Foo Playlist' }),
|
factory.states('orphan')('playlist', { name: 'Foo Playlist' }),
|
||||||
factory.states('orphan')('playlist', { name: 'Bar Playlist' }),
|
factory.states('orphan')('playlist', { name: 'Bar Playlist' }),
|
||||||
factory.states('smart', 'orphan')('playlist', { name: 'Smart Playlist' })
|
factory.states('smart', 'orphan')('playlist', { name: 'Smart Playlist' }),
|
||||||
]
|
]
|
||||||
|
|
||||||
this.renderComponent()
|
this.renderComponent()
|
||||||
|
@ -26,7 +26,7 @@ new class extends UnitTestCase {
|
||||||
it('displays playlist folders', () => {
|
it('displays playlist folders', () => {
|
||||||
playlistFolderStore.state.folders = [
|
playlistFolderStore.state.folders = [
|
||||||
factory('playlist-folder', { name: 'Foo Folder' }),
|
factory('playlist-folder', { name: 'Foo Folder' }),
|
||||||
factory('playlist-folder', { name: 'Bar Folder' })
|
factory('playlist-folder', { name: 'Bar Folder' }),
|
||||||
]
|
]
|
||||||
|
|
||||||
this.renderComponent()
|
this.renderComponent()
|
||||||
|
@ -39,9 +39,9 @@ new class extends UnitTestCase {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
PlaylistSidebarItem,
|
PlaylistSidebarItem,
|
||||||
PlaylistFolderSidebarItem
|
PlaylistFolderSidebarItem,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,9 @@ const playlists = toRef(playlistStore.state, 'playlists')
|
||||||
const favorites = toRef(favoriteStore.state, 'playables')
|
const favorites = toRef(favoriteStore.state, 'playables')
|
||||||
|
|
||||||
const orphanPlaylists = computed(() => playlists.value.filter(({ folder_id }) => {
|
const orphanPlaylists = computed(() => playlists.value.filter(({ folder_id }) => {
|
||||||
if (folder_id === null) return true
|
if (folder_id === null) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// if the playlist's folder is not found, it's an orphan
|
// if the playlist's folder is not found, it's an orphan
|
||||||
// this can happen if the playlist belongs to another user (collaborative playlist)
|
// this can happen if the playlist belongs to another user (collaborative playlist)
|
||||||
|
|
|
@ -18,6 +18,6 @@ const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void }>()
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => emit('update:modelValue', value)
|
set: value => emit('update:modelValue', value),
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -39,9 +39,9 @@ new class extends UnitTestCase {
|
||||||
return this.render(AboutKoelModel, {
|
return this.render(AboutKoelModel, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
SponsorList: this.stub('sponsor-list')
|
SponsorList: this.stub('sponsor-list'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@
|
||||||
<a href="https://github.com/phanan" rel="noopener" target="_blank">Phan An</a>
|
<a href="https://github.com/phanan" rel="noopener" target="_blank">Phan An</a>
|
||||||
and quite a few
|
and quite a few
|
||||||
<a href="https://github.com/koel/core/graphs/contributors" rel="noopener" target="_blank">awesome</a> <a
|
<a href="https://github.com/koel/core/graphs/contributors" rel="noopener" target="_blank">awesome</a> <a
|
||||||
href="https://github.com/koel/koel/graphs/contributors" rel="noopener" target="_blank"
|
href="https://github.com/koel/koel/graphs/contributors" rel="noopener" target="_blank"
|
||||||
>contributors</a>.
|
>contributors</a>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<CreditsBlock v-if="isDemo" />
|
<CreditsBlock v-if="isDemo" />
|
||||||
|
@ -71,17 +71,18 @@ import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import BtnUpgradeToPlus from '@/components/koel-plus/BtnUpgradeToPlus.vue'
|
import BtnUpgradeToPlus from '@/components/koel-plus/BtnUpgradeToPlus.vue'
|
||||||
import CreditsBlock from '@/components/meta/CreditsBlock.vue'
|
import CreditsBlock from '@/components/meta/CreditsBlock.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldNotifyNewVersion,
|
shouldNotifyNewVersion,
|
||||||
currentVersion,
|
currentVersion,
|
||||||
latestVersion,
|
latestVersion,
|
||||||
latestVersionReleaseUrl
|
latestVersionReleaseUrl,
|
||||||
} = useNewVersionNotification()
|
} = useNewVersionNotification()
|
||||||
|
|
||||||
const { isPlus, license } = useKoelPlus()
|
const { isPlus, license } = useKoelPlus()
|
||||||
const { isAdmin } = useAuthorization()
|
const { isAdmin } = useAuthorization()
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const showPlusModal = () => {
|
const showPlusModal = () => {
|
||||||
|
@ -89,7 +90,7 @@ const showPlusModal = () => {
|
||||||
eventBus.emit('MODAL_SHOW_KOEL_PLUS')
|
eventBus.emit('MODAL_SHOW_KOEL_PLUS')
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDemo = window.IS_DEMO;
|
const isDemo = window.IS_DEMO
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|
|
@ -11,7 +11,7 @@ new class extends UnitTestCase {
|
||||||
const getMock = this.mock(http, 'get').mockResolvedValue([
|
const getMock = this.mock(http, 'get').mockResolvedValue([
|
||||||
{ name: 'Foo', url: 'https://foo.com' },
|
{ name: 'Foo', url: 'https://foo.com' },
|
||||||
{ name: 'Bar', url: 'https://bar.com' },
|
{ name: 'Bar', url: 'https://bar.com' },
|
||||||
{ name: 'Something Else', url: 'https://something-else.net' }
|
{ name: 'Something Else', url: 'https://something-else.net' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const { html } = this.render(CreditsBlock)
|
const { html } = this.render(CreditsBlock)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { orderBy } from 'lodash'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { http } from '@/services'
|
import { http } from '@/services'
|
||||||
|
|
||||||
type DemoCredits = {
|
interface DemoCredits {
|
||||||
name: string
|
name: string
|
||||||
url: string
|
url: string
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ onMounted(async () => {
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
li&:last-child {
|
li&:last-child {
|
||||||
&::before {
|
&::before {
|
||||||
content: ', and '
|
content: ', and ';
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
|
|
@ -9,7 +9,7 @@ new class extends UnitTestCase {
|
||||||
protected beforeEach () {
|
protected beforeEach () {
|
||||||
// Prevent actual HTTP requests from being made
|
// Prevent actual HTTP requests from being made
|
||||||
this.setReadOnlyProperty(http, 'silently', {
|
this.setReadOnlyProperty(http, 'silently', {
|
||||||
patch: vi.fn()
|
patch: vi.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
super.beforeEach(() => vi.useFakeTimers())
|
super.beforeEach(() => vi.useFakeTimers())
|
||||||
|
|
|
@ -38,9 +38,15 @@ const stopBugging = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(preferenceStore.initialized, initialized => {
|
watch(preferenceStore.initialized, initialized => {
|
||||||
if (!initialized) return
|
if (!initialized) {
|
||||||
if (preferenceStore.state.support_bar_no_bugging || isMobile.any) return
|
return
|
||||||
if (isPlus.value) return
|
}
|
||||||
|
if (preferenceStore.state.support_bar_no_bugging || isMobile.any) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isPlus.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setUpShowBarTimeout()
|
setUpShowBarTimeout()
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { expect, it } from 'vitest'
|
||||||
import { screen, waitFor } from '@testing-library/vue'
|
import { screen, waitFor } from '@testing-library/vue'
|
||||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||||
import { eventBus } from '@/utils'
|
import { eventBus } from '@/utils'
|
||||||
import { Events } from '@/config'
|
import type { Events } from '@/config'
|
||||||
|
|
||||||
import CreateNewPlaylistContextMenu from './CreatePlaylistContextMenu.vue'
|
import CreateNewPlaylistContextMenu from './CreatePlaylistContextMenu.vue'
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ new class extends UnitTestCase {
|
||||||
it.each<[string, keyof Events]>([
|
it.each<[string, keyof Events]>([
|
||||||
['playlist-context-menu-create-simple', 'MODAL_SHOW_CREATE_PLAYLIST_FORM'],
|
['playlist-context-menu-create-simple', 'MODAL_SHOW_CREATE_PLAYLIST_FORM'],
|
||||||
['playlist-context-menu-create-smart', 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM'],
|
['playlist-context-menu-create-smart', 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM'],
|
||||||
['playlist-context-menu-create-folder', 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM']
|
['playlist-context-menu-create-folder', 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM'],
|
||||||
])('when clicking on %s, should emit %s', async (id, eventName) => {
|
])('when clicking on %s, should emit %s', async (id, eventName) => {
|
||||||
await this.renderComponent()
|
await this.renderComponent()
|
||||||
const emitMock = this.mock(eventBus, 'emit')
|
const emitMock = this.mock(eventBus, 'emit')
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { useContextMenu } from '@/composables'
|
import { useContextMenu } from '@/composables'
|
||||||
import { eventBus } from '@/utils'
|
import { eventBus } from '@/utils'
|
||||||
import { Events } from '@/config'
|
import type { Events } from '@/config'
|
||||||
|
|
||||||
const { base, ContextMenu, open, trigger } = useContextMenu()
|
const { base, ContextMenu, open, trigger } = useContextMenu()
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ type Action = 'new-playlist' | 'new-smart-playlist' | 'new-folder'
|
||||||
const actionToEventMap: Record<Action, keyof Events> = {
|
const actionToEventMap: Record<Action, keyof Events> = {
|
||||||
'new-playlist': 'MODAL_SHOW_CREATE_PLAYLIST_FORM',
|
'new-playlist': 'MODAL_SHOW_CREATE_PLAYLIST_FORM',
|
||||||
'new-smart-playlist': 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM',
|
'new-smart-playlist': 'MODAL_SHOW_CREATE_SMART_PLAYLIST_FORM',
|
||||||
'new-folder': 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM'
|
'new-folder': 'MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM',
|
||||||
}
|
}
|
||||||
|
|
||||||
const onItemClicked = (key: keyof typeof actionToEventMap) => trigger(() => eventBus.emit(actionToEventMap[key]))
|
const onItemClicked = (key: keyof typeof actionToEventMap) => trigger(() => eventBus.emit(actionToEventMap[key]))
|
||||||
|
|
|
@ -18,7 +18,7 @@ const requestContextMenu = (e: MouseEvent) => {
|
||||||
|
|
||||||
eventBus.emit('CREATE_NEW_PLAYLIST_CONTEXT_MENU_REQUESTED', {
|
eventBus.emit('CREATE_NEW_PLAYLIST_CONTEXT_MENU_REQUESTED', {
|
||||||
top: bottom,
|
top: bottom,
|
||||||
left: right
|
left: right,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -32,13 +32,13 @@ import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
|
|
@ -16,9 +16,9 @@ new class extends UnitTestCase {
|
||||||
this.render(CreatePlaylistForm, {
|
this.render(CreatePlaylistForm, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>ModalContextKey]: [ref({ folder })]
|
[<symbol>ModalContextKey]: [ref({ folder })],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(screen.queryByTestId('from-playables')).toBeNull()
|
expect(screen.queryByTestId('from-playables')).toBeNull()
|
||||||
|
@ -27,7 +27,7 @@ new class extends UnitTestCase {
|
||||||
await this.user.click(screen.getByRole('button', { name: 'Save' }))
|
await this.user.click(screen.getByRole('button', { name: 'Save' }))
|
||||||
|
|
||||||
expect(storeMock).toHaveBeenCalledWith('My playlist', {
|
expect(storeMock).toHaveBeenCalledWith('My playlist', {
|
||||||
folder_id: folder.id
|
folder_id: folder.id,
|
||||||
}, [])
|
}, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ new class extends UnitTestCase {
|
||||||
this.render(CreatePlaylistForm, {
|
this.render(CreatePlaylistForm, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>ModalContextKey]: [ref({ folder, playables })]
|
[<symbol>ModalContextKey]: [ref({ folder, playables })],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
screen.getByText('from 3 songs')
|
screen.getByText('from 3 songs')
|
||||||
|
@ -50,7 +50,7 @@ new class extends UnitTestCase {
|
||||||
await this.user.click(screen.getByRole('button', { name: 'Save' }))
|
await this.user.click(screen.getByRole('button', { name: 'Save' }))
|
||||||
|
|
||||||
expect(storeMock).toHaveBeenCalledWith('My playlist', {
|
expect(storeMock).toHaveBeenCalledWith('My playlist', {
|
||||||
folder_id: folder.id
|
folder_id: folder.id,
|
||||||
}, playables)
|
}, playables)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
@ -56,7 +57,6 @@ const folderId = ref(targetFolder?.id)
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const noun = computed(() => {
|
const noun = computed(() => {
|
||||||
|
@ -75,7 +75,7 @@ const submit = async () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const playlist = await playlistStore.store(name.value, {
|
const playlist = await playlistStore.store(name.value, {
|
||||||
folder_id: folderId.value
|
folder_id: folderId.value,
|
||||||
}, playables)
|
}, playables)
|
||||||
|
|
||||||
close()
|
close()
|
||||||
|
|
|
@ -15,9 +15,9 @@ new class extends UnitTestCase {
|
||||||
this.render(EditPlaylistFolderForm, {
|
this.render(EditPlaylistFolderForm, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>ModalContextKey]: [ref({ folder })]
|
[<symbol>ModalContextKey]: [ref({ folder })],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.type(screen.getByPlaceholderText('Folder name'), 'Your folder')
|
await this.type(screen.getByPlaceholderText('Folder name'), 'Your folder')
|
||||||
|
|
|
@ -26,6 +26,7 @@ import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
@ -33,6 +34,8 @@ const folder = useModal().getFromContext<PlaylistFolder>('folder')
|
||||||
|
|
||||||
const name = ref(folder.name)
|
const name = ref(folder.name)
|
||||||
|
|
||||||
|
const close = () => emit('close')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
showOverlay()
|
showOverlay()
|
||||||
|
|
||||||
|
@ -47,9 +50,6 @@ const submit = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
|
||||||
|
|
||||||
const maybeClose = async () => {
|
const maybeClose = async () => {
|
||||||
if (name.value.trim() === folder.name) {
|
if (name.value.trim() === folder.name) {
|
||||||
close()
|
close()
|
||||||
|
|
|
@ -14,7 +14,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
const playlist = factory('playlist', {
|
const playlist = factory('playlist', {
|
||||||
name: 'My playlist',
|
name: 'My playlist',
|
||||||
folder_id: playlistFolderStore.state.folders[0].id
|
folder_id: playlistFolderStore.state.folders[0].id,
|
||||||
})
|
})
|
||||||
|
|
||||||
playlistStore.state.playlists = [playlist]
|
playlistStore.state.playlists = [playlist]
|
||||||
|
@ -24,9 +24,9 @@ new class extends UnitTestCase {
|
||||||
this.render(EditPlaylistForm, {
|
this.render(EditPlaylistForm, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>ModalContextKey]: [ref({ playlist })]
|
[<symbol>ModalContextKey]: [ref({ playlist })],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.type(screen.getByPlaceholderText('Playlist name'), 'Your playlist')
|
await this.type(screen.getByPlaceholderText('Playlist name'), 'Your playlist')
|
||||||
|
@ -35,7 +35,7 @@ new class extends UnitTestCase {
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(updateMock).toHaveBeenCalledWith(playlist, {
|
expect(updateMock).toHaveBeenCalledWith(playlist, {
|
||||||
name: 'Your playlist',
|
name: 'Your playlist',
|
||||||
folder_id: playlist.folder_id
|
folder_id: playlist.folder_id,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -44,6 +44,7 @@ import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
@ -53,7 +54,6 @@ const name = ref(playlist.name)
|
||||||
const folderId = ref(playlist.folder_id)
|
const folderId = ref(playlist.folder_id)
|
||||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
|
@ -62,7 +62,7 @@ const submit = async () => {
|
||||||
try {
|
try {
|
||||||
await playlistStore.update(playlist, {
|
await playlistStore.update(playlist, {
|
||||||
name: name.value,
|
name: name.value,
|
||||||
folder_id: folderId.value
|
folder_id: folderId.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
toastSuccess('Playlist updated.')
|
toastSuccess('Playlist updated.')
|
||||||
|
@ -92,6 +92,6 @@ form {
|
||||||
}
|
}
|
||||||
|
|
||||||
label.folder {
|
label.folder {
|
||||||
flex: .6;
|
flex: 0.6;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,8 +13,8 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
this.render(Component, {
|
this.render(Component, {
|
||||||
props: {
|
props: {
|
||||||
playlist
|
playlist,
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.user.click(screen.getByText('Invite'))
|
await this.user.click(screen.getByText('Invite'))
|
||||||
|
|
|
@ -11,13 +11,13 @@ new class extends UnitTestCase {
|
||||||
const { html } = this.render(Modal, {
|
const { html } = this.render(Modal, {
|
||||||
global: {
|
global: {
|
||||||
provide: {
|
provide: {
|
||||||
[<symbol>ModalContextKey]: [ref({ playlist: factory('playlist') })]
|
[<symbol>ModalContextKey]: [ref({ playlist: factory('playlist') })],
|
||||||
},
|
},
|
||||||
stubs: {
|
stubs: {
|
||||||
InviteCollaborators: this.stub('InviteCollaborators'),
|
InviteCollaborators: this.stub('InviteCollaborators'),
|
||||||
CollaboratorList: this.stub('CollaboratorList')
|
CollaboratorList: this.stub('CollaboratorList'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(html()).toMatchSnapshot()
|
expect(html()).toMatchSnapshot()
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
<div
|
<div
|
||||||
class="collaboration-modal max-w-[640px]"
|
class="collaboration-modal max-w-[640px]"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keydown.esc="close"
|
|
||||||
data-testid="playlist-collaboration"
|
data-testid="playlist-collaboration"
|
||||||
|
@keydown.esc="close"
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<h1>Playlist Collaboration</h1>
|
<h1>Playlist Collaboration</h1>
|
||||||
|
@ -41,12 +41,12 @@ import Btn from '@/components/ui/form/Btn.vue'
|
||||||
import InviteCollaborators from '@/components/playlist/InvitePlaylistCollaborators.vue'
|
import InviteCollaborators from '@/components/playlist/InvitePlaylistCollaborators.vue'
|
||||||
import CollaboratorList from '@/components/playlist/PlaylistCollaboratorList.vue'
|
import CollaboratorList from '@/components/playlist/PlaylistCollaboratorList.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const playlist = useModal().getFromContext<Playlist>('playlist')
|
const playlist = useModal().getFromContext<Playlist>('playlist')
|
||||||
const { currentUser } = useAuthorization()
|
const { currentUser } = useAuthorization()
|
||||||
|
|
||||||
const canManageCollaborators = computed(() => currentUser.value?.id === playlist.user_id)
|
const canManageCollaborators = computed(() => currentUser.value?.id === playlist.user_id)
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ new class extends UnitTestCase {
|
||||||
protected test () {
|
protected test () {
|
||||||
it('renders', async () => {
|
it('renders', async () => {
|
||||||
const playlist = factory('playlist', {
|
const playlist = factory('playlist', {
|
||||||
is_collaborative: true
|
is_collaborative: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fetchMock = this.mock(playlistCollaborationService, 'fetchCollaborators').mockResolvedValue(
|
const fetchMock = this.mock(playlistCollaborationService, 'fetchCollaborators').mockResolvedValue(
|
||||||
factory('playlist-collaborator', 5)
|
factory('playlist-collaborator', 5),
|
||||||
)
|
)
|
||||||
|
|
||||||
const { html } = await this.be().renderComponent(playlist)
|
const { html } = await this.be().renderComponent(playlist)
|
||||||
|
@ -24,13 +24,13 @@ new class extends UnitTestCase {
|
||||||
private async renderComponent (playlist: Playlist) {
|
private async renderComponent (playlist: Playlist) {
|
||||||
const rendered = this.render(Component, {
|
const rendered = this.render(Component, {
|
||||||
props: {
|
props: {
|
||||||
playlist
|
playlist,
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
ListItem: this.stub('ListItem')
|
ListItem: this.stub('ListItem'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.tick(2)
|
await this.tick(2)
|
||||||
|
|
|
@ -15,7 +15,8 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { sortBy } from 'lodash'
|
import { sortBy } from 'lodash'
|
||||||
import { computed, onMounted, ref, Ref, toRefs } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
import { computed, onMounted, ref, toRefs } from 'vue'
|
||||||
import { useAuthorization, useDialogBox, useErrorHandler } from '@/composables'
|
import { useAuthorization, useDialogBox, useErrorHandler } from '@/composables'
|
||||||
import { playlistCollaborationService } from '@/services'
|
import { playlistCollaborationService } from '@/services'
|
||||||
import { eventBus } from '@/utils'
|
import { eventBus } from '@/utils'
|
||||||
|
@ -29,7 +30,7 @@ const { playlist } = toRefs(props)
|
||||||
const { currentUser } = useAuthorization()
|
const { currentUser } = useAuthorization()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
|
||||||
let collaborators: Ref<PlaylistCollaborator[]> = ref([])
|
const collaborators: Ref<PlaylistCollaborator[]> = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const currentUserIsOwner = computed(() => currentUser.value?.id === playlist.value.user_id)
|
const currentUserIsOwner = computed(() => currentUser.value?.id === playlist.value.user_id)
|
||||||
|
@ -41,10 +42,14 @@ const fetchCollaborators = async () => {
|
||||||
collaborators.value = sortBy(
|
collaborators.value = sortBy(
|
||||||
await playlistCollaborationService.fetchCollaborators(playlist.value),
|
await playlistCollaborationService.fetchCollaborators(playlist.value),
|
||||||
({ id }) => {
|
({ id }) => {
|
||||||
if (id === currentUser.value.id) return 0
|
if (id === currentUser.value.id) {
|
||||||
if (id === playlist.value.user_id) return 1
|
return 0
|
||||||
|
}
|
||||||
|
if (id === playlist.value.user_id) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
return 2
|
return 2
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
@ -53,10 +58,12 @@ const fetchCollaborators = async () => {
|
||||||
|
|
||||||
const removeCollaborator = async (collaborator: PlaylistCollaborator) => {
|
const removeCollaborator = async (collaborator: PlaylistCollaborator) => {
|
||||||
const deadSure = await showConfirmDialog(
|
const deadSure = await showConfirmDialog(
|
||||||
`Remove ${collaborator.name} as a collaborator? This will remove their contributions as well.`
|
`Remove ${collaborator.name} as a collaborator? This will remove their contributions as well.`,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!deadSure) return
|
if (!deadSure) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
collaborators.value = collaborators.value.filter(({ id }) => id !== collaborator.id)
|
collaborators.value = collaborators.value.filter(({ id }) => id !== collaborator.id)
|
||||||
|
|
|
@ -12,7 +12,7 @@ new class extends UnitTestCase {
|
||||||
collaborator: factory('playlist-collaborator', { id: currentUser.id + 1 }),
|
collaborator: factory('playlist-collaborator', { id: currentUser.id + 1 }),
|
||||||
removable: true,
|
removable: true,
|
||||||
manageable: true,
|
manageable: true,
|
||||||
role: 'owner'
|
role: 'owner',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(screen.queryByTitle('This is you!')).toBeNull()
|
expect(screen.queryByTitle('This is you!')).toBeNull()
|
||||||
|
@ -21,16 +21,14 @@ new class extends UnitTestCase {
|
||||||
it('shows a badge when current user is the collaborator', async () => {
|
it('shows a badge when current user is the collaborator', async () => {
|
||||||
const currentUser = factory('user')
|
const currentUser = factory('user')
|
||||||
this.be(currentUser).renderComponent({
|
this.be(currentUser).renderComponent({
|
||||||
collaborator: factory('playlist-collaborator',
|
collaborator: factory('playlist-collaborator', {
|
||||||
{
|
id: currentUser.id,
|
||||||
id: currentUser.id,
|
name: currentUser.name,
|
||||||
name: currentUser.name,
|
avatar: currentUser.avatar,
|
||||||
avatar: currentUser.avatar
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
removable: true,
|
removable: true,
|
||||||
manageable: true,
|
manageable: true,
|
||||||
role: 'owner'
|
role: 'owner',
|
||||||
})
|
})
|
||||||
|
|
||||||
screen.getByTitle('This is you!')
|
screen.getByTitle('This is you!')
|
||||||
|
@ -43,7 +41,7 @@ new class extends UnitTestCase {
|
||||||
collaborator,
|
collaborator,
|
||||||
removable: true,
|
removable: true,
|
||||||
manageable: true,
|
manageable: true,
|
||||||
role: 'owner'
|
role: 'owner',
|
||||||
})
|
})
|
||||||
|
|
||||||
screen.getByText('Owner')
|
screen.getByText('Owner')
|
||||||
|
@ -52,7 +50,7 @@ new class extends UnitTestCase {
|
||||||
collaborator,
|
collaborator,
|
||||||
removable: true,
|
removable: true,
|
||||||
manageable: true,
|
manageable: true,
|
||||||
role: 'contributor'
|
role: 'contributor',
|
||||||
})
|
})
|
||||||
|
|
||||||
screen.getByText('Contributor')
|
screen.getByText('Contributor')
|
||||||
|
@ -64,7 +62,7 @@ new class extends UnitTestCase {
|
||||||
collaborator,
|
collaborator,
|
||||||
removable: true,
|
removable: true,
|
||||||
manageable: true,
|
manageable: true,
|
||||||
role: 'owner'
|
role: 'owner',
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.user.click(screen.getByRole('button', { name: 'Remove' }))
|
await this.user.click(screen.getByRole('button', { name: 'Remove' }))
|
||||||
|
@ -74,18 +72,18 @@ new class extends UnitTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderComponent (props: {
|
private renderComponent (props: {
|
||||||
collaborator: PlaylistCollaborator,
|
collaborator: PlaylistCollaborator
|
||||||
removable: boolean,
|
removable: boolean
|
||||||
manageable: boolean,
|
manageable: boolean
|
||||||
role: 'owner' | 'contributor'
|
role: 'owner' | 'contributor'
|
||||||
}) {
|
}) {
|
||||||
return this.render(Component, {
|
return this.render(Component, {
|
||||||
props,
|
props,
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
UserAvatar: this.stub('UserAvatar')
|
UserAvatar: this.stub('UserAvatar'),
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,16 +34,15 @@ import UserAvatar from '@/components/user/UserAvatar.vue'
|
||||||
import { useAuthorization } from '@/composables'
|
import { useAuthorization } from '@/composables'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
collaborator: PlaylistCollaborator,
|
collaborator: PlaylistCollaborator
|
||||||
removable: boolean,
|
removable: boolean
|
||||||
manageable: boolean,
|
manageable: boolean
|
||||||
role: 'owner' | 'contributor'
|
role: 'owner' | 'contributor'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'remove'): void }>()
|
||||||
const { collaborator, removable, role } = toRefs(props)
|
const { collaborator, removable, role } = toRefs(props)
|
||||||
const { currentUser } = useAuthorization()
|
const { currentUser } = useAuthorization()
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'remove'): void }>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="postcss" scoped>
|
<style lang="postcss" scoped>
|
||||||
|
|
|
@ -133,7 +133,7 @@ new class extends UnitTestCase {
|
||||||
it('does not have an option to edit or delete if the playlist is not owned by the current user', async () => {
|
it('does not have an option to edit or delete if the playlist is not owned by the current user', async () => {
|
||||||
const user = factory('user')
|
const user = factory('user')
|
||||||
const playlist = factory('playlist', {
|
const playlist = factory('playlist', {
|
||||||
user_id: user.id + 1
|
user_id: user.id + 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.renderComponent(playlist, user)
|
await this.renderComponent(playlist, user)
|
||||||
|
@ -156,7 +156,7 @@ new class extends UnitTestCase {
|
||||||
|
|
||||||
private async renderComponent (playlist: Playlist, user: User | null = null) {
|
private async renderComponent (playlist: Playlist, user: User | null = null) {
|
||||||
userStore.state.current = user || factory('user', {
|
userStore.state.current = user || factory('user', {
|
||||||
id: playlist.user_id
|
id: playlist.user_id,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.render(PlaylistContextMenu)
|
this.render(PlaylistContextMenu)
|
||||||
|
|
|
@ -62,7 +62,7 @@ import {
|
||||||
useModal,
|
useModal,
|
||||||
useOverlay,
|
useOverlay,
|
||||||
useRouter,
|
useRouter,
|
||||||
useSmartPlaylistForm
|
useSmartPlaylistForm,
|
||||||
} from '@/composables'
|
} from '@/composables'
|
||||||
|
|
||||||
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
||||||
|
@ -70,13 +70,15 @@ import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Btn,
|
Btn,
|
||||||
FormBase,
|
FormBase,
|
||||||
RuleGroup,
|
RuleGroup,
|
||||||
collectedRuleGroups,
|
collectedRuleGroups,
|
||||||
addGroup,
|
addGroup,
|
||||||
onGroupChanged
|
onGroupChanged,
|
||||||
} = useSmartPlaylistForm()
|
} = useSmartPlaylistForm()
|
||||||
|
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
|
@ -92,7 +94,6 @@ const folderId = ref(targetFolder?.id)
|
||||||
const folders = toRef(playlistFolderStore.state, 'folders')
|
const folders = toRef(playlistFolderStore.state, 'folders')
|
||||||
const ownSongsOnly = ref(false)
|
const ownSongsOnly = ref(false)
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const isPristine = () => name.value === ''
|
const isPristine = () => name.value === ''
|
||||||
|
@ -115,7 +116,7 @@ const submit = async () => {
|
||||||
const playlist = await playlistStore.store(name.value, {
|
const playlist = await playlistStore.store(name.value, {
|
||||||
rules: collectedRuleGroups.value,
|
rules: collectedRuleGroups.value,
|
||||||
folder_id: folderId.value,
|
folder_id: folderId.value,
|
||||||
own_songs_only: ownSongsOnly.value
|
own_songs_only: ownSongsOnly.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
close()
|
close()
|
||||||
|
|
|
@ -68,13 +68,14 @@ import {
|
||||||
useMessageToaster,
|
useMessageToaster,
|
||||||
useModal,
|
useModal,
|
||||||
useOverlay,
|
useOverlay,
|
||||||
useSmartPlaylistForm
|
useSmartPlaylistForm,
|
||||||
} from '@/composables'
|
} from '@/composables'
|
||||||
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
import CheckBox from '@/components/ui/form/CheckBox.vue'
|
||||||
import TextInput from '@/components/ui/form/TextInput.vue'
|
import TextInput from '@/components/ui/form/TextInput.vue'
|
||||||
import FormRow from '@/components/ui/form/FormRow.vue'
|
import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||||
const { showOverlay, hideOverlay } = useOverlay()
|
const { showOverlay, hideOverlay } = useOverlay()
|
||||||
const { toastSuccess } = useMessageToaster()
|
const { toastSuccess } = useMessageToaster()
|
||||||
const { showConfirmDialog } = useDialogBox()
|
const { showConfirmDialog } = useDialogBox()
|
||||||
|
@ -94,10 +95,9 @@ const {
|
||||||
RuleGroup,
|
RuleGroup,
|
||||||
collectedRuleGroups,
|
collectedRuleGroups,
|
||||||
addGroup,
|
addGroup,
|
||||||
onGroupChanged
|
onGroupChanged,
|
||||||
} = useSmartPlaylistForm(mutablePlaylist.rules)
|
} = useSmartPlaylistForm(mutablePlaylist.rules)
|
||||||
|
|
||||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
|
||||||
const close = () => emit('close')
|
const close = () => emit('close')
|
||||||
|
|
||||||
const maybeClose = async () => {
|
const maybeClose = async () => {
|
||||||
|
|
|
@ -47,9 +47,15 @@ import FormRow from '@/components/ui/form/FormRow.vue'
|
||||||
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
import SelectBox from '@/components/ui/form/SelectBox.vue'
|
||||||
import Btn from '@/components/ui/form/Btn.vue'
|
import Btn from '@/components/ui/form/Btn.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ rule: SmartPlaylistRule }>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'input', rule: SmartPlaylistRule): void
|
||||||
|
(e: 'remove'): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const RuleInput = defineAsyncComponent(() => import('@/components/playlist/smart-playlist/SmartPlaylistRuleInput.vue'))
|
const RuleInput = defineAsyncComponent(() => import('@/components/playlist/smart-playlist/SmartPlaylistRuleInput.vue'))
|
||||||
|
|
||||||
const props = defineProps<{ rule: SmartPlaylistRule }>()
|
|
||||||
const { rule } = toRefs(props)
|
const { rule } = toRefs(props)
|
||||||
|
|
||||||
const mutatedRule = Object.assign({}, rule.value) as SmartPlaylistRule
|
const mutatedRule = Object.assign({}, rule.value) as SmartPlaylistRule
|
||||||
|
@ -78,8 +84,8 @@ if (!operator) {
|
||||||
selectedOperator.value = operator
|
selectedOperator.value = operator
|
||||||
|
|
||||||
const isOriginalOperatorSelected = computed(() => {
|
const isOriginalOperatorSelected = computed(() => {
|
||||||
return selectedModel.value?.name === mutatedRule.model.name &&
|
return selectedModel.value?.name === mutatedRule.model.name
|
||||||
selectedOperator.value?.operator === mutatedRule.operator
|
&& selectedOperator.value?.operator === mutatedRule.operator
|
||||||
})
|
})
|
||||||
|
|
||||||
const availableInputs = computed<{ id: string, value: any }[]>(() => {
|
const availableInputs = computed<{ id: string, value: any }[]>(() => {
|
||||||
|
@ -92,7 +98,7 @@ const availableInputs = computed<{ id: string, value: any }[]>(() => {
|
||||||
for (let i = 0, inputCount = selectedOperator.value.inputs || 1; i < inputCount; ++i) {
|
for (let i = 0, inputCount = selectedOperator.value.inputs || 1; i < inputCount; ++i) {
|
||||||
inputs.push({
|
inputs.push({
|
||||||
id: `${mutatedRule.model.name}_${selectedOperator.value.operator}_${i}`,
|
id: `${mutatedRule.model.name}_${selectedOperator.value.operator}_${i}`,
|
||||||
value: isOriginalOperatorSelected.value ? mutatedRule.value[i] : ''
|
value: isOriginalOperatorSelected.value ? mutatedRule.value[i] : '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,17 +115,12 @@ watch(availableOperators, () => {
|
||||||
|
|
||||||
const valueSuffix = computed(() => selectedOperator.value?.unit || selectedModel.value?.unit)
|
const valueSuffix = computed(() => selectedOperator.value?.unit || selectedModel.value?.unit)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: 'input', rule: SmartPlaylistRule): void,
|
|
||||||
(e: 'remove'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const onInput = () => {
|
const onInput = () => {
|
||||||
emit('input', {
|
emit('input', {
|
||||||
id: mutatedRule.id,
|
id: mutatedRule.id,
|
||||||
model: selectedModel.value!,
|
model: selectedModel.value!,
|
||||||
operator: selectedOperator.value?.operator!,
|
operator: selectedOperator.value!.operator,
|
||||||
value: availableInputs.value.map(input => input.value)
|
value: availableInputs.value.map(input => input.value),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue