Skip to content

Commit 75143ae

Browse files
authored
First iteration of reworking the settings pages: the providers view (#1150)
1 parent 171093d commit 75143ae

File tree

8 files changed

+1055
-383
lines changed

8 files changed

+1055
-383
lines changed

src/components/ProviderFilters.vue

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
<template>
2+
<div class="filters-container">
3+
<v-text-field
4+
v-model="searchQuery"
5+
prepend-inner-icon="mdi-magnify"
6+
:label="$t('search')"
7+
variant="outlined"
8+
density="compact"
9+
clearable
10+
hide-details
11+
class="search-field"
12+
/>
13+
<div class="d-flex ga-2 filter-buttons">
14+
<v-btn height="40" elevation="0">
15+
{{ $t("sort.provider") }}
16+
<v-icon end>mdi-chevron-down</v-icon>
17+
<v-menu activator="parent" :close-on-content-click="false">
18+
<v-list>
19+
<v-list-item
20+
v-for="(providerType, index) in providerTypes"
21+
:key="index"
22+
:value="index"
23+
@click="toggleProviderType(providerType.value)"
24+
>
25+
<template #append>
26+
<v-checkbox-btn
27+
:model-value="
28+
selectedProviderTypes.includes(providerType.value)
29+
"
30+
@click.stop="toggleProviderType(providerType.value)"
31+
/>
32+
</template>
33+
<v-list-item-title>{{ providerType.title }}</v-list-item-title>
34+
</v-list-item>
35+
</v-list>
36+
</v-menu>
37+
</v-btn>
38+
<v-btn v-if="showStageFilter" height="40" elevation="0">
39+
{{ $t("settings.stage.label") }}
40+
<v-icon end>mdi-chevron-down</v-icon>
41+
<v-menu activator="parent" :close-on-content-click="false">
42+
<v-list>
43+
<v-list-item
44+
v-for="(stage, index) in providerStages"
45+
:key="index"
46+
:value="index"
47+
@click="toggleProviderStage(stage.value)"
48+
>
49+
<template #append>
50+
<v-checkbox-btn
51+
:model-value="selectedProviderStages.includes(stage.value)"
52+
@click.stop="toggleProviderStage(stage.value)"
53+
/>
54+
</template>
55+
<v-list-item-title>{{ stage.title }}</v-list-item-title>
56+
</v-list-item>
57+
</v-list>
58+
</v-menu>
59+
</v-btn>
60+
</div>
61+
</div>
62+
</template>
63+
64+
<script setup lang="ts">
65+
import { ProviderStage, ProviderType } from "@/plugins/api/interfaces";
66+
import { $t } from "@/plugins/i18n";
67+
import { computed, ref, watch } from "vue";
68+
import { useRoute, useRouter } from "vue-router";
69+
70+
// Props
71+
const { showStageFilter = false } = defineProps<{
72+
showStageFilter?: boolean;
73+
}>();
74+
75+
const router = useRouter();
76+
const route = useRoute();
77+
78+
const searchQuery = ref<string>("");
79+
const selectedProviderTypes = ref<string[]>([]);
80+
const selectedProviderStages = ref<string[]>([]);
81+
let searchDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
82+
let typesDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
83+
let stagesDebounceTimeout: ReturnType<typeof setTimeout> | null = null;
84+
85+
const providerTypes = computed(() => [
86+
{ title: $t("settings.musicprovider"), value: ProviderType.MUSIC },
87+
{ title: $t("settings.playerprovider"), value: ProviderType.PLAYER },
88+
{ title: $t("settings.metadataprovider"), value: ProviderType.METADATA },
89+
{ title: $t("settings.pluginprovider"), value: ProviderType.PLUGIN },
90+
]);
91+
92+
const providerStages = computed(() => [
93+
{ title: $t("settings.stage.options.stable"), value: ProviderStage.STABLE },
94+
{ title: $t("settings.stage.options.beta"), value: ProviderStage.BETA },
95+
{ title: $t("settings.stage.options.alpha"), value: ProviderStage.ALPHA },
96+
{
97+
title: $t("settings.stage.options.experimental"),
98+
value: ProviderStage.EXPERIMENTAL,
99+
},
100+
{
101+
title: $t("settings.stage.options.unmaintained"),
102+
value: ProviderStage.UNMAINTAINED,
103+
},
104+
{
105+
title: $t("settings.stage.options.deprecated"),
106+
value: ProviderStage.DEPRECATED,
107+
},
108+
]);
109+
110+
// Emits
111+
const emit = defineEmits<{
112+
(e: "update:search", value: string): void;
113+
(e: "update:types", value: string[]): void;
114+
(e: "update:stages", value: string[]): void;
115+
}>();
116+
117+
const toggleProviderType = function (type: string) {
118+
const index = selectedProviderTypes.value.indexOf(type);
119+
if (index > -1) {
120+
selectedProviderTypes.value.splice(index, 1);
121+
} else {
122+
selectedProviderTypes.value.push(type);
123+
}
124+
};
125+
126+
const toggleProviderStage = function (stage: string) {
127+
const index = selectedProviderStages.value.indexOf(stage);
128+
if (index > -1) {
129+
selectedProviderStages.value.splice(index, 1);
130+
} else {
131+
selectedProviderStages.value.push(stage);
132+
}
133+
};
134+
135+
const initializeFromUrl = function () {
136+
if (route.query.search) {
137+
searchQuery.value = route.query.search as string;
138+
}
139+
140+
if (route.query.types) {
141+
const types = route.query.types as string;
142+
selectedProviderTypes.value = types.split(",");
143+
}
144+
145+
if (route.query.stages) {
146+
const stages = route.query.stages as string;
147+
selectedProviderStages.value = stages.split(",");
148+
}
149+
};
150+
151+
// Watch search query and update URL with debounce
152+
watch(searchQuery, (newQuery) => {
153+
emit("update:search", newQuery);
154+
155+
if (searchDebounceTimeout) {
156+
clearTimeout(searchDebounceTimeout);
157+
}
158+
searchDebounceTimeout = setTimeout(() => {
159+
const query = { ...route.query };
160+
if (newQuery) {
161+
query.search = newQuery;
162+
} else {
163+
delete query.search;
164+
}
165+
router.replace({ query });
166+
}, 750);
167+
});
168+
169+
// Watch selected provider types and update URL with debounce
170+
watch(
171+
selectedProviderTypes,
172+
(newTypes) => {
173+
emit("update:types", newTypes);
174+
175+
if (typesDebounceTimeout) {
176+
clearTimeout(typesDebounceTimeout);
177+
}
178+
typesDebounceTimeout = setTimeout(() => {
179+
const query = { ...route.query };
180+
if (newTypes.length > 0) {
181+
query.types = newTypes.join(",");
182+
} else {
183+
delete query.types;
184+
}
185+
router.replace({ query });
186+
}, 750);
187+
},
188+
{ deep: true },
189+
);
190+
191+
// Watch selected provider stages and update URL with debounce
192+
watch(
193+
selectedProviderStages,
194+
(newStages) => {
195+
emit("update:stages", newStages);
196+
197+
if (stagesDebounceTimeout) {
198+
clearTimeout(stagesDebounceTimeout);
199+
}
200+
stagesDebounceTimeout = setTimeout(() => {
201+
const query = { ...route.query };
202+
if (newStages.length > 0) {
203+
query.stages = newStages.join(",");
204+
} else {
205+
delete query.stages;
206+
}
207+
router.replace({ query });
208+
}, 750);
209+
},
210+
{ deep: true },
211+
);
212+
213+
initializeFromUrl();
214+
</script>
215+
216+
<style scoped>
217+
.filters-container {
218+
display: flex;
219+
align-items: stretch;
220+
gap: 12px;
221+
flex: 1;
222+
flex-wrap: wrap;
223+
}
224+
225+
.search-field {
226+
flex: 1 1 auto;
227+
min-width: 250px;
228+
max-width: 400px;
229+
}
230+
231+
.filter-buttons {
232+
display: flex;
233+
gap: 8px;
234+
flex-shrink: 0;
235+
flex-wrap: wrap;
236+
}
237+
238+
.filter-buttons .v-btn {
239+
min-width: 100px;
240+
}
241+
242+
/* Mobile responsive */
243+
@media (max-width: 960px) {
244+
.filters-container {
245+
flex-direction: column;
246+
align-items: stretch;
247+
}
248+
249+
.search-field {
250+
width: 100%;
251+
min-width: 100%;
252+
}
253+
254+
.filter-buttons {
255+
width: 100%;
256+
}
257+
258+
.filter-buttons .v-btn {
259+
flex: 1 1 auto;
260+
min-width: 120px;
261+
}
262+
}
263+
264+
:deep(.v-list-item .v-checkbox-btn) {
265+
display: flex;
266+
align-items: center;
267+
}
268+
269+
:deep(.v-list-item .v-checkbox-btn .v-input__control) {
270+
display: flex;
271+
align-items: center;
272+
}
273+
274+
:deep(.v-list-item .v-checkbox-btn .v-selection-control) {
275+
min-height: auto;
276+
}
277+
</style>

src/plugins/api/interfaces.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { ComputedRef } from "vue";
2-
31
/// constants
42
export const SECURE_STRING_SUBSTITUTE = "this_value_is_encrypted";
53
export const MASS_LOGO_ONLINE =
@@ -898,6 +896,7 @@ export interface ProviderManifest {
898896
builtin: boolean;
899897
// allow_disable: whether this provider can be disabled (used with builtin)
900898
allow_disable: boolean;
899+
stage: ProviderStage;
901900
// icon: material design icon
902901
icon?: string;
903902
// icon_svg: svg icon (full xml string)
@@ -910,6 +909,15 @@ export interface ProviderManifest {
910909
depends_on?: string;
911910
}
912911

912+
export enum ProviderStage {
913+
ALPHA = "alpha",
914+
BETA = "beta",
915+
STABLE = "stable",
916+
EXPERIMENTAL = "experimental",
917+
UNMAINTAINED = "unmaintained",
918+
DEPRECATED = "deprecated",
919+
}
920+
913921
export interface ProviderInstance {
914922
// Provider instance details when a provider is serialized over the api.
915923
type: ProviderType;

src/plugins/router.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,14 +240,23 @@ const routes = [
240240
props: true,
241241
},
242242
{
243-
path: "addprovider/:domain",
243+
path: "addprovider",
244244
name: "addprovider",
245245
component: () =>
246246
import(
247247
/* webpackChunkName: "addprovider" */ "@/views/settings/AddProvider.vue"
248248
),
249249
props: true,
250250
},
251+
{
252+
path: "addprovider/:domain",
253+
name: "addproviderdetails",
254+
component: () =>
255+
import(
256+
/* webpackChunkName: "addproviderdetails" */ "@/views/settings/AddProviderDetails.vue"
257+
),
258+
props: true,
259+
},
251260
{
252261
path: "editprovider/:instanceId",
253262
name: "editprovider",

src/translations/en.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,24 @@
9696
"delete": "Delete",
9797
"disable": "Disable",
9898
"documentation": "Documentation",
99+
"edit_provider": "Edit provider: {0}",
99100
"enable": "Enable",
100101
"enable_player": "Enable this player",
101102
"enable_provider": "Enable this provider entry",
102103
"enabled": "Enable",
103104
"invalid_input": "The input is invalid",
105+
"metadata": "Metadata",
104106
"metadataproviders": "Metadata providers",
107+
"metadataprovider": "Metadata provider",
108+
"music": "Music",
105109
"musicproviders": "Music providers",
110+
"musicprovider": "Music provider",
106111
"need_help_setup_provider": "Need help setting up this provider ?",
107112
"no_providers": "No Music Providers configured",
108113
"no_providers_detail": "Start your Music Assistant experience by adding your Music providers below.",
109114
"not_loaded": "The provider is not (yet) loaded",
110115
"password": "Password",
116+
"player": "Player",
111117
"player_address": "Address",
112118
"player_disabled": "This player is disabled",
113119
"player_id": "Player ID",
@@ -118,15 +124,30 @@
118124
"player_settings": "Player settings",
119125
"player_type_label": "Player type",
120126
"playerproviders": "Player providers",
127+
"playerprovider": "Player provider",
121128
"players": "Players",
129+
"plugin": "Plugin",
122130
"pluginproviders": "Plugin providers",
131+
"pluginprovider": "Plugin provider",
123132
"provider_disabled": "This provider entry is disabled",
124133
"provider_name": "Custom name for this provider entry",
134+
"providers_total": "{0} total provider{1}",
125135
"providers": "Providers",
126136
"reload": "Reload",
127137
"save": "Save",
128138
"settings": "Settings",
129139
"setup_provider": "Setup provider: {0}",
140+
"stage": {
141+
"label": "Stage",
142+
"options": {
143+
"stable": "Stable",
144+
"beta": "Beta",
145+
"alpha": "Alpha",
146+
"experimental": "Experimental",
147+
"unmaintained": "Unmaintained",
148+
"deprecated": "Deprecated"
149+
}
150+
},
130151
"sync": "Synchronize now",
131152
"sync_running": "This provider is now being synchronized",
132153
"username": "Username",

0 commit comments

Comments
 (0)