Migrate remaining class/decorator Vue components to Composition API (#3)
* Plan: migrate class-based Vue components to Composition API Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> * Migrate legacy class-based Vue components to Composition API Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> * Fix BlogIndex tag key uniqueness after review Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> * Apply validation feedback and finalize composition migration Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/061a81e2-3249-43c2-a205-af770343be40 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> * Remove package-lock and validate project with Bun lockfile Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> * Normalize bun.lockb file mode Agent-Logs-Url: https://github.com/hykilpikonna/corner/sessions/ee17e721-3915-4dfb-8ccc-cf9b0b715fb0 Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
"moment": "^2.30.1",
|
||||
"tg-blog": "^1.1.7",
|
||||
"vue": "^3.4.35",
|
||||
"vue-facing-decorator": "^4.0.1",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
|
||||
+81
-85
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div id="nav" class="fbox-v"
|
||||
:class="(currentRoute) + ' ' + (menuOpen ? 'open' : '')"
|
||||
v-if="currentRoute !== 'colorpicker'">
|
||||
v-if="currentRoute !== 'colorpicker'">
|
||||
<div id="menu" @click="showMenu"><i class="fas fa-bars"></i></div>
|
||||
|
||||
<div id="items" class="fbox-v">
|
||||
<router-link class="router-link" ref="others" to="/others">{{ $t('nav.others') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('others')" to="/others">{{ $t('nav.others') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="photo" to="/photo">{{ $t('nav.photo') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('photo')" to="/photo">{{ $t('nav.photo') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="blog" to="/blog">{{ $t('nav.blog') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('blog')" to="/blog">{{ $t('nav.blog') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="life" to="/life">{{ $t('nav.life') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('life')" to="/life">{{ $t('nav.life') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="about" to="/about">{{ $t('nav.about') }}</router-link>
|
||||
<router-link class="router-link" :ref="setNavRef('about')" to="/about">{{ $t('nav.about') }}</router-link>
|
||||
<div class="dot">·</div>
|
||||
<router-link class="router-link" ref="home" to="/">
|
||||
<router-link class="router-link" :ref="setNavRef('home')" to="/">
|
||||
<svg focusable="false" data-prefix="fal" data-icon="house-night" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" class="svg-inline--fa fa-house-night fa-w-20"><path fill="currentColor" d="M112,224a111.5,111.5,0,0,0,87-41.45,20.51,20.51,0,0,0-19.75-33.08A59.2,59.2,0,0,1,138.84,39.85a20.3,20.3,0,0,0,10.07-21.27,20.26,20.26,0,0,0-16.47-16.7A136,136,0,0,0,112,0a112,112,0,0,0,0,224ZM97.78,33.27a91.21,91.21,0,0,0,54.47,147.9A80,80,0,1,1,97.78,33.27Zm97.15,35.51,39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L317,68.78a5.33,5.33,0,0,0,0-9.54L277.32,42.68,260.76,3a5.33,5.33,0,0,0-9.55,0L234.65,42.68,194.93,59.24a5.34,5.34,0,0,0,0,9.54ZM157,379.24l-39.72-16.57L100.76,323a5.34,5.34,0,0,0-9.55,0L74.65,362.67,34.93,379.24a5.34,5.34,0,0,0,0,9.54l39.72,16.56,16.56,39.72a5.33,5.33,0,0,0,9.55,0l16.56-39.72L157,388.78a5.33,5.33,0,0,0,0-9.54Zm179-101.9v85.33A21.39,21.39,0,0,0,357.36,384h85.31A21.39,21.39,0,0,0,464,362.67V277.34A21.4,21.4,0,0,0,442.67,256H357.36A21.4,21.4,0,0,0,336,277.34ZM368,288H432v64H368Zm266.49,8L576,244.75V144a16,16,0,0,0-32,0v72.75L410.53,100a16,16,0,0,0-21.07,0l-224,196a16,16,0,0,0,21.07,24.09L224,287.28V464a48.05,48.05,0,0,0,48,48H528a48.06,48.06,0,0,0,48-48V287.28l37.46,32.78A16,16,0,0,0,634.53,296ZM544,464a16,16,0,0,1-16,16H272a16,16,0,0,1-16-16V264a15.94,15.94,0,0,0-.81-4L400,133.27l144,126Z" class=""></path></svg></router-link>
|
||||
</div>
|
||||
|
||||
@@ -28,92 +28,88 @@
|
||||
<router-view/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ComponentPublicInstance } from 'vue';
|
||||
import { Component, Vue, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {nextTick, onMounted, onUnmounted, ref, ComponentPublicInstance} from 'vue';
|
||||
import router from "@/scripts/router";
|
||||
import { RouteLocationNormalized, RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
import { TranslateResult } from "vue-i18n";
|
||||
import {RouteLocationNormalized, RouteLocationRaw, useRoute} from "vue-router";
|
||||
|
||||
@Component
|
||||
class App extends Vue
|
||||
{
|
||||
currentRoute = ''
|
||||
currentLink: Element = null as never as Element
|
||||
bookmarkCss = ""
|
||||
bookmarkUpdateIntervalId!: number
|
||||
lastTop = 0
|
||||
const route = useRoute()
|
||||
|
||||
menuOpen = false
|
||||
const currentRoute = ref('')
|
||||
const bookmarkCss = ref('')
|
||||
const lastTop = ref(0)
|
||||
const menuOpen = ref(false)
|
||||
const bookmarkUpdateIntervalId = ref<number | null>(null)
|
||||
const removeAfterEach = ref<(() => void) | null>(null)
|
||||
|
||||
declare $t: (arg: string) => TranslateResult
|
||||
declare $route: RouteLocationNormalizedLoaded
|
||||
declare $router: Router
|
||||
const navRefs = ref<Record<string, Element | ComponentPublicInstance | null>>({})
|
||||
|
||||
showMenu(): void
|
||||
{
|
||||
this.menuOpen = !this.menuOpen
|
||||
const showMenu = (): void => {
|
||||
menuOpen.value = !menuOpen.value
|
||||
|
||||
// Auto close
|
||||
if (this.menuOpen) setTimeout(() => this.menuOpen = false, 2000)
|
||||
}
|
||||
|
||||
updateBookmark(to: RouteLocationNormalized): void
|
||||
{
|
||||
// Update title
|
||||
// Use next tick to handle router history correctly
|
||||
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
|
||||
this.$nextTick(() => {
|
||||
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
|
||||
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
|
||||
})
|
||||
|
||||
console.log('AfterEach called', to)
|
||||
this.currentRoute = ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
|
||||
|
||||
this.calculateBookmarkCss()
|
||||
this.menuOpen = false
|
||||
}
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
console.log('Mounted called', this.$route)
|
||||
router.afterEach(this.updateBookmark)
|
||||
if (this.$route.name) this.currentRoute = ((this.$route.meta?.navBookmark ?? this.$route.name) as string).toLowerCase()
|
||||
|
||||
// Resize listener
|
||||
window.addEventListener('resize', this.calculateBookmarkCss, true);
|
||||
|
||||
// Update every second
|
||||
this.bookmarkUpdateIntervalId = window.setInterval(this.calculateBookmarkCss, 1000)
|
||||
}
|
||||
|
||||
unmounted(): void
|
||||
{
|
||||
window.removeEventListener('resize', this.calculateBookmarkCss)
|
||||
window.clearInterval(this.bookmarkUpdateIntervalId)
|
||||
}
|
||||
|
||||
calculateBookmarkCss(): void
|
||||
{
|
||||
if (this.currentRoute in this.$refs)
|
||||
this.currentLink = (this.$refs[this.currentRoute] as ComponentPublicInstance).$el
|
||||
else return
|
||||
|
||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
|
||||
let box = this.currentLink.getBoundingClientRect()
|
||||
if (box.top == this.lastTop) return
|
||||
this.lastTop = box.top
|
||||
|
||||
let h = box.bottom - box.top
|
||||
let width = Math.round(h / 2) + 8
|
||||
|
||||
this.bookmarkCss = `top: ${box.top - 8}px;` +
|
||||
`border-width: ${width}px 20px ${width}px 50px;`
|
||||
}
|
||||
if (menuOpen.value) setTimeout(() => menuOpen.value = false, 2000)
|
||||
}
|
||||
|
||||
export default toNative(App)
|
||||
const setNavRef = (name: string) => (el: Element | ComponentPublicInstance | null) => {
|
||||
navRefs.value[name] = el
|
||||
}
|
||||
|
||||
const resolveNavElement = (target: Element | ComponentPublicInstance | null | undefined): Element | null => {
|
||||
if (!target) return null
|
||||
if (target instanceof Element) return target
|
||||
return (target.$el as Element | undefined) ?? null
|
||||
}
|
||||
|
||||
const calculateBookmarkCss = (): void => {
|
||||
const currentLink = resolveNavElement(navRefs.value[currentRoute.value])
|
||||
if (!currentLink) return
|
||||
|
||||
const box = currentLink.getBoundingClientRect()
|
||||
if (box.top === lastTop.value) return
|
||||
lastTop.value = box.top
|
||||
|
||||
const h = box.bottom - box.top
|
||||
const width = Math.round(h / 2) + 8
|
||||
|
||||
bookmarkCss.value = `top: ${box.top - 8}px;` +
|
||||
`border-width: ${width}px 20px ${width}px 50px;`
|
||||
}
|
||||
|
||||
const getRouteBookmark = (to: RouteLocationNormalized): string => {
|
||||
return ((to.meta?.navBookmark ?? to.name) as string).toLowerCase()
|
||||
}
|
||||
|
||||
const updateBookmark = (to: RouteLocationNormalized): void => {
|
||||
nextTick(() => {
|
||||
if (to.name == 'Blog' && Object.keys(to.query).length != 0) return
|
||||
document.title = to.meta.title ? `Aza - ${to.meta.title}` : 'Aza - Home';
|
||||
})
|
||||
|
||||
console.log('AfterEach called', to)
|
||||
currentRoute.value = getRouteBookmark(to)
|
||||
|
||||
calculateBookmarkCss()
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Mounted called', route)
|
||||
removeAfterEach.value = router.afterEach(updateBookmark)
|
||||
if (route.name) {
|
||||
currentRoute.value = getRouteBookmark(route as unknown as RouteLocationNormalized)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', calculateBookmarkCss, true)
|
||||
bookmarkUpdateIntervalId.value = window.setInterval(calculateBookmarkCss, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
removeAfterEach.value?.()
|
||||
window.removeEventListener('resize', calculateBookmarkCss, true)
|
||||
if (bookmarkUpdateIntervalId.value !== null) {
|
||||
window.clearInterval(bookmarkUpdateIntervalId.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="index index-tags" v-if="mode === 'tags'">
|
||||
<Tag v-for="t in meta.tags" :key="t" :tag-name="t[0]" direction="right"
|
||||
<Tag v-for="t in meta.tags" :key="`${t[0]}-${t[1]}`" :tag-name="t[0]" direction="right"
|
||||
@click="e => clickTag(e, t)">{{ t[0] }} ({{ t[1] }})</Tag>
|
||||
</div>
|
||||
<div class="index index-categories" v-else>
|
||||
@@ -9,35 +9,27 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import Tag from "@/components/Tag.vue";
|
||||
import {pushQuery} from "@/scripts/router";
|
||||
import {BlogMeta} from "@/scripts/models";
|
||||
import {globals} from "@/scripts/global";
|
||||
|
||||
@Component({components: {Tag}})
|
||||
class BlogIndexLinks extends Vue
|
||||
{
|
||||
@Prop({default: 'tags'}) mode: 'tags' | 'categories' = 'tags'
|
||||
withDefaults(defineProps<{ mode?: 'tags' | 'categories' }>(), {
|
||||
mode: 'tags'
|
||||
})
|
||||
|
||||
meta: BlogMeta = globals.staticMeta
|
||||
const meta: BlogMeta = globals.staticMeta
|
||||
|
||||
clickCat(e: MouseEvent, cat: [string, number]): void
|
||||
{
|
||||
e.stopPropagation()
|
||||
pushQuery({category: cat[0], tag: null})
|
||||
}
|
||||
|
||||
|
||||
clickTag(e: MouseEvent, tag: [string, number]): void
|
||||
{
|
||||
e.stopPropagation()
|
||||
pushQuery({tag: tag[0], category: null})
|
||||
}
|
||||
const clickCat = (e: MouseEvent, cat: [string, number]): void => {
|
||||
e.stopPropagation()
|
||||
pushQuery({category: cat[0], tag: null})
|
||||
}
|
||||
|
||||
export default toNative(BlogIndexLinks)
|
||||
const clickTag = (e: MouseEvent, tag: [string, number]): void => {
|
||||
e.stopPropagation()
|
||||
pushQuery({tag: tag[0], category: null})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
+14
-20
@@ -7,30 +7,24 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from 'vue'
|
||||
import {$} from '@/scripts/constants';
|
||||
|
||||
@Component
|
||||
class Collapse extends Vue
|
||||
{
|
||||
@Prop title!: string
|
||||
@Prop({default: false}) active = false
|
||||
const props = withDefaults(defineProps<{ title: string, active?: boolean }>(), {
|
||||
active: false
|
||||
})
|
||||
|
||||
show = false
|
||||
const displayTitle = computed((): string => decodeURIComponent(props.title))
|
||||
|
||||
get displayTitle(): string
|
||||
{
|
||||
return decodeURIComponent(this.title)
|
||||
}
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
$('.collapse').accordion({collapsible: true, header: 'h3', heightStyle: 'content',
|
||||
active: this.active})
|
||||
}
|
||||
}
|
||||
export default toNative(Collapse)
|
||||
onMounted((): void => {
|
||||
$('.collapse').accordion({
|
||||
collapsible: true,
|
||||
header: 'h3',
|
||||
heightStyle: 'content',
|
||||
active: props.active
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
@@ -7,33 +7,24 @@
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
|
||||
@Component
|
||||
class MetaTable extends Vue
|
||||
{
|
||||
@Prop({required: true}) table!: {[id: string]: unknown}
|
||||
const props = defineProps<{ table: {[id: string]: unknown} }>()
|
||||
|
||||
get filteredTable(): {[id: string]: unknown}
|
||||
{
|
||||
const t: {[id: string]: unknown} = {}
|
||||
const filteredTable = computed((): {[id: string]: unknown} => {
|
||||
const t: {[id: string]: unknown} = {}
|
||||
|
||||
Object.keys(this.table).forEach(k => {
|
||||
// Ignore empty
|
||||
if (!this.table[k]) return
|
||||
Object.keys(props.table).forEach(k => {
|
||||
if (!props.table[k]) return
|
||||
|
||||
// Convert to sentence case (https://stackoverflow.com/a/7225450/7346633)
|
||||
let newK = k.replace(/([A-Z])/g, " $1")
|
||||
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
||||
t[newK] = this.table[k]
|
||||
})
|
||||
let newK = k.replace(/([A-Z])/g, " $1")
|
||||
newK = newK.charAt(0).toUpperCase() + newK.slice(1)
|
||||
t[newK] = props.table[k]
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
export default toNative(MetaTable)
|
||||
return t
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted} from 'vue'
|
||||
import moment from "moment";
|
||||
import MetaTable from "@/components/MetaTable.vue";
|
||||
import {capitalize} from "@/scripts/utils";
|
||||
@@ -32,36 +32,32 @@ import linkifyUrls from "linkify-urls";
|
||||
import {$} from '@/scripts/constants';
|
||||
import {ZoteroData, ZoteroItem} from "@/scripts/zotero";
|
||||
|
||||
@Component({components: {MetaTable}})
|
||||
class ZoteroPublicationView extends Vue
|
||||
{
|
||||
@Prop({required: true}) item!: ZoteroItem
|
||||
const props = defineProps<{ item: ZoteroItem }>()
|
||||
|
||||
get d(): ZoteroData { return this.item.data }
|
||||
get date(): moment.Moment { return moment(this.item.meta.parsedDate) }
|
||||
get authors(): string { return this.d.creators.map(it => it.firstName + ' ' + it.lastName).join(' & ') }
|
||||
const d = computed((): ZoteroData => props.item.data)
|
||||
const date = computed((): moment.Moment => moment(props.item.meta.parsedDate))
|
||||
const authors = computed((): string => d.value.creators.map(it => it.firstName + ' ' + it.lastName).join(' & '))
|
||||
|
||||
get tableData(): {[id: string]: unknown}
|
||||
{
|
||||
const t: {[id: string]: unknown} = {...this.d}
|
||||
t.creators = this.authors
|
||||
delete t.key
|
||||
delete t.version
|
||||
delete t.title
|
||||
delete t.abstractNote
|
||||
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
||||
if (t.url) t.url = linkifyUrls(t.url as string)
|
||||
return t
|
||||
}
|
||||
const tableData = computed((): {[id: string]: unknown} => {
|
||||
const t: {[id: string]: unknown} = {...d.value}
|
||||
t.creators = authors.value
|
||||
delete t.key
|
||||
delete t.version
|
||||
delete t.title
|
||||
delete t.abstractNote
|
||||
if (t.itemType) t.itemType = capitalize(t.itemType as string)
|
||||
if (t.url) t.url = linkifyUrls(t.url as string)
|
||||
return t
|
||||
})
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
$('.publication').accordion({collapsible: true, header: 'div.header', active: false,
|
||||
heightStyle: "content"})
|
||||
}
|
||||
}
|
||||
|
||||
export default toNative(ZoteroPublicationView)
|
||||
onMounted((): void => {
|
||||
$('.publication').accordion({
|
||||
collapsible: true,
|
||||
header: 'div.header',
|
||||
active: false,
|
||||
heightStyle: 'content'
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Component, Vue, Prop } from 'vue-facing-decorator'
|
||||
|
||||
/**
|
||||
* Same as python's range
|
||||
*
|
||||
@@ -41,9 +39,9 @@ export function minMax(val: number, min: number, max: number): number
|
||||
export type Keybinds = {[id: string]: (e: KeyboardEvent) => unknown}
|
||||
|
||||
/**
|
||||
* Key handler mixin
|
||||
* Key handler standalone helper class (not a Vue mixin)
|
||||
*/
|
||||
export class KeyHandler extends Vue
|
||||
export class KeyHandler
|
||||
{
|
||||
keybinds: Keybinds = {}
|
||||
_keybinds: Keybinds = {}
|
||||
|
||||
+18
-31
@@ -9,8 +9,8 @@
|
||||
<Loading v-else></Loading>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {marked} from 'marked';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
import {parseExtensions} from '@/scripts/extended_markdown'
|
||||
@@ -19,37 +19,24 @@ import {hosts} from "@/scripts/constants";
|
||||
import Loading from "@/components/Loading.vue";
|
||||
import {ZoteroAttachment, ZoteroItem} from "@/scripts/zotero";
|
||||
|
||||
@Component({components: {Loading, ZoteroPublication}})
|
||||
class About extends Vue
|
||||
{
|
||||
html = ""
|
||||
publications: ZoteroItem[] = []
|
||||
const html = ref("")
|
||||
const publications = ref<ZoteroItem[]>([])
|
||||
|
||||
mounted(): void
|
||||
{
|
||||
// Fetch readme
|
||||
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
||||
.then(it => this.html = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
||||
return `<span class="emoji">${emoji}</span>`
|
||||
}))))
|
||||
onMounted((): void => {
|
||||
fetch(`${hosts.content}/README.md`).then(it => it.text())
|
||||
.then(it => html.value = marked(parseExtensions(it.replace(emojiRegex(), (emoji) => {
|
||||
return `<span class="emoji">${emoji}</span>`
|
||||
}))))
|
||||
|
||||
// Fetch publications from zotero
|
||||
fetch(`${hosts.api}/zotero.json`)
|
||||
.then(it => it.json()).then(it =>
|
||||
{
|
||||
// Filter out publications and attachments
|
||||
this.publications = it
|
||||
let files: ZoteroAttachment[] = it
|
||||
files = files.filter(it => it.data.itemType === 'attachment')
|
||||
this.publications = this.publications.filter(it => it.data.itemType !== 'attachment')
|
||||
|
||||
// Add attachments to
|
||||
this.publications.forEach(it => it.attachments = files.filter(a => a.data.parentItem == it.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default toNative(About)
|
||||
fetch(`${hosts.api}/zotero.json`)
|
||||
.then(it => it.json()).then((it: ZoteroItem[]) => {
|
||||
publications.value = it
|
||||
let files: ZoteroAttachment[] = it as unknown as ZoteroAttachment[]
|
||||
files = files.filter(file => file.data.itemType === 'attachment')
|
||||
publications.value = publications.value.filter(pub => pub.data.itemType !== 'attachment')
|
||||
publications.value.forEach(pub => pub.attachments = files.filter(a => a.data.parentItem == pub.key))
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
|
||||
+73
-75
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, toNative } from 'vue-facing-decorator';
|
||||
import { RouteLocationNormalizedLoaded } from "vue-router";
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
interface PhotoMetadata {
|
||||
id: string
|
||||
owner_key: string // This will be filtered out for public requests
|
||||
owner_key: string
|
||||
upload_time: string
|
||||
original_photo: string
|
||||
edited_photo: string
|
||||
@@ -13,7 +13,6 @@ interface PhotoMetadata {
|
||||
exif: {[id: string]: string}
|
||||
}
|
||||
|
||||
// Take in a string, use its hash to produce a number from 0 to 1
|
||||
function detRandom(seed: string): number {
|
||||
return Array.from(seed).reduce((acc, char) => (acc + char.charCodeAt(0) * 65535) % 22859, 0) / 22859
|
||||
}
|
||||
@@ -21,84 +20,83 @@ function detRandom(seed: string): number {
|
||||
async function waitTruthy<T>(condition: () => T, interval = 100): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
const check = () => {
|
||||
let a = condition()
|
||||
if (a) resolve(a)
|
||||
const value = condition()
|
||||
if (value) resolve(value)
|
||||
else setTimeout(check, interval)
|
||||
}
|
||||
check()
|
||||
})
|
||||
}
|
||||
|
||||
@Component({})
|
||||
class Photos extends Vue {
|
||||
photos: PhotoMetadata[]
|
||||
photoRows: PhotoMetadata[][]
|
||||
const route = useRoute()
|
||||
const photos = ref<PhotoMetadata[]>([])
|
||||
const photoRows = ref<PhotoMetadata[][]>([])
|
||||
|
||||
declare $route: RouteLocationNormalizedLoaded
|
||||
|
||||
async created() {
|
||||
this.photos = await (await fetch('https://p.aza.moe/photos')).json()
|
||||
this.photos.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
|
||||
|
||||
let rowProbabilityTable = {
|
||||
1: 0, 2: 0.3, 3: 0.5
|
||||
}
|
||||
|
||||
// Generate photo rows: there is a 10% chance that a photo will be the only photo in its row
|
||||
this.photoRows = []
|
||||
let currentRow: PhotoMetadata[] = []
|
||||
this.photos.forEach((p) => {
|
||||
if (currentRow.length === 0) currentRow.push(p)
|
||||
else if (currentRow.length >= 3) {
|
||||
this.photoRows.push(currentRow)
|
||||
currentRow = [ p ]
|
||||
}
|
||||
else {
|
||||
const singleChance = detRandom(p.original_photo)
|
||||
if (singleChance < rowProbabilityTable[currentRow.length]) {
|
||||
this.photoRows.push(currentRow)
|
||||
currentRow = [ p ]
|
||||
} else currentRow.push(p)
|
||||
}
|
||||
})
|
||||
if (currentRow.length > 0) this.photoRows.push(currentRow)
|
||||
}
|
||||
|
||||
async mounted() {
|
||||
if (this.$route.params.id) {
|
||||
const photoEl = await waitTruthy(() => document.getElementById(`photo-${this.$route.params.id}`))
|
||||
photoEl.click()
|
||||
}
|
||||
}
|
||||
|
||||
url(s: string): string {
|
||||
s = s.replace('data/photos', 'static').replace('./', '')
|
||||
return `https://p.aza.moe/${s}`
|
||||
}
|
||||
|
||||
randomRotation(s: string): string {
|
||||
const angle = (detRandom(s) * 20) - 10 // -10 to +10 degrees
|
||||
return `rotate(${angle}deg)`
|
||||
}
|
||||
|
||||
async clickPhoto(p: PhotoMetadata, e: MouseEvent) {
|
||||
console.log("Clicked photo:", p.id)
|
||||
const dom = e.currentTarget as HTMLDivElement
|
||||
const photoEl = dom.querySelector('.photo-wrapper') as HTMLDivElement
|
||||
|
||||
photoEl.style.viewTransitionName = `photo-${p.id}`
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
dom.classList.toggle('active')
|
||||
document.getElementsByClassName('blur')[0].toggleAttribute('hidden')
|
||||
})
|
||||
|
||||
await transition.finished
|
||||
photoEl.style.viewTransitionName = ''
|
||||
}
|
||||
const rowProbabilityTable: Record<number, number> = {
|
||||
1: 0,
|
||||
2: 0.3,
|
||||
3: 0.5
|
||||
}
|
||||
|
||||
export default toNative(Photos)
|
||||
const initPhotos = async () => {
|
||||
photos.value = await (await fetch('https://p.aza.moe/photos')).json()
|
||||
photos.value.sort((a, b) => (a.exif.DateTime < b.exif.DateTime ? 1 : -1))
|
||||
|
||||
const rows: PhotoMetadata[][] = []
|
||||
let currentRow: PhotoMetadata[] = []
|
||||
|
||||
photos.value.forEach((p) => {
|
||||
if (currentRow.length === 0) currentRow.push(p)
|
||||
else if (currentRow.length >= 3) {
|
||||
rows.push(currentRow)
|
||||
currentRow = [p]
|
||||
} else {
|
||||
const singleChance = detRandom(p.original_photo)
|
||||
if (singleChance < rowProbabilityTable[currentRow.length]) {
|
||||
rows.push(currentRow)
|
||||
currentRow = [p]
|
||||
} else currentRow.push(p)
|
||||
}
|
||||
})
|
||||
|
||||
if (currentRow.length > 0) rows.push(currentRow)
|
||||
photoRows.value = rows
|
||||
}
|
||||
|
||||
const url = (s: string): string => {
|
||||
s = s.replace('data/photos', 'static').replace('./', '')
|
||||
return `https://p.aza.moe/${s}`
|
||||
}
|
||||
|
||||
const randomRotation = (s: string): string => {
|
||||
const angle = (detRandom(s) * 20) - 10
|
||||
return `rotate(${angle}deg)`
|
||||
}
|
||||
|
||||
const clickPhoto = async (p: PhotoMetadata, e: MouseEvent) => {
|
||||
console.log("Clicked photo:", p.id)
|
||||
const dom = e.currentTarget as HTMLDivElement
|
||||
const photoEl = dom.querySelector('.photo-wrapper') as HTMLDivElement
|
||||
|
||||
photoEl.style.viewTransitionName = `photo-${p.id}`
|
||||
|
||||
const transition = document.startViewTransition(() => {
|
||||
dom.classList.toggle('active')
|
||||
document.getElementsByClassName('blur')[0].toggleAttribute('hidden')
|
||||
})
|
||||
|
||||
await transition.finished
|
||||
photoEl.style.viewTransitionName = ''
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initPhotos()
|
||||
|
||||
if (route.params.id) {
|
||||
const photoEl = await waitTruthy(() => document.getElementById(`photo-${route.params.id}`))
|
||||
photoEl.click()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -176,4 +174,4 @@ img.pin
|
||||
width: 40px
|
||||
height: 40px
|
||||
z-index: 2000
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -23,57 +23,49 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from 'vue'
|
||||
import {fab, hosts} from "@/scripts/constants";
|
||||
import {shuffle} from "@/scripts/utils";
|
||||
|
||||
export interface Friend {
|
||||
interface Friend {
|
||||
name: string
|
||||
avatar: string
|
||||
banner: string
|
||||
|
||||
desc?: string
|
||||
[id: string]: string | undefined
|
||||
}
|
||||
|
||||
const excludes = new Set(["name", "avatar", "banner", "desc"])
|
||||
const icons = {
|
||||
const icons: {[id: string]: string} = {
|
||||
blog: 'fas fa-book'
|
||||
}
|
||||
|
||||
@Component
|
||||
class Friends extends Vue
|
||||
{
|
||||
friends: Friend[] = []
|
||||
const friends = ref<Friend[]>([])
|
||||
|
||||
async created()
|
||||
{
|
||||
this.friends = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
|
||||
|
||||
// Fix avatar relative url
|
||||
this.friends.forEach(f => {
|
||||
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
|
||||
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
|
||||
})
|
||||
this.friends = shuffle(this.friends)
|
||||
}
|
||||
|
||||
bgStyle(f: Friend)
|
||||
{
|
||||
if (f.banner) return {'background-image': `url("${f.banner}")`}
|
||||
else return {}
|
||||
}
|
||||
|
||||
getFriendLinks(f: Friend): { link: string, icon: string }[]
|
||||
{
|
||||
return Object.entries(f).filter(pair => !excludes.has(pair[0].toString()))
|
||||
.map(pair => {
|
||||
return { link: pair[1], icon: fab.includes(pair[0]) ? `fab fa-${pair[0]}` :
|
||||
pair[0] in icons ? icons[pair[0]] : pair[0] }
|
||||
})
|
||||
}
|
||||
const bgStyle = (f: Friend) => {
|
||||
if (f.banner) return {'background-image': `url("${f.banner}")`}
|
||||
return {}
|
||||
}
|
||||
export default toNative(Friends)
|
||||
|
||||
const getFriendLinks = (f: Friend): { link: string, icon: string }[] => {
|
||||
return Object.entries(f)
|
||||
.filter(([key, value]) => !excludes.has(key) && typeof value === 'string')
|
||||
.map(([key, value]) => ({
|
||||
link: value,
|
||||
icon: fab.includes(key) ? `fab fa-${key}` : (key in icons ? icons[key] : key)
|
||||
}))
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
friends.value = await (await fetch(`${hosts.content}/content/generated/friends/friends.json`)).json()
|
||||
|
||||
friends.value.forEach(f => {
|
||||
if (!f.avatar.startsWith('http')) f.avatar = `${hosts.content}/${f.avatar}`
|
||||
if (f.banner && !f.banner.startsWith('http')) f.banner = `${hosts.content}/${f.banner}`
|
||||
})
|
||||
friends.value = shuffle(friends.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
+20
-39
@@ -23,30 +23,26 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
|
||||
export interface MenuItem
|
||||
{
|
||||
interface MenuItem {
|
||||
name: string
|
||||
sub?: string
|
||||
img?: string
|
||||
recommend?: boolean
|
||||
original?: boolean
|
||||
|
||||
id?: number
|
||||
}
|
||||
|
||||
export interface MenuCategory
|
||||
{
|
||||
interface MenuCategory {
|
||||
cat: string
|
||||
subtitle?: string
|
||||
items: MenuItem[]
|
||||
|
||||
column?: number
|
||||
}
|
||||
|
||||
export const menu: MenuCategory[] = [
|
||||
const menu: MenuCategory[] = [
|
||||
{
|
||||
cat: '🍖 猪肉',
|
||||
items: [
|
||||
@@ -134,40 +130,25 @@ export const menu: MenuCategory[] = [
|
||||
},
|
||||
]
|
||||
|
||||
@Component
|
||||
class Menu extends Vue
|
||||
{
|
||||
max_cols = 2
|
||||
cols: MenuCategory[][] = new Array(this.max_cols)
|
||||
const maxCols = 2
|
||||
const cols = ref<MenuCategory[][]>(Array.from({length: maxCols}, () => []))
|
||||
|
||||
created()
|
||||
{
|
||||
// Calculate menu layout
|
||||
const tmp = Array.from(menu)
|
||||
tmp.sort((a, b) => b.items.length - a.items.length)
|
||||
const tmp = Array.from(menu)
|
||||
tmp.sort((a, b) => b.items.length - a.items.length)
|
||||
|
||||
// Two columns
|
||||
let col_counts = new Array(this.max_cols).fill(0)
|
||||
for (const cat of tmp)
|
||||
{
|
||||
// Get column index with minimal item count
|
||||
let col = col_counts.indexOf(Math.min(...col_counts))
|
||||
|
||||
cat.column = col
|
||||
col_counts[col] += cat.items.length
|
||||
}
|
||||
|
||||
// Separate arrays by column
|
||||
for (let i = 0; i < this.max_cols; i++)
|
||||
this.cols[i] = menu.filter(it => it.column == i)
|
||||
|
||||
// Assign ID to each item
|
||||
let id = 0
|
||||
this.cols.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
|
||||
}
|
||||
const colCounts = new Array(maxCols).fill(0)
|
||||
for (const cat of tmp) {
|
||||
const col = colCounts.indexOf(Math.min(...colCounts))
|
||||
cat.column = col
|
||||
colCounts[col] += cat.items.length
|
||||
}
|
||||
|
||||
export default toNative(Menu)
|
||||
for (let i = 0; i < maxCols; i++) {
|
||||
cols.value[i] = menu.filter(it => it.column == i)
|
||||
}
|
||||
|
||||
let id = 0
|
||||
cols.value.forEach(col => col.forEach(cat => cat.items.forEach(it => it.id = id++)))
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
@@ -4,16 +4,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Prop, toNative } from 'vue-facing-decorator'
|
||||
|
||||
@Component
|
||||
class Projects extends Vue
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
export default toNative(Projects)
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style lang="sass" scoped>
|
||||
|
||||
Reference in New Issue
Block a user