unwind/unwind-ui/src/App.vue

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

270 lines
6.4 KiB
Vue
Raw Normal View History

2021-07-25 19:01:25 +02:00
<template>
<section class="section">
<div class="container">
<user-login
class="is-justify-content-end"
@login="(login) => (credentials = login)"
2021-07-25 19:01:25 +02:00
/>
<div class="field is-flex is-justify-content-end" v-if="login_user">
<div class="control has-icons-left">
<div class="select is-small">
<select v-model="filter_group" @change="search">
<option selected value="">Me ({{ login_user.name }})</option>
2021-08-05 17:50:44 +02:00
<option v-for="g in login_user.groups" :key="g.id" :value="g.id">
{{ g.id }}
</option>
</select>
</div>
<div class="icon is-small is-left">
<span class="fas fa-group">👥</span>
</div>
</div>
</div>
<div class="field has-addons mt-6">
2021-07-25 19:01:25 +02:00
<div class="control is-expanded">
<input
class="input is-loading"
type="text"
placeholder="Movie title"
v-model="query"
@change="search"
/>
</div>
<p class="control">
<span class="select">
<select v-model="media_type" @change="search">
<option value="">(any type)</option>
2021-08-05 17:50:44 +02:00
<option v-for="t in media_types" :key="t" :value="t">{{ t }}</option>
2021-07-25 19:01:25 +02:00
</select>
</span>
</p>
<div class="control">
2021-08-04 17:30:17 +02:00
<a class="button is-info" :disabled="!active"> Search </a>
2021-07-25 19:01:25 +02:00
</div>
</div>
<movie-list :items="items" @reachBottom="loadMore" />
<footer class="footer" v-if="is_end">
<p class="subtitle content has-text-centered is-italic"> fin </p>
</footer>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from "vue"
import config from "./config.ts"
import { is_object } from "./utils.ts"
const media_types = [
"Movie",
"Radio Episode",
"Radio Series",
"Short",
"TV Episode",
"TV Mini Series",
"TV Movie",
"TV Series",
"TV Short",
"TV Special",
"Video",
"Video Game",
]
function clean_obj(o) {
return Object.fromEntries(Object.entries(o).filter(([k, v]) => v))
}
function api_url(resource, params = []) {
const url = new URL(resource, config.api_url)
if (is_object(params)) {
params = clean_obj(params)
}
if (is_object(params) || params.length) {
url.search = new URLSearchParams(params)
}
return url
}
function set_view_params(params) {
history.replaceState(null, "", "#" + new URLSearchParams(clean_obj(params)))
}
function view_params() {
const fragment = (window.location.hash || "").replace(/^#/, "")
const params = new URLSearchParams(fragment)
return {
query: params.get("query") || "",
type: params.get("type") || "",
}
}
async function req(url, opts = {}, { user_id = "", secret = "" }, data = null) {
opts.headers = opts.headers || {}
if (data !== null) {
opts.body = JSON.stringify(data)
opts.headers["Content-Type"] = "application/json"
}
if (secret) {
2021-08-03 17:06:06 +02:00
const credentials = btoa(`${user_id}:${secret}`)
opts.headers["Authorization"] = `Basic ${credentials}`
2021-07-25 19:01:25 +02:00
opts.mode = "cors"
opts.credentials = "include"
}
const resp = await window.fetch(url, opts)
if (!resp.ok) {
throw resp
}
2021-07-25 19:01:25 +02:00
return await resp.json()
}
async function get(resource, params = [], user = {}) {
const url = api_url(resource, params)
const opts = {}
return await req(url, opts, user)
}
async function patch(resource, params = [], user = {}, data = null) {
const url = api_url(resource, params)
const opts = { method: "PATCH" }
return await req(url, opts, user, data)
}
function debounce_async(ms, func) {
let abort
return async (...args) => {
if (abort) {
abort()
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
resolve(func(...args))
}, ms)
abort = () => {
reject("Function call aborted, replaced by a later call.")
clearTimeout(timer)
abort = null
}
})
}
}
const debounced_get = debounce_async(100, get)
type ULID = string
type User = {
2021-08-05 17:50:44 +02:00
id: ULID
name: string
groups: Array<{ id: ULID }>
}
function parse_query(query) {
const match = /^(.*?)(?:\s+([<=>])?(\d+|\(\d+\)))?$/.exec(query)
const title = match[1].replaceAll('"', "")
const comp = match[2] || ""
const year = match[3] ? /\d+/.exec(match[3])[0] : ""
return { title, year: year ? `${comp}${year}` : null }
}
2021-07-25 19:01:25 +02:00
export default defineComponent({
data: () => ({
query: "",
items: [],
credentials: { user_id: "", secret: "" },
login_user: null,
2021-07-25 19:01:25 +02:00
page: 1,
is_end: false,
media_types,
media_type: "",
active: false,
2021-08-05 17:50:44 +02:00
filter_group: "",
2021-07-25 19:01:25 +02:00
}),
mounted() {
const { query, type } = view_params()
this.query = query
this.media_type = media_types.includes(type) ? type : ""
},
methods: {
async search() {
this.active = true
set_view_params({ query: this.query, type: this.media_type })
if (!this.query) {
return
}
this.page = 1
this.is_end = false
this.items = []
await this.loadMore()
},
async loadMore() {
this.active = false
if (!this.query || this.is_end) {
return
}
const per_page = 100
const media_type = this.media_type === "" ? null : this.media_type
2021-08-05 17:50:44 +02:00
const user_id = (this.filter_group === "" && this.credentials.user_id) || null
const group_id = user_id === null && this.filter_group ? this.filter_group : null
const { title, year } = parse_query(this.query)
2021-07-25 19:01:25 +02:00
const more = await debounced_get(
"movies",
{
title,
year,
2021-07-25 19:01:25 +02:00
per_page,
include_unrated: true,
page: this.page,
media_type,
user_id,
group_id,
2021-07-25 19:01:25 +02:00
},
this.credentials,
2021-07-25 19:01:25 +02:00
)
this.items = this.items.concat(more)
if (more.length < per_page) {
this.is_end = true
} else {
this.page += 1
}
},
},
watch: {
async credentials(creds) {
if (!creds.user_id) {
return
}
const user: User = await get(`users/${creds.user_id}`, {}, creds)
this.login_user = user
},
},
2021-07-25 19:01:25 +02:00
})
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 60px;
}
</style>