Feat: Dynamic colors (#101)

* On every route change, update local variables from query params

* ResultSection is keyed to query to force re-render

* Feat: vite & upgraded dependencies (#100)

* On every route change, update local variables from query params

* ResultSection is keyed to query to force re-render

* Resolved lint warnings

* replace webpack w/ vite

* update all imports with alias @ and scss

* vite environment variables, also typed

* upgraded eslint, defined new rules & added ignore comments

* resolved linting issues

* moved index.html to project root

* updated dockerfile w/ build stage before runtime image definition

* sign drone config

* dynamic colors from poster for popup bg & text colors

* more torrents nav button now link elem & better for darker bg

* make list item title clickable

* removed extra no-shadow eslint rule definitions

* fixed movie import

* adhere to eslint rules & package.json clean command

* remove debounce autocomplete search, track & hault on failure
This commit is contained in:
2026-02-24 00:22:51 +01:00
committed by GitHub
parent 1238cf50cc
commit 426b376d05
19 changed files with 443 additions and 278 deletions

View File

@@ -10,7 +10,6 @@
>
<IconMovie v-if="result.type == 'movie'" class="type-icon" />
<IconShow v-if="result.type == 'show'" class="type-icon" />
<IconPerson v-if="result.type == 'person'" class="type-icon" />
<span class="title">{{ result.title }}</span>
</li>
@@ -24,10 +23,6 @@
</transition>
</template>
<!--
Searches Elasticsearch for results based on changes to `query`.
-->
<script setup lang="ts">
import type { Ref } from "vue";
import { ref, watch, defineProps } from "vue";
@@ -38,10 +33,7 @@ Searches Elasticsearch for results based on changes to `query`.
import { MediaTypes } from "../../interfaces/IList";
import type {
IAutocompleteResult,
IAutocompleteSearchResults,
Hit,
Option,
Source
IAutocompleteSearchResults
} from "../../interfaces/IAutocompleteSearch";
interface Props {
@@ -55,7 +47,6 @@ Searches Elasticsearch for results based on changes to `query`.
}
const numberOfResults = 10;
let timeoutId = null;
const props = defineProps<Props>();
const emit = defineEmits<Emit>();
const store = useStore();
@@ -63,9 +54,25 @@ Searches Elasticsearch for results based on changes to `query`.
const searchResults: Ref<Array<IAutocompleteResult>> = ref([]);
const keyboardNavigationIndex: Ref<number> = ref(0);
let disableOnFailure = false;
watch(
() => props.query,
newQuery => {
if (newQuery?.length > 0 && !disableOnFailure)
fetchAutocompleteResults(); /* eslint-disable-line no-use-before-define */
}
);
function openPopup(result: IAutocompleteResult) {
if (!result.id || !result.type) return;
store.dispatch("popup/open", { ...result });
}
function removeDuplicates(_searchResults: Array<IAutocompleteResult>) {
const filteredResults = [];
_searchResults.forEach((result: IAutocompleteResult) => {
_searchResults.forEach(result => {
if (result === undefined) return;
const numberOfDuplicates = filteredResults.filter(
filterItem => filterItem.id === result.id
@@ -80,83 +87,58 @@ Searches Elasticsearch for results based on changes to `query`.
return filteredResults;
}
function convertMediaType(type: string | null): MediaTypes | null {
function elasticTypeToMediaType(type: string): MediaTypes {
if (type === "movie") return MediaTypes.Movie;
if (type === "tv_series") return MediaTypes.Show;
if (type === "person") return MediaTypes.Person;
return null;
}
function parseElasticResponse(elasticResponse: IAutocompleteSearchResults) {
const elasticResults = elasticResponse.hits.hits;
const suggestResults = elasticResponse.suggest["movie-suggest"][0].options;
let data: Array<Source> = elasticResults.map((el: Hit) => el._source);
data = data.concat(suggestResults.map((el: Option) => el._source));
// data = data.concat(elasticResponse['suggest']['person-suggest'][0]['options'])
// data = data.concat(elasticResponse['suggest']['show-suggest'][0]['options'])
data = data.sort((a, b) => (a.popularity < b.popularity ? 1 : -1));
const data = elasticResponse.hits.hits;
const results: Array<IAutocompleteResult> = [];
data.forEach(item => {
if (!item._index) return;
results.push({
title: item?.original_name || item?.original_title || item?.name,
id: item.id,
adult: item.adult,
type: convertMediaType(item?.type)
title: item._source?.original_name || item._source.original_title,
id: item._source.id,
adult: item._source.adult,
type: elasticTypeToMediaType(item._source.type)
});
});
return removeDuplicates(results)
.map((el, index) => {
return { ...el, index };
})
.slice(0, 10);
return removeDuplicates(results).map((el, index) => {
return { ...el, index };
});
}
function fetchAutocompleteResults() {
async function fetchAutocompleteResults() {
keyboardNavigationIndex.value = 0;
searchResults.value = [];
elasticSearchMoviesAndShows(props.query, numberOfResults)
return elasticSearchMoviesAndShows(props.query, numberOfResults)
.catch(error => {
// TODO display error
disableOnFailure = true;
throw error;
})
.then(elasticResponse => parseElasticResponse(elasticResponse))
.then(_searchResults => {
console.log(_searchResults);
emit("update:results", _searchResults);
searchResults.value = _searchResults;
})
.catch(error => {
// TODO display error
disableOnFailure = true;
throw error;
});
}
const debounce = (callback: () => void, wait: number) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback();
}, wait);
};
watch(
() => props.query,
newQuery => {
if (newQuery?.length > 0) {
debounce(fetchAutocompleteResults, 150);
}
}
);
function openPopup(result: IAutocompleteResult) {
if (!result.id || !result.type) return;
store.dispatch("popup/open", { ...result });
}
// on load functions
fetchAutocompleteResults();
// end on load functions
</script>
<style lang="scss" scoped>