2021-07-25 19:01:25 +02:00
|
|
|
|
<template>
|
|
|
|
|
|
<section class="section">
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<user-login
|
2021-08-05 15:53:27 +02:00
|
|
|
|
class="is-justify-content-end"
|
|
|
|
|
|
@login="(login) => (credentials = login)"
|
2021-07-25 19:01:25 +02:00
|
|
|
|
/>
|
|
|
|
|
|
|
2021-08-05 15:53:27 +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>
|
|
|
|
|
|
<option v-for="g in login_user.groups" :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>
|
|
|
|
|
|
<option v-for="t in media_types" :value="t">{{ t }}</option>
|
|
|
|
|
|
</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)
|
2021-08-05 15:53:27 +02:00
|
|
|
|
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)
|
|
|
|
|
|
|
2021-08-05 15:53:27 +02:00
|
|
|
|
type ULID = string
|
|
|
|
|
|
type User = {
|
|
|
|
|
|
id: ULID,
|
|
|
|
|
|
name: string,
|
|
|
|
|
|
groups: Array<{id: ULID}>,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2021-07-25 19:01:25 +02:00
|
|
|
|
export default defineComponent({
|
|
|
|
|
|
data: () => ({
|
|
|
|
|
|
query: "",
|
|
|
|
|
|
items: [],
|
2021-08-05 15:53:27 +02:00
|
|
|
|
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 15:53:27 +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 15:53:27 +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
|
2021-07-25 19:01:25 +02:00
|
|
|
|
const more = await debounced_get(
|
|
|
|
|
|
"movies",
|
|
|
|
|
|
{
|
|
|
|
|
|
title: this.query,
|
|
|
|
|
|
per_page,
|
|
|
|
|
|
include_unrated: true,
|
|
|
|
|
|
page: this.page,
|
|
|
|
|
|
media_type,
|
2021-08-05 15:53:27 +02:00
|
|
|
|
user_id,
|
|
|
|
|
|
group_id,
|
2021-07-25 19:01:25 +02:00
|
|
|
|
},
|
2021-08-05 15:53:27 +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
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2021-08-05 15:53:27 +02:00
|
|
|
|
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>
|