tangled
alpha
login
or
join now
vt3e.cat
/
www
2
fork
atom
this repo has no descr,ription
vt3e.cat
2
fork
atom
overview
issues
pulls
pipelines
feat: gallery page
vt3e.cat
2 months ago
1d56c286
7305632e
verified
This commit was signed with the committer's
known signature
.
vt3e.cat
SSH Key Fingerprint:
SHA256:bC12nO0d6wKnJ426YBbLO7LVxmZlwJ1l2X0eqOroDV0=
+346
-18
7 changed files
expand all
collapse all
unified
split
bun.lock
package.json
pkgs
web
package.json
src
components
Gallery
GalleryItem.vue
router
index.ts
views
GalleryView.vue
tsconfig.app.json
+33
-4
bun.lock
···
4
4
"": {
5
5
"name": "@sillowww/web",
6
6
"dependencies": {
7
7
+
"@atcute/atproto": "^3.1.10",
8
8
+
"@atcute/client": "^4.2.0",
9
9
+
"@atcute/lexicons": "^1.2.6",
7
10
"@iconify-prerendered/vue-material-symbols": "^0.28.1755063979",
11
11
+
"blurhash": "^2.0.5",
8
12
},
9
13
"devDependencies": {
10
14
"@biomejs/biome": "2.1.4",
···
50
54
"vue-router": "^4.6.4",
51
55
},
52
56
"devDependencies": {
57
57
+
"@sillowww/gallery": "workspace:*",
53
58
"@tsconfig/node24": "^24.0.3",
54
59
"@types/node": "^24.10.4",
55
60
"@vitejs/plugin-vue": "^6.0.3",
···
74
79
},
75
80
},
76
81
"packages": {
77
77
-
"@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
82
82
+
"@atcute/atproto": ["@atcute/atproto@3.1.10", "", { "dependencies": { "@atcute/lexicons": "^1.2.6" } }, "sha512-+GKZpOc0PJcdWMQEkTfg/rSNDAAHxmAUGBl60g2az15etqJn5WaUPNGFE2sB7hKpwi5Ue2h/L0OacINcE/JDDQ=="],
78
83
79
79
-
"@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
84
84
+
"@atcute/client": ["@atcute/client@4.2.0", "", { "dependencies": { "@atcute/identity": "^1.1.3", "@atcute/lexicons": "^1.2.6" } }, "sha512-vYixpXevM+dkzN4HGTfmxCJBOXNSRAMki1bfi1atFdmZGo9n1zStyClHqn0SRs8I5nOQoVG1XBkYAGSFJWqy2Q=="],
80
85
81
86
"@atcute/identity": ["@atcute/identity@1.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6" } }, "sha512-zax42n693VEhnC+5tndvO2KLDTMkHOz8UExwmklvJv7R9VujfEwiSWhcv6Jgwb3ellaG8wjiQ1lMOIjLLvwh0Q=="],
82
87
···
86
91
87
92
"@atcute/lexicon-doc": ["@atcute/lexicon-doc@1.1.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-HlQBmB4NCZPzREyVzr7lzjRxSiRHook2xfa7DgA3dk3oYZ+KnnPEtS6M1sAmAAddtUdrOrJ+0xJPQHkfElZmpQ=="],
88
93
89
89
-
"@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
94
94
+
"@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="],
95
95
+
96
96
+
"@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="],
90
97
91
98
"@atcute/util-fetch": ["@atcute/util-fetch@1.0.3", "", { "dependencies": { "@badrap/valita": "^0.4.6" } }, "sha512-f8zzTb/xlKIwv2OQ31DhShPUNCmIIleX6p7qIXwWwEUjX6x8skUtpdISSjnImq01LXpltGV5y8yhV4/Mlb7CRQ=="],
99
99
+
100
100
+
"@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="],
92
101
93
102
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
94
103
···
452
461
453
462
"@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="],
454
463
455
455
-
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
464
464
+
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
456
465
457
466
"@tsconfig/node24": ["@tsconfig/node24@24.0.3", "", {}, "sha512-vcERKtKQKHgzt/vfS3Gjasd8SUI2a0WZXpgJURdJsMySpS5+ctgbPfuLj2z/W+w4lAfTWxoN4upKfu2WzIRYnw=="],
458
467
···
1021
1030
"typescript-eslint": ["typescript-eslint@8.50.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/parser": "8.50.1", "@typescript-eslint/typescript-estree": "8.50.1", "@typescript-eslint/utils": "8.50.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ=="],
1022
1031
1023
1032
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
1033
1033
+
1034
1034
+
"unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="],
1024
1035
1025
1036
"unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
1026
1037
···
1094
1105
1095
1106
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
1096
1107
1108
1108
+
"@atcute/client/@atcute/identity": ["@atcute/identity@1.1.3", "", { "dependencies": { "@atcute/lexicons": "^1.2.4", "@badrap/valita": "^0.4.6" } }, "sha512-oIqPoI8TwWeQxvcLmFEZLdN2XdWcaLVtlm8pNk0E72As9HNzzD9pwKPrLr3rmTLRIoULPPFmq9iFNsTeCIU9ng=="],
1109
1109
+
1110
1110
+
"@atcute/identity/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
1111
1111
+
1112
1112
+
"@atcute/identity-resolver/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
1113
1113
+
1097
1114
"@atcute/lex-cli/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
1098
1115
1099
1116
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
···
1114
1131
1115
1132
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
1116
1133
1134
1134
+
"@sillowww/uploader/@atcute/atproto": ["@atcute/atproto@3.1.9", "", { "dependencies": { "@atcute/lexicons": "^1.2.2" } }, "sha512-DyWwHCTdR4hY2BPNbLXgVmm7lI+fceOwWbE4LXbGvbvVtSn+ejSVFaAv01Ra3kWDha0whsOmbJL8JP0QPpf1+w=="],
1135
1135
+
1136
1136
+
"@sillowww/uploader/@atcute/client": ["@atcute/client@4.0.5", "", { "dependencies": { "@atcute/identity": "^1.1.1", "@atcute/lexicons": "^1.2.2" } }, "sha512-R8Qen8goGmEkynYGg2m6XFlVmz0GTDvQ+9w+4QqOob+XMk8/WDpF4aImev7WKEde/rV2gjcqW7zM8E6W9NShDA=="],
1137
1137
+
1138
1138
+
"@sillowww/uploader/@atcute/lexicons": ["@atcute/lexicons@1.2.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "esm-env": "^1.2.2" } }, "sha512-bgEhJq5Z70/0TbK5sx+tAkrR8FsCODNiL2gUEvS5PuJfPxmFmRYNWaMGehxSPaXWpU2+Oa9ckceHiYbrItDTkA=="],
1139
1139
+
1117
1140
"@types/react/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
1118
1141
1119
1142
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
···
1148
1171
1149
1172
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
1150
1173
1174
1174
+
"@atcute/identity-resolver/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
1175
1175
+
1176
1176
+
"@atcute/identity/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
1177
1177
+
1151
1178
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
1152
1179
1153
1180
"@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
···
1157
1184
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
1158
1185
1159
1186
"@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
1187
1187
+
1188
1188
+
"@sillowww/uploader/@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
1160
1189
1161
1190
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
1162
1191
+5
-1
package.json
···
23
23
},
24
24
"private": true,
25
25
"dependencies": {
26
26
-
"@iconify-prerendered/vue-material-symbols": "^0.28.1755063979"
26
26
+
"@atcute/atproto": "^3.1.10",
27
27
+
"@atcute/client": "^4.2.0",
28
28
+
"@atcute/lexicons": "^1.2.6",
29
29
+
"@iconify-prerendered/vue-material-symbols": "^0.28.1755063979",
30
30
+
"blurhash": "^2.0.5"
27
31
}
28
32
}
+1
pkgs/web/package.json
···
25
25
"vue-router": "^4.6.4"
26
26
},
27
27
"devDependencies": {
28
28
+
"@sillowww/gallery": "workspace:*",
28
29
"@tsconfig/node24": "^24.0.3",
29
30
"@types/node": "^24.10.4",
30
31
"@vitejs/plugin-vue": "^6.0.3",
+225
pkgs/web/src/components/Gallery/GalleryItem.vue
···
1
1
+
<script setup lang="ts">
2
2
+
import { onMounted, computed, ref, onUnmounted } from 'vue'
3
3
+
import { decode } from 'blurhash'
4
4
+
import { isLegacyBlob } from '@atcute/lexicons/interfaces'
5
5
+
import { MoeWloGalleryImage } from '@sillowww/gallery'
6
6
+
7
7
+
import { DID } from '@/utils/links'
8
8
+
9
9
+
const props = defineProps<{
10
10
+
item: MoeWloGalleryImage.Main
11
11
+
}>()
12
12
+
13
13
+
const rootEl = ref<HTMLElement | null>(null)
14
14
+
const imageUrl = ref<string | null>(null)
15
15
+
const isLoaded = ref(false)
16
16
+
const error = ref(false)
17
17
+
18
18
+
const aspectRatio = computed(() => {
19
19
+
if (!props.item?.width || !props.item?.height) return 1
20
20
+
return props.item.width / props.item.height
21
21
+
})
22
22
+
23
23
+
const blobUrl = computed(() => {
24
24
+
const parts = [
25
25
+
'https://pds.wlo.moe/xrpc/com.atproto.sync.getBlob',
26
26
+
`?did=${DID}`,
27
27
+
`&cid=${isLegacyBlob(props.item.image) ? props.item.image.cid : props.item.image.ref.$link}`,
28
28
+
]
29
29
+
return parts.join('')
30
30
+
})
31
31
+
32
32
+
const blurhashData = computed(() => {
33
33
+
if (!props.item.blurhash) return null
34
34
+
try {
35
35
+
const pixels = decode(props.item.blurhash, 32, 32)
36
36
+
const canvas = document.createElement('canvas')
37
37
+
canvas.width = 32
38
38
+
canvas.height = 32
39
39
+
const ctx = canvas.getContext('2d')
40
40
+
if (!ctx) return null
41
41
+
const imageData = ctx.createImageData(32, 32)
42
42
+
imageData.data.set(pixels)
43
43
+
ctx.putImageData(imageData, 0, 0)
44
44
+
return canvas.toDataURL()
45
45
+
} catch {
46
46
+
return null
47
47
+
}
48
48
+
})
49
49
+
50
50
+
const CACHE_NAME = 'wlo-gallery-v1'
51
51
+
52
52
+
const loadImage = async () => {
53
53
+
if (imageUrl.value || error.value) return
54
54
+
55
55
+
try {
56
56
+
const url = blobUrl.value
57
57
+
let blob: Blob | null = null
58
58
+
59
59
+
const cache = await caches.open(CACHE_NAME)
60
60
+
const cachedRes = await cache.match(url)
61
61
+
62
62
+
if (cachedRes) {
63
63
+
blob = await cachedRes.blob()
64
64
+
} else {
65
65
+
const res = await fetch(url)
66
66
+
if (!res.ok) throw new Error('fetch failed')
67
67
+
68
68
+
cache.put(url, res.clone())
69
69
+
70
70
+
blob = await res.blob()
71
71
+
}
72
72
+
73
73
+
const objectUrl = URL.createObjectURL(blob)
74
74
+
75
75
+
const img = new Image()
76
76
+
img.src = objectUrl
77
77
+
await img.decode()
78
78
+
79
79
+
imageUrl.value = objectUrl
80
80
+
requestAnimationFrame(() => {
81
81
+
isLoaded.value = true
82
82
+
})
83
83
+
} catch (e) {
84
84
+
console.error(e)
85
85
+
error.value = true
86
86
+
}
87
87
+
}
88
88
+
89
89
+
let observer: IntersectionObserver | null = null
90
90
+
91
91
+
onMounted(() => {
92
92
+
if (!rootEl.value) return
93
93
+
94
94
+
observer = new IntersectionObserver(
95
95
+
(entries) => {
96
96
+
if (entries[0]?.isIntersecting) {
97
97
+
loadImage()
98
98
+
observer?.disconnect()
99
99
+
}
100
100
+
},
101
101
+
{ rootMargin: '200px' },
102
102
+
)
103
103
+
104
104
+
observer.observe(rootEl.value)
105
105
+
})
106
106
+
107
107
+
onUnmounted(() => {
108
108
+
observer?.disconnect()
109
109
+
if (imageUrl.value) URL.revokeObjectURL(imageUrl.value)
110
110
+
})
111
111
+
</script>
112
112
+
113
113
+
<template>
114
114
+
<div
115
115
+
ref="rootEl"
116
116
+
class="gallery-item"
117
117
+
:class="{ 'is-loaded': isLoaded, 'has-error': error }"
118
118
+
:style="{ aspectRatio: aspectRatio }"
119
119
+
>
120
120
+
<div class="blurhash-layer">
121
121
+
<img v-if="blurhashData" :src="blurhashData" alt="" />
122
122
+
<div v-else class="fallback-gradient"></div>
123
123
+
</div>
124
124
+
125
125
+
<div class="image-layer">
126
126
+
<img v-if="imageUrl" :src="imageUrl" :alt="item.alt" />
127
127
+
</div>
128
128
+
129
129
+
<div v-if="error" class="error-layer">
130
130
+
<span>could not load</span>
131
131
+
</div>
132
132
+
</div>
133
133
+
</template>
134
134
+
135
135
+
<style scoped lang="scss">
136
136
+
@use '@/styles/variables.scss' as *;
137
137
+
138
138
+
.gallery-item {
139
139
+
position: relative;
140
140
+
break-inside: avoid;
141
141
+
border-radius: 0.75rem;
142
142
+
overflow: hidden;
143
143
+
background-color: hsla(var(--surface0) / 0.5);
144
144
+
145
145
+
transform: translateZ(0); /* try to force hardware accel */
146
146
+
cursor: zoom-in;
147
147
+
border: 1px solid hsla(var(--surface2));
148
148
+
149
149
+
&:hover {
150
150
+
filter: brightness(1.1);
151
151
+
.image-layer {
152
152
+
transform: scale(1.025);
153
153
+
}
154
154
+
}
155
155
+
&:active .image-layer {
156
156
+
transform: scale(1);
157
157
+
}
158
158
+
}
159
159
+
160
160
+
.blurhash-layer,
161
161
+
.image-layer {
162
162
+
position: absolute;
163
163
+
top: 0;
164
164
+
left: 0;
165
165
+
width: 100%;
166
166
+
height: 100%;
167
167
+
168
168
+
img {
169
169
+
width: 100%;
170
170
+
height: 100%;
171
171
+
object-fit: cover;
172
172
+
display: block;
173
173
+
}
174
174
+
}
175
175
+
176
176
+
.blurhash-layer {
177
177
+
z-index: 1;
178
178
+
opacity: 1;
179
179
+
180
180
+
img {
181
181
+
transform: scale(1.2);
182
182
+
filter: blur(20px);
183
183
+
}
184
184
+
}
185
185
+
186
186
+
.image-layer {
187
187
+
z-index: 2;
188
188
+
opacity: 0;
189
189
+
190
190
+
will-change: opacity, transform;
191
191
+
192
192
+
transform: scale(1.1);
193
193
+
filter: blur(8px) saturate(0.8);
194
194
+
}
195
195
+
196
196
+
.is-loaded {
197
197
+
.blurhash-layer {
198
198
+
opacity: 0;
199
199
+
}
200
200
+
201
201
+
.image-layer {
202
202
+
opacity: 1;
203
203
+
transform: scale(1.05);
204
204
+
filter: blur(0px) saturate(1);
205
205
+
}
206
206
+
}
207
207
+
208
208
+
.error-layer {
209
209
+
z-index: 3;
210
210
+
position: absolute;
211
211
+
inset: 0;
212
212
+
display: flex;
213
213
+
align-items: center;
214
214
+
justify-content: center;
215
215
+
background: hsla(var(--surface0) / 0.8);
216
216
+
color: hsla(var(--red));
217
217
+
font-size: 0.8rem;
218
218
+
}
219
219
+
220
220
+
.fallback-gradient {
221
221
+
width: 100%;
222
222
+
height: 100%;
223
223
+
background: linear-gradient(45deg, hsla(var(--surface0)), hsla(var(--surface1)));
224
224
+
}
225
225
+
</style>
+2
-1
pkgs/web/src/router/index.ts
···
13
13
const ProjectsView = () => import('../views/ProjectsView.vue')
14
14
const UsesView = () => import('../views/UsesView.vue')
15
15
const AboutView = () => import('../views/AboutView.vue')
16
16
+
const GalleryView = () => import('../views/GalleryView.vue')
16
17
17
18
declare module 'vue-router' {
18
19
interface RouteMeta {
···
131
132
{
132
133
path: '/gallery',
133
134
name: 'gallery',
134
134
-
component: ProjectsView,
135
135
+
component: GalleryView,
135
136
meta: {
136
137
isCard: true,
137
138
title: 'gallery',
+70
-2
pkgs/web/src/views/GalleryView.vue
···
1
1
<script setup lang="ts">
2
2
+
import { Client, simpleFetchHandler } from '@atcute/client'
3
3
+
import type { MoeWloGalleryImage } from '@sillowww/gallery'
4
4
+
import { onMounted, ref } from 'vue'
5
5
+
2
6
import CardLayout from '@/components/Card/CardLayout.vue'
7
7
+
import GalleryItem from '@/components/Gallery/GalleryItem.vue'
8
8
+
9
9
+
import { DID } from '@/utils/links'
10
10
+
11
11
+
const items = ref<{ uri: string; value: MoeWloGalleryImage.Main }[]>([])
12
12
+
const loading = ref(true)
13
13
+
const error = ref<string | null>(null)
14
14
+
15
15
+
onMounted(async () => {
16
16
+
const manager = simpleFetchHandler({ service: 'https://pds.wlo.moe' })
17
17
+
const client = new Client({ handler: manager })
18
18
+
19
19
+
const { ok, data } = await client.get('com.atproto.repo.listRecords', {
20
20
+
params: {
21
21
+
repo: DID,
22
22
+
collection: 'moe.wlo.gallery.image',
23
23
+
},
24
24
+
})
25
25
+
26
26
+
if (!ok) {
27
27
+
error.value = data.error || 'An unknown error occurred while fetching gallery images.'
28
28
+
loading.value = false
29
29
+
return
30
30
+
}
31
31
+
32
32
+
items.value = data.records.map((record) => {
33
33
+
return { uri: record.uri, value: record.value as MoeWloGalleryImage.Main }
34
34
+
})
35
35
+
loading.value = false
36
36
+
})
3
37
</script>
4
38
5
39
<template>
6
6
-
<CardLayout title="placeholder!"> i love place holding :D </CardLayout>
40
40
+
<CardLayout title="gallery">
41
41
+
<template #intro>
42
42
+
<p>a collection of images that i've taken in the past</p>
43
43
+
</template>
44
44
+
45
45
+
<div v-if="loading">loading...</div>
46
46
+
<div v-else-if="error">error: {{ error }}</div>
47
47
+
48
48
+
<div class="masonry-grid" v-else>
49
49
+
<GalleryItem v-for="item in items" :key="item.uri" :item="item.value" />
50
50
+
</div>
51
51
+
</CardLayout>
7
52
</template>
8
53
9
9
-
<style scoped></style>
54
54
+
<style scoped lang="scss">
55
55
+
.masonry-grid {
56
56
+
--gap: 0.5rem;
57
57
+
column-count: 4;
58
58
+
column-gap: var(--gap);
59
59
+
margin-top: var(--gap);
60
60
+
61
61
+
:deep(.gallery-item) {
62
62
+
margin-bottom: var(--gap);
63
63
+
}
64
64
+
65
65
+
@media (max-width: 1200px) {
66
66
+
column-count: 3;
67
67
+
}
68
68
+
69
69
+
@media (max-width: 768px) {
70
70
+
column-count: 2;
71
71
+
}
72
72
+
73
73
+
@media (max-width: 480px) {
74
74
+
column-count: 1;
75
75
+
}
76
76
+
}
77
77
+
</style>
+10
-10
pkgs/web/tsconfig.app.json
···
1
1
{
2
2
-
"extends": "@vue/tsconfig/tsconfig.dom.json",
3
3
-
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4
4
-
"exclude": ["src/**/__tests__/*"],
5
5
-
"compilerOptions": {
6
6
-
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
7
7
-
8
8
-
"paths": {
9
9
-
"@/*": ["./src/*"]
10
10
-
}
11
11
-
}
2
2
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
3
3
+
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
4
4
+
"exclude": ["src/**/__tests__/*"],
5
5
+
"compilerOptions": {
6
6
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
7
7
+
"types": ["@atcute/atproto", "@sillowww/gallery"],
8
8
+
"paths": {
9
9
+
"@/*": ["./src/*"],
10
10
+
},
11
11
+
},
12
12
}