Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c0b7c5ed | ||
|
|
ad70bdc5b6 | ||
|
|
a7c03d2327 | ||
|
|
673948bce9 | ||
|
|
7a3cdcc1da | ||
|
|
44469bd9f2 | ||
|
|
00060ede92 | ||
|
|
2204709c0e | ||
|
|
3e39bda014 | ||
|
|
05db0b693e | ||
|
|
4543680a92 | ||
|
|
858142f09a | ||
|
|
830c34855c | ||
|
|
a7f2d527dc | ||
|
|
e72a61b317 | ||
|
|
9339416c1b | ||
|
|
753bdb5c0a | ||
|
|
bfeaca5f09 | ||
|
|
10e3a7fc40 | ||
|
|
1ed1d47b07 | ||
|
|
d1e249b51d | ||
|
|
7ce8674fea | ||
|
|
36dd770ce4 | ||
|
|
eadc0b119d | ||
|
|
44c60f7069 | ||
|
|
b58407d358 | ||
|
|
9386c1d158 | ||
|
|
3853e8fa20 | ||
|
|
cfbaf56159 | ||
|
|
014e64d174 | ||
|
|
1f7c8f0ed4 | ||
|
|
4ac1fd9574 | ||
|
|
dd084afb61 | ||
|
|
3c69f73dc6 | ||
|
|
92ad0a50f9 | ||
|
|
4939d24f62 | ||
|
|
2bd1e4a512 | ||
|
|
2a1bcfc3d2 | ||
|
|
c509c770f6 | ||
|
|
a76d959431 | ||
|
|
0cdee66c1d | ||
|
|
a39ad3b464 |
@@ -1,4 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
not ie 11
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# .dockerignore
|
||||||
|
/.git
|
||||||
|
/.nuxt
|
||||||
|
/.output
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
},
|
|
||||||
extends: [
|
|
||||||
'@nuxtjs/eslint-config-typescript',
|
|
||||||
'plugin:nuxt/recommended',
|
|
||||||
'prettier',
|
|
||||||
],
|
|
||||||
plugins: [],
|
|
||||||
rules: {
|
|
||||||
indent: ['error', 2],
|
|
||||||
quotes: [2, 'single', { avoidEscape: true }],
|
|
||||||
'no-console': 'off',
|
|
||||||
'vue/no-multiple-template-root': 'off',
|
|
||||||
'vue/multi-word-component-names': 'off',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
'import/ignore': ['vue-fontawesome'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,9 @@ dist
|
|||||||
# Node dependencies
|
# Node dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -1,96 +0,0 @@
|
|||||||
###
|
|
||||||
# Place your Prettier ignore content here
|
|
||||||
|
|
||||||
###
|
|
||||||
# .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506
|
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
|
||||||
### Node template
|
|
||||||
# Logs
|
|
||||||
/logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# nuxt.js build output
|
|
||||||
.nuxt
|
|
||||||
|
|
||||||
# Nuxt generate
|
|
||||||
dist
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless
|
|
||||||
|
|
||||||
# IDE / Editor
|
|
||||||
.idea
|
|
||||||
|
|
||||||
# Service worker
|
|
||||||
sw.*
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Vim swap files
|
|
||||||
*.swp
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
useHead({
|
|
||||||
titleTemplate: (chunk) => {
|
|
||||||
return !chunk || chunk === 'Enderman' ? 'Enderman' : `${chunk} – Enderman`
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
id="ender-layout"
|
|
||||||
class="d-flex flex-column align-items-center pt-sm-5 h-100"
|
|
||||||
>
|
|
||||||
<Transition name="page">
|
|
||||||
<Card />
|
|
||||||
</Transition>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Portal
|
|
||||||
layout="#ender-layout"
|
|
||||||
directory="/images/portal"
|
|
||||||
animate
|
|
||||||
randomize
|
|
||||||
fade
|
|
||||||
/>
|
|
||||||
<Footer />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style></style>
|
|
||||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 18 KiB |
@@ -1,22 +0,0 @@
|
|||||||
@charset "UTF-8";
|
|
||||||
@counter-style do {
|
|
||||||
system: cyclic;
|
|
||||||
symbols: "💻" "📌" "👻" "📷";
|
|
||||||
suffix: " ";
|
|
||||||
}
|
|
||||||
@counter-style enjoy {
|
|
||||||
system: cyclic;
|
|
||||||
symbols: "💻" "📌" "👻" "📷" "♟" "⛄";
|
|
||||||
suffix: " ";
|
|
||||||
}
|
|
||||||
.list-style-type-none {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
.list-style-type-do {
|
|
||||||
list-style-type: do;
|
|
||||||
}
|
|
||||||
.list-style-type-enjoy {
|
|
||||||
list-style-type: enjoy;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*# sourceMappingURL=list-types.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["list-types.scss"],"names":[],"mappings":";AAAA;EACE;EACA;EACA;;AAGF;EACE;EACA;EACA;;AAIA;EACE;;AAGF;EACE;;AAGF;EACE","file":"list-types.css"}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
@counter-style do {
|
|
||||||
system: fixed;
|
|
||||||
symbols: "\1F4BB" "\1F4CC" "\1F47B" "\1F4F7";
|
|
||||||
suffix: " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
@counter-style enjoy {
|
|
||||||
system: fixed;
|
|
||||||
symbols: "\2728" "\1F4A1" "\1F4D5" "\1F30D" "\2615" "\26C4";
|
|
||||||
suffix: " ";
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-style-type {
|
|
||||||
&-none {
|
|
||||||
list-style-type: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-do {
|
|
||||||
list-style-type: do;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enjoy {
|
|
||||||
list-style-type: enjoy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
// Fonts.
|
|
||||||
@import '@fontsource/Lato/index.css';
|
|
||||||
@import '@fontsource/Alexandria/700.css';
|
|
||||||
|
|
||||||
$grid-breakpoints: (
|
|
||||||
xs: 0,
|
|
||||||
sm: 601px,
|
|
||||||
md: 996px,
|
|
||||||
lg: 1281px,
|
|
||||||
xl: 1921px,
|
|
||||||
xxl: 2561px
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bootstrap & animations.
|
|
||||||
@import '../../node_modules/bootstrap/scss/bootstrap';
|
|
||||||
@import '../../node_modules/animate.css/animate';
|
|
||||||
@import 'transitions';
|
|
||||||
@import 'list-types';
|
|
||||||
|
|
||||||
// CSS Variables.
|
|
||||||
:root {
|
|
||||||
--animate-duration: 1s;
|
|
||||||
--animate-delay: 0.75s;
|
|
||||||
--animate-repeat: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The HTML page.
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The body element.
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
background-color: rgb(6, 25, 28);
|
|
||||||
background-image: url('~/assets/images/sky.png');
|
|
||||||
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-blend-mode: multiply;
|
|
||||||
background-size: cover;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
|
|
||||||
image-rendering: pixelated;
|
|
||||||
|
|
||||||
color: white;
|
|
||||||
font-family: 'Lato', sans-serif;
|
|
||||||
font-size: 18px;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entry point (Vue mount).
|
|
||||||
#ender-app {
|
|
||||||
flex: 1;
|
|
||||||
image-rendering: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper classes that don't exist in Bootstrap.
|
|
||||||
.no-decoration {
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-align-center {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-small {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-3 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-xs-none {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alex {
|
|
||||||
font-family: 'Alexandria', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query-overridable classes.
|
|
||||||
.background {
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dimensions {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: 0.3s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: cornflowerblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-hi {
|
|
||||||
@extend .link;
|
|
||||||
color: cornflowerblue;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: royalblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-force-dark {
|
|
||||||
@extend .link;
|
|
||||||
|
|
||||||
color: cornflowerblue;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: royalblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.radial-gradient {
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at left,
|
|
||||||
rgba(255, 255, 255, 0.8),
|
|
||||||
rgba(255, 255, 255, 0.75),
|
|
||||||
rgba(255, 255, 255, 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --scrollbar {
|
|
||||||
syntax: '<color>';
|
|
||||||
inherits: true;
|
|
||||||
initial-value: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar {
|
|
||||||
min-height: 128px;
|
|
||||||
|
|
||||||
transition: --scrollbar 0.15s ease;
|
|
||||||
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(0, 0, 0, 0.3);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
scrollbar-color: rgba(255, 255, 255, 0.3) rgba(0, 0, 0, 0.3);
|
|
||||||
--scrollbar: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--scrollbar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
body {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar {
|
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.15) rgba(255, 255, 255, 0.3);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
scrollbar-color: rgba(0, 0, 0, 0.3) rgba(255, 255, 255, 0.3);
|
|
||||||
--scrollbar: rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background-color: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.background {
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.link {
|
|
||||||
&:hover {
|
|
||||||
color: royalblue;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-hi {
|
|
||||||
color: royalblue;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: darkblue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.radial-gradient {
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at left,
|
|
||||||
rgba(0, 0, 0, 0.95),
|
|
||||||
rgba(0, 0, 0, 0.8),
|
|
||||||
rgba(0, 0, 0, 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dynamic classes.
|
|
||||||
@include media-breakpoint-up(sm) {
|
|
||||||
#ender-app {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollbar {
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dimensions {
|
|
||||||
width: 80%;
|
|
||||||
min-height: auto;
|
|
||||||
|
|
||||||
max-height: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-mask {
|
|
||||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 5%, rgba(0, 0, 0, 1) 95%, rgba(0, 0, 0, 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded-card {
|
|
||||||
border-radius: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.display-xs-none {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate__animated-sm {
|
|
||||||
animation-duration: var(--animate-duration);
|
|
||||||
-webkit-animation-fill-mode: both;
|
|
||||||
animation-fill-mode: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate__delay-1-5s {
|
|
||||||
animation-delay: calc(var(--animate-delay) * 1.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(lg) {
|
|
||||||
.dimensions {
|
|
||||||
width: 60%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
|
||||||
.dimensions {
|
|
||||||
width: 40%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-breakpoint-up(xxl) {
|
|
||||||
.dimensions {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
body::-webkit-scrollbar {
|
|
||||||
width: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar-track {
|
|
||||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
body::-webkit-scrollbar-thumb {
|
|
||||||
background-color: darkgrey;
|
|
||||||
outline: 1px solid slategrey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*# sourceMappingURL=scrollbar.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["scrollbar.scss"],"names":[],"mappings":"AAAA;EACI;;;AAGJ;EACI;;;AAGJ;EACE;EACA","file":"scrollbar.css"}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.page-enter-active, .page-leave-active {
|
|
||||||
transition: all 0.4s;
|
|
||||||
}
|
|
||||||
.page-enter-from, .page-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(1rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*# sourceMappingURL=transitions.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sourceRoot":"","sources":["transitions.scss"],"names":[],"mappings":"AACE;EAEE;;AAGF;EAEE;EACA","file":"transitions.css"}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
.page {
|
|
||||||
&-enter-active,
|
|
||||||
&-leave-active {
|
|
||||||
transition: all 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-enter-from,
|
|
||||||
&-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
filter: blur(1em);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<main
|
|
||||||
class="dimensions background overflow-auto d-flex flex-column rounded-card gap-4 gap-sm-2 px-4 pt-4 pb-3 animate__animated-sm animate__delay-1-5s animate__fadeInDown"
|
|
||||||
>
|
|
||||||
<Navigation />
|
|
||||||
<NuxtPage
|
|
||||||
class="scrollbar overflow-x-hidden overflow-y-auto py-sm-4 pe-sm-3 fade-mask"
|
|
||||||
/>
|
|
||||||
</main>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { useRuntimeConfig } from '#imports'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const currentYear = new Date().getFullYear()
|
|
||||||
|
|
||||||
const mailTemplate = `I've just found a bug on https://enderman.ch and would like to report it.%0D%0A%0D%0AWebsite version: ${
|
|
||||||
config.public.version
|
|
||||||
}%0D%0ABuild date: ${
|
|
||||||
config.public.buildDate
|
|
||||||
}%0D%0ATimestamp: ${new Date().toISOString()}%0D%0A%0D%0A%0D%0ASteps to reproduce:%0D%0A{ Explain in detail what happened here, or attach a video/screenshot }%0D%0A%0D%0AAdditional information:%0D%0A{ Helpful information, such as developer console output / Leave empty if none }%0D%0A%0D%0A%0D%0A// Keep in mind that it's just a template, you can change it as you wish! :)`
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<footer
|
|
||||||
class="float user-select-none mb-2 animate__animated animate__delay-1s animate__fadeInUpBig animate__slow text-align-center line-height-1-5"
|
|
||||||
>
|
|
||||||
<span class="transpaque font-small text-shadow">
|
|
||||||
© 2018-{{ currentYear }},
|
|
||||||
<a class="link-hi-force-dark" href="https://enderman.ch">Enderman</a>. All
|
|
||||||
rights reserved. •
|
|
||||||
<EMail
|
|
||||||
class="link-hi-force-dark"
|
|
||||||
address="[email protected]"
|
|
||||||
cc="[email protected]"
|
|
||||||
subject="Bug report: enderman.ch"
|
|
||||||
:body="mailTemplate"
|
|
||||||
>
|
|
||||||
<strong>Report a bug</strong>
|
|
||||||
</EMail>
|
|
||||||
<sub>
|
|
||||||
β{{ config.public.version ? config.public.version : '0.0.0' }} ({{
|
|
||||||
config.public.buildDate ? config.public.buildDate : '1970-01-01'
|
|
||||||
}})
|
|
||||||
</sub>
|
|
||||||
</span>
|
|
||||||
</footer>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.float {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transpaque {
|
|
||||||
color: white;
|
|
||||||
|
|
||||||
opacity: 0.25;
|
|
||||||
transition: ease 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transpaque:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-height-1-5 {
|
|
||||||
line-height: 14px * 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-shadow {
|
|
||||||
text-shadow: black 0 2px 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
src: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
alt: {
|
|
||||||
type: String,
|
|
||||||
default: 'Logo',
|
|
||||||
},
|
|
||||||
width: {
|
|
||||||
type: Number,
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 100,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
type: String,
|
|
||||||
default: 'Title',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
type: String,
|
|
||||||
default: 'Description',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NuxtLink class="no-decoration user-select-none" to="/">
|
|
||||||
<div
|
|
||||||
class="logo d-flex flex-row align-items-center justify-content-sm-center"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="logo-image"
|
|
||||||
draggable="false"
|
|
||||||
:width="props.width"
|
|
||||||
:height="props.height"
|
|
||||||
:src="props.src"
|
|
||||||
:alt="props.alt"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h1 class="alex mb-0">{{ props.title }}</h1>
|
|
||||||
<Separator />
|
|
||||||
<p class="mb-0">{{ props.description }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.logo {
|
|
||||||
> img.logo-image {
|
|
||||||
transition: ease 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
> img.logo-image {
|
|
||||||
transform: scale(105%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { storeToRefs } from 'pinia'
|
|
||||||
import { usePageStore } from '~/stores/pages'
|
|
||||||
|
|
||||||
import logo from '~/assets/images/logo.gif'
|
|
||||||
import aboutIcon from '~/assets/images/icons/info.png'
|
|
||||||
import projectIcon from '~/assets/images/icons/defrag.png'
|
|
||||||
import socialIcon from '~/assets/images/icons/user.png'
|
|
||||||
|
|
||||||
const pageStore = usePageStore()
|
|
||||||
const { pages } = storeToRefs(pageStore)
|
|
||||||
|
|
||||||
const icons = [aboutIcon, projectIcon, socialIcon]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<nav
|
|
||||||
class="d-flex flex-row flex-wrap flex-sm-column flex-lg-row justify-content-around justify-content-sm-start justify-content-lg-between gap-2 gap-lg-3"
|
|
||||||
>
|
|
||||||
<Logo
|
|
||||||
:src="logo"
|
|
||||||
alt="Endermanch"
|
|
||||||
title="Enderman"
|
|
||||||
description="official website"
|
|
||||||
/>
|
|
||||||
<ul
|
|
||||||
class="navbar-nav flex-row align-items-center align-items-sm-start align-items-lg-center justify-content-center gap-3 m-2 m-sm-0 mx-lg-2 my-sm-2 my-lg-0"
|
|
||||||
>
|
|
||||||
<li v-for="(page, index) in pages" :key="index" class="nav-item">
|
|
||||||
<LinkButton
|
|
||||||
:path="page.path"
|
|
||||||
:name="page.name"
|
|
||||||
:icon="icons[index]"
|
|
||||||
alt="Info"
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const props = defineProps({
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: 'white',
|
|
||||||
},
|
|
||||||
height: {
|
|
||||||
type: Number,
|
|
||||||
default: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<hr
|
|
||||||
class="radial-gradient m-0 border-0 w-100"
|
|
||||||
:style="{ height: props.height + 'px' }"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
services:
|
||||||
|
index:
|
||||||
|
container_name: frontend-index
|
||||||
|
image: endermanch/index:latest
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
build:
|
||||||
|
context: ./src
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3670:3000"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import packageJSON from './package.json'
|
|
||||||
|
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
|
||||||
export default defineNuxtConfig({
|
|
||||||
app: {
|
|
||||||
head: {
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: 'en',
|
|
||||||
},
|
|
||||||
link: [
|
|
||||||
{
|
|
||||||
rel: 'icon',
|
|
||||||
type: 'image/x-icon',
|
|
||||||
href: '/favicon.ico',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
meta: [
|
|
||||||
{
|
|
||||||
charset: 'utf-8',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'viewport',
|
|
||||||
content: 'width=device-width, initial-scale=1',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'http-equiv': 'X-UA-Compatible',
|
|
||||||
content: 'ie=edge',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
|
||||||
rootId: 'ender-app',
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
dirs: [
|
|
||||||
{
|
|
||||||
path: '~/components',
|
|
||||||
pathPrefix: false,
|
|
||||||
extensions: ['.vue'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
css: [
|
|
||||||
'~/assets/styles/main.css',
|
|
||||||
'@fortawesome/fontawesome-svg-core/styles.css',
|
|
||||||
],
|
|
||||||
plugins: [],
|
|
||||||
modules: [
|
|
||||||
'nuxt-font-loader',
|
|
||||||
'@nuxt/content',
|
|
||||||
'@nuxtjs/eslint-module',
|
|
||||||
'@pinia/nuxt',
|
|
||||||
'nuxt-quasar-ui',
|
|
||||||
],
|
|
||||||
quasar: {
|
|
||||||
lang: 'en-US',
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
typeCheck: true,
|
|
||||||
},
|
|
||||||
runtimeConfig: {
|
|
||||||
public: {
|
|
||||||
version: JSON.stringify(packageJSON.version).slice(1, -1),
|
|
||||||
buildDate: new Date().toISOString().split('T')[0],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "index",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"build": "nuxt build",
|
|
||||||
"dev": "nuxt dev",
|
|
||||||
"generate": "nuxt generate",
|
|
||||||
"preview": "nuxt preview",
|
|
||||||
"postinstall": "nuxt prepare",
|
|
||||||
"lint:js": "eslint --ext \".js,.ts,.vue\" --ignore-path .gitignore .",
|
|
||||||
"lint:prettier": "prettier --check .",
|
|
||||||
"lint": "npm run lint:js && npm run lint:style && npm run lint:prettier",
|
|
||||||
"lintfix": "prettier --write --list-different . && npm run lint:js -- --fix"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nuxt/content": "^2.9.0",
|
|
||||||
"@nuxt/devtools": "latest",
|
|
||||||
"@nuxt/types": "^2.17.2",
|
|
||||||
"@nuxtjs/eslint-config-typescript": "^12.1.0",
|
|
||||||
"@nuxtjs/eslint-module": "^4.1.0",
|
|
||||||
"@pinia/nuxt": "^0.5.1",
|
|
||||||
"@typescript-eslint/parser": "^6.10.0",
|
|
||||||
"eslint": "^8.53.0",
|
|
||||||
"eslint-config-prettier": "^9.0.0",
|
|
||||||
"eslint-plugin-nuxt": "^4.0.0",
|
|
||||||
"eslint-plugin-prettier": "^5.0.1",
|
|
||||||
"nuxt": "^3.8.1",
|
|
||||||
"nuxt-font-loader": "^2.3.4",
|
|
||||||
"nuxt-quasar-ui": "^2.0.6",
|
|
||||||
"prettier": "^3.0.3",
|
|
||||||
"sass": "^1.69.5",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite-plugin-require": "^0.0.3",
|
|
||||||
"vue": "^3.3.8",
|
|
||||||
"vue-router": "^4.2.5",
|
|
||||||
"vue-tsc": "^1.8.22"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@fontsource/alexandria": "^5.0.8",
|
|
||||||
"@fontsource/jetbrains-mono": "^5.0.17",
|
|
||||||
"@fontsource/lato": "^5.0.17",
|
|
||||||
"@fortawesome/fontawesome": "^1.1.8",
|
|
||||||
"@fortawesome/fontawesome-free": "^6.4.2",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.4.2",
|
|
||||||
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.5",
|
|
||||||
"@quasar/extras": "^1.16.8",
|
|
||||||
"animate.css": "^4.1.1",
|
|
||||||
"bootstrap": "^5.3.2",
|
|
||||||
"humps": "^2.0.1",
|
|
||||||
"pinia": "^2.1.7",
|
|
||||||
"quasar": "^2.14.0"
|
|
||||||
},
|
|
||||||
"version": "0.1.0"
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const age =
|
|
||||||
new Date(Number(new Date()) - Number(new Date('2003-09-18'))).getFullYear() -
|
|
||||||
1970
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'About Me',
|
|
||||||
description: `Nice to meet you!~ I'm Andrew, a ${age}-year-old developer from Kaluga, Russia.`,
|
|
||||||
image: 'https://enderman.ch/images/logo.png',
|
|
||||||
url: 'https://enderman.ch/about',
|
|
||||||
}
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: meta.title,
|
|
||||||
description: meta.description,
|
|
||||||
ogTitle: meta.title,
|
|
||||||
ogDescription: meta.description,
|
|
||||||
ogImage: meta.image,
|
|
||||||
ogUrl: meta.url,
|
|
||||||
ogType: 'website',
|
|
||||||
twitterTitle: meta.title,
|
|
||||||
twitterDescription: meta.description,
|
|
||||||
twitterImage: meta.image,
|
|
||||||
twitterCard: 'summary',
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: 'About',
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: 'en',
|
|
||||||
},
|
|
||||||
link: [
|
|
||||||
{
|
|
||||||
rel: 'icon',
|
|
||||||
type: 'image/x-icon',
|
|
||||||
href: '/favicon.ico',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section>
|
|
||||||
<p>This page shall be added in a future release.</p>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
const socials = [
|
|
||||||
{
|
|
||||||
name: 'YouTube',
|
|
||||||
url: 'https://go.enderman.ch/youtube',
|
|
||||||
icon: 'youtube',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Andrew',
|
|
||||||
url: 'https://go.enderman.ch/andrew',
|
|
||||||
icon: 'youtube',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Twitch',
|
|
||||||
url: 'https://go.enderman.ch/twitch',
|
|
||||||
icon: 'twitch',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TikTok',
|
|
||||||
url: 'https://go.enderman.ch/tiktok',
|
|
||||||
icon: 'tiktok',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Twitter',
|
|
||||||
url: 'https://go.enderman.ch/twitter',
|
|
||||||
icon: 'twitter',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Telegram',
|
|
||||||
url: 'https://go.enderman.ch/telegram',
|
|
||||||
icon: 'telegram',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Discord',
|
|
||||||
url: 'https://go.enderman.ch/discord',
|
|
||||||
icon: 'discord',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'GitHub',
|
|
||||||
url: 'https://go.enderman.ch/github',
|
|
||||||
icon: 'github',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Steam',
|
|
||||||
url: 'https://go.enderman.ch/steam',
|
|
||||||
icon: 'steam',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const meta = {
|
|
||||||
title: 'Enderman Online',
|
|
||||||
description:
|
|
||||||
'Discover me on your favorite social media and find ways to contact me here.',
|
|
||||||
image: 'https://enderman.ch/images/logo.png',
|
|
||||||
url: 'https://enderman.ch/projects',
|
|
||||||
}
|
|
||||||
|
|
||||||
useSeoMeta({
|
|
||||||
title: meta.title,
|
|
||||||
description: meta.description,
|
|
||||||
ogTitle: meta.title,
|
|
||||||
ogDescription: meta.description,
|
|
||||||
ogImage: meta.image,
|
|
||||||
ogUrl: meta.url,
|
|
||||||
ogType: 'website',
|
|
||||||
twitterTitle: meta.title,
|
|
||||||
twitterDescription: meta.description,
|
|
||||||
twitterImage: meta.image,
|
|
||||||
twitterCard: 'summary',
|
|
||||||
})
|
|
||||||
|
|
||||||
useHead({
|
|
||||||
title: 'Socials',
|
|
||||||
htmlAttrs: {
|
|
||||||
lang: 'en',
|
|
||||||
},
|
|
||||||
link: [
|
|
||||||
{
|
|
||||||
rel: 'icon',
|
|
||||||
type: 'image/x-icon',
|
|
||||||
href: '/favicon.ico',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h3 class="alex">Social media</h3>
|
|
||||||
<ul class="list-style-type-none p-0">
|
|
||||||
<li v-for="(page, index) in socials" :key="index">
|
|
||||||
<FontAwesomeIcon :icon="`fa-brands fa-${page.icon}`" />
|
|
||||||
<a class="link-hi" target="_blank" rel="noopener" :href="page.url">{{
|
|
||||||
page.name
|
|
||||||
}}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<h3 class="alex">Contact me</h3>
|
|
||||||
<p>
|
|
||||||
Fanmail:
|
|
||||||
<EMail
|
|
||||||
class="link-hi"
|
|
||||||
address="[email protected]"
|
|
||||||
subject="Hey Enderman!"
|
|
||||||
/><br />
|
|
||||||
Manager: <EMail class="link-hi" address="[email protected]" /><br />
|
|
||||||
Abuse: <EMail class="link-hi" address="[email protected]" />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { library, config } from '@fortawesome/fontawesome-svg-core'
|
|
||||||
|
|
||||||
import { far } from '@fortawesome/free-regular-svg-icons'
|
|
||||||
import { fab } from '@fortawesome/free-brands-svg-icons'
|
|
||||||
import { fas } from '@fortawesome/free-solid-svg-icons'
|
|
||||||
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
// Add icon packs.
|
|
||||||
library.add(far, fab, fas)
|
|
||||||
|
|
||||||
// Let Nuxt manage CSS.
|
|
||||||
config.autoAddCss = false
|
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
|
||||||
nuxtApp.vueApp.component('FontAwesomeIcon', FontAwesomeIcon)
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
FROM oven/bun:alpine AS base
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# install dependencies into temp directory
|
||||||
|
# this will cache them and speed up future builds
|
||||||
|
FROM base AS install
|
||||||
|
RUN mkdir -p /temp
|
||||||
|
|
||||||
|
COPY package.json bun.lockb /temp/
|
||||||
|
RUN cd /temp && bun install --production
|
||||||
|
|
||||||
|
# copy node_modules from temp directory
|
||||||
|
# then copy all (non-ignored) project files into the image
|
||||||
|
FROM base AS prerelease
|
||||||
|
COPY --from=install /temp/node_modules node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
RUN bun test
|
||||||
|
RUN bun --bun run build
|
||||||
|
|
||||||
|
# copy production dependencies and source code into final image
|
||||||
|
FROM base AS release
|
||||||
|
COPY --from=install /temp/node_modules node_modules
|
||||||
|
COPY --from=prerelease /usr/src/app/.output .
|
||||||
|
COPY --from=prerelease /usr/src/app/package.json .
|
||||||
|
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
USER bun
|
||||||
|
|
||||||
|
ENTRYPOINT [ "bun", "run", "server/index.mjs" ]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import config from '../config'
|
||||||
|
|
||||||
|
export default defineAppConfig({
|
||||||
|
...config,
|
||||||
|
})
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import 'iconify-icon'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { reader, animate } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
titleTemplate: (chunk?) => {
|
||||||
|
if (route.fullPath === '/') return config.title.full
|
||||||
|
if (route.fullPath.split('/').length === 2)
|
||||||
|
switch (route.fullPath.split('/')[1]) {
|
||||||
|
case 'blog':
|
||||||
|
return 'The Ender Chest'
|
||||||
|
default:
|
||||||
|
return `${chunk} – ${config.title.short}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunk!
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="ender-layout"
|
||||||
|
class="flex flex-col lm:justify-center lt:justify-center items-center h-full"
|
||||||
|
>
|
||||||
|
<ClientOnly>
|
||||||
|
<LazySwipeControls v-if="!reader" class="block sm:hidden" />
|
||||||
|
</ClientOnly>
|
||||||
|
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
<NuxtLayout name="card">
|
||||||
|
<template #footer>
|
||||||
|
<footer
|
||||||
|
v-if="!reader"
|
||||||
|
class="lm:static sm:fixed sm:bottom-0 sm:left-0 sm:right-0 mx-auto w-auto lm:mb-0 sm:mb-2 pass-through text-center"
|
||||||
|
>
|
||||||
|
<small
|
||||||
|
class="lm:text-inherit sm:accent-text sm:drop-shadow-lg sm:transition-all lm:opacity-100 sm:opacity-25 sm:hover:opacity-100"
|
||||||
|
>
|
||||||
|
© 2018-{{ new Date().getFullYear() }},
|
||||||
|
<a class="sm:link-dark" :href="config.url">Enderman</a>. All rights
|
||||||
|
reserved.
|
||||||
|
<wbr />
|
||||||
|
<sub class="whitespace-nowrap">
|
||||||
|
β{{ config.build.version ? config.build.version : '?.?.?' }} ({{
|
||||||
|
config.build.date ? config.build.date : '1970-01-01'
|
||||||
|
}})
|
||||||
|
</sub>
|
||||||
|
</small>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</NuxtLayout>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ClientOnly>
|
||||||
|
<Portal v-model="animate" layout="#ender-layout" randomize fade />
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
@counter-style do {
|
||||||
|
system: fixed;
|
||||||
|
symbols: "\1F4BB" "\1F4CC" "\1F47B" "\1F4F7";
|
||||||
|
suffix: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
@counter-style enjoy {
|
||||||
|
system: fixed;
|
||||||
|
symbols: "\2728" "\1F4A1" "\1F4BE" "\1F3A5" "\1F4D5" "\1F30D" "\2615" "\26C4";
|
||||||
|
suffix: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
@counter-style faq {
|
||||||
|
system: cyclic;
|
||||||
|
symbols: "\2753" "\1F4A1";
|
||||||
|
suffix: " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-style-type {
|
||||||
|
&-none {
|
||||||
|
list-style-type: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-do {
|
||||||
|
list-style-type: do !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enjoy {
|
||||||
|
list-style-type: enjoy !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-faq {
|
||||||
|
list-style-type: faq !important;
|
||||||
|
|
||||||
|
> li:nth-child(2n):not(:last-child) {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,598 @@
|
|||||||
|
@use 'animate.css';
|
||||||
|
|
||||||
|
// Modules.
|
||||||
|
@use 'transitions';
|
||||||
|
@use 'lists';
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Enchant;
|
||||||
|
src:
|
||||||
|
url("~/assets/fonts/enchant/enchant.woff") format('woff'),
|
||||||
|
url("~/assets/fonts/enchant/enchant.woff2") format('woff2')
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS Variables.
|
||||||
|
:root {
|
||||||
|
--animate-duration: 1s;
|
||||||
|
--animate-delay: 0.75s;
|
||||||
|
--animate-repeat: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-moz-document url-prefix() {
|
||||||
|
html {
|
||||||
|
min-height: 128px;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgb(255 255 255 / 15%) rgb(0 0 0 / 20%);
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
scrollbar-color: rgb(0 0 0 / 15%) rgb(255 255 255 / 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTML page.
|
||||||
|
html {
|
||||||
|
height: 100dvh;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgb(0 0 0 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: rgb(255 255 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: rgb(255 255 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: rgb(255 255 255 / 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background-color: rgb(0 0 0 / 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The body element.
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
|
background-color: rgb(6 25 28);
|
||||||
|
background-image: url('~/assets/images/textures/sky.png');
|
||||||
|
background-attachment: fixed;
|
||||||
|
background-blend-mode: multiply;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
image-rendering: pixelated;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
h1, h2, h3, h4, h5, h6,
|
||||||
|
.text-h1, .text-h2, .text-h3, .text-h4, .text-h5, .text-h6 {
|
||||||
|
@apply font-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .text-h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2, .text-h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, .text-h3 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4, .text-h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5, .text-h5 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6, .text-h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply no-underline transition-all;
|
||||||
|
|
||||||
|
color: cornflowerblue;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: royalblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
center {
|
||||||
|
text-align: center;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(nav) {
|
||||||
|
> ul {
|
||||||
|
@apply ps-6;
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
> ol {
|
||||||
|
@apply ps-6;
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(section, article, aside).page {
|
||||||
|
:is(div, p):not(:last-child) {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h1, h2, h3, h4, h5, h6) {
|
||||||
|
a {
|
||||||
|
@apply text-inherit no-underline transition-all;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: indianred;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
@apply mb-2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(ul, ol) {
|
||||||
|
&:not(:last-child) {
|
||||||
|
@apply mb-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:not(:last-child) {
|
||||||
|
@apply mb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(pre *) {
|
||||||
|
@apply px-1 py-0.5 break-words cursor-pointer rounded-lg transition-ease;
|
||||||
|
|
||||||
|
background-color: rgb(83 35 162 / 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(138 71 245 / 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
@apply max-w-full max-h-[400px];
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
@apply px-4 py-3 mb-4 border-l-4 rounded-r-xl transition-ease;
|
||||||
|
|
||||||
|
border-left-color: rgb(153 153 255 / 60%);
|
||||||
|
background-color: rgb(153 153 255 / 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left-color: rgb(153 153 255 / 100%);
|
||||||
|
background-color: rgb(153 153 255 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
@apply relative font-small overflow-auto min-h-14 max-h-[80rem] rounded-xl p-2 mb-4;
|
||||||
|
|
||||||
|
background-color: rgb(83 35 162 / 5%);
|
||||||
|
|
||||||
|
&:hover > .absolute {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
> code {
|
||||||
|
@apply scroll-p-2;
|
||||||
|
|
||||||
|
counter-reset: step;
|
||||||
|
counter-increment: step 0;
|
||||||
|
|
||||||
|
.line::before {
|
||||||
|
@apply inline-block w-4 mr-6 text-right;
|
||||||
|
|
||||||
|
content: counter(step);
|
||||||
|
counter-increment: step;
|
||||||
|
|
||||||
|
color: rgb(238 246 250 / 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
> span span:last-child {
|
||||||
|
@apply me-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-action {
|
||||||
|
background-color: rgb(153 153 255 / 10%);
|
||||||
|
border-color: rgb(153 153 255 / 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point (Vue mount).
|
||||||
|
#ender-app {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
image-rendering: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper classes that don't exist in Bootstrap.
|
||||||
|
.overlay {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pass-through {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ruby {
|
||||||
|
ruby-position: under;
|
||||||
|
|
||||||
|
ruby {
|
||||||
|
ruby-position: over;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rt {
|
||||||
|
ruby-align: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-small {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query-overridable classes.
|
||||||
|
.dimensions {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.accent-background {
|
||||||
|
background-color: rgb(0 0 0 / 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-overlay-background {
|
||||||
|
background-color: rgb(0 0 0 / 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-text {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-text-shadow {
|
||||||
|
text-shadow: black 2px 3px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-shadow,
|
||||||
|
.accent-drop-shadow {
|
||||||
|
filter: drop-shadow(1px 2px 3px black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-dark {
|
||||||
|
@apply text-inherit no-underline transition-all;
|
||||||
|
|
||||||
|
color: cornflowerblue;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: royalblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-fade {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
black,
|
||||||
|
rgb(0 0 0 / 50%) 75%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-mask {
|
||||||
|
mask-image: linear-gradient(to bottom, transparent, black 3%, black 97%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 640px;
|
||||||
|
|
||||||
|
background-color: rgb(25 20 40 / 95%);
|
||||||
|
box-shadow: rgb(81 75 89 / 50%) 7px 7px 2em;
|
||||||
|
border-radius: 16px !important;
|
||||||
|
|
||||||
|
> div.background {
|
||||||
|
background-image: url('~/assets/images/textures/aero.png');
|
||||||
|
background-attachment: scroll;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
background-blend-mode: lighten;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
opacity: 0.2;
|
||||||
|
|
||||||
|
box-shadow: azure 0 0 20rem;
|
||||||
|
filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
> div.content > img.icon-badge {
|
||||||
|
position: fixed;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px;
|
||||||
|
transform: rotate(-25deg);
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-gradient {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at left,
|
||||||
|
rgb(255 255 255 / 80%),
|
||||||
|
rgb(255 255 255 / 75%),
|
||||||
|
rgb(255 255 255 / 0%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-snap-stop: normal;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
scroll-snap-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-box {
|
||||||
|
border-color: rgb(153 153 255 / 60%);
|
||||||
|
background-color: rgb(45 7 110 / 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgb(153 153 255 / 100%);
|
||||||
|
background-color: rgb(45 7 110 / 30%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-pocket {
|
||||||
|
background-color: rgb(153 153 255 / 10%);
|
||||||
|
border-left-color: rgb(153 153 255 / 60%);
|
||||||
|
border-top-color: rgb(153 153 255 / 60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tag {
|
||||||
|
background-color: rgb(153 153 255 / 10%);
|
||||||
|
border-color: rgb(153 153 255 / 60%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(html.light) {
|
||||||
|
body {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-background {
|
||||||
|
background-color: rgb(255 255 255 / 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-overlay-background {
|
||||||
|
background-color: rgb(255 255 255 / 90%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-drop-shadow {
|
||||||
|
filter: drop-shadow(1px 2px 3px white);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply no-underline transition-all;
|
||||||
|
|
||||||
|
color: royalblue;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: darkblue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-text {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-gradient {
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at left,
|
||||||
|
rgb(0 0 0 / 95%),
|
||||||
|
rgb(0 0 0 / 80%),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-text-shadow {
|
||||||
|
text-shadow: white 1px 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accent-fade {
|
||||||
|
background: linear-gradient(
|
||||||
|
to top,
|
||||||
|
white,
|
||||||
|
rgb(255 255 255 / 50%) 75%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
&-box {
|
||||||
|
border-color: rgb(123 123 255 / 1000%);
|
||||||
|
background-color: rgb(45 7 110 / 20%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: rgb(123 123 255 / 80%);
|
||||||
|
background-color: rgb(45 7 110 / 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-pocket {
|
||||||
|
background-color: rgb(123 123 255 / 10%);
|
||||||
|
border-left-color: rgb(123 123 255 / 80%);
|
||||||
|
border-top-color: rgb(123 123 255 / 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tag {
|
||||||
|
background-color: rgb(123 123 255 / 10%);
|
||||||
|
border-color: rgb(123 123 255 / 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(section, article, aside).page {
|
||||||
|
code:not(pre *) {
|
||||||
|
background-color: rgb(138 71 245 / 20%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgb(83 35 162 / 20%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left-color: rgb(153 153 255 / 60%);
|
||||||
|
background-color: rgb(153 153 255 / 10%);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-left-color: rgb(153 153 255 / 100%);
|
||||||
|
background-color: rgb(153 153 255 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: rgb(83 35 162 / 10%);
|
||||||
|
|
||||||
|
code {
|
||||||
|
.line::before {
|
||||||
|
color: rgb(83 85 89 / 40%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-action {
|
||||||
|
background-color: rgb(153 153 255 / 30%);
|
||||||
|
border-color: rgb(153 153 255 / 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic classes.
|
||||||
|
@screen lm {
|
||||||
|
.dimensions {
|
||||||
|
width: 100% !important;
|
||||||
|
min-height: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ender-app {
|
||||||
|
height: 100dvh;
|
||||||
|
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimensions {
|
||||||
|
width: 90%;
|
||||||
|
|
||||||
|
min-height: fit-content;
|
||||||
|
max-height: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate__animated-sm {
|
||||||
|
animation-duration: var(--animate-duration);
|
||||||
|
animation-fill-mode: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate__delay-1-5s {
|
||||||
|
animation-delay: calc(var(--animate-delay) * 1.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
html {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen lg {
|
||||||
|
html {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dimensions {
|
||||||
|
width: 75%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen xl {
|
||||||
|
.dimensions {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen 2xl {
|
||||||
|
.dimensions {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.page {
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(1em);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch {
|
||||||
|
&-enter-active,
|
||||||
|
&-leave-active {
|
||||||
|
transition: all 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-enter-from,
|
||||||
|
&-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition {
|
||||||
|
&-ease {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-portal {
|
||||||
|
transition: all 0.6942s ease-in;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
&-twitter:hover {
|
||||||
|
transform: translateZ(0) scale(1.1) rotate(-10deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&-back:hover {
|
||||||
|
transform: translateZ(0) scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,5 +39,3 @@ const href =
|
|||||||
</slot>
|
</slot>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sectionLogo: {
|
||||||
|
type: null as unknown as PropType<string | null>,
|
||||||
|
default: null,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
thumbnail: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const sentence = computed(() => {
|
||||||
|
return props.description.split('.').at(0) + '...'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full h-full flex flex-col justify-end text-white relative">
|
||||||
|
<img
|
||||||
|
:src="thumbnail"
|
||||||
|
:alt="alt"
|
||||||
|
class="w-full h-full object-cover absolute top-0 left-0"
|
||||||
|
/>
|
||||||
|
<div class="absolute top-0 left-0 p-5">
|
||||||
|
<img :src="logo" alt="Logo" class="w-[100] h-[100]" />
|
||||||
|
</div>
|
||||||
|
<div v-if="sectionLogo" class="absolute top-0 right-0 p-5">
|
||||||
|
<img :src="sectionLogo" alt="Logo" class="w-[100] h-[100]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-5 px-5" style="text-shadow: black 1px 1px 10px">
|
||||||
|
<h1 class="m-0 text-[60px]">{{ title }}</h1>
|
||||||
|
<strong class="min-h-[90px] m-0 text-[24px]" style="font-family: Lato">
|
||||||
|
{{ sentence }}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button type="button" data-theme-toggle aria-label="Change to light theme">
|
||||||
|
Change to light theme
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const config = useAppConfig()
|
||||||
|
const meta = {
|
||||||
|
title: 'Page not found',
|
||||||
|
description:
|
||||||
|
'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
|
||||||
|
image: `${config.url}/images/logo.png`,
|
||||||
|
url: config.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
let slug = useRoute().params.slug
|
||||||
|
|
||||||
|
if (Array.isArray(slug)) slug = slug.join('/')
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
http_code: 200,
|
||||||
|
title: config.title,
|
||||||
|
url: config.url + '/' + slug,
|
||||||
|
locale: config.locale || 'en',
|
||||||
|
data: {
|
||||||
|
path: '/error.vue',
|
||||||
|
content: {
|
||||||
|
title: 'Page not found!',
|
||||||
|
description:
|
||||||
|
"The page you are looking for does not exist. However, no 404 error has occurred, as you're browsing through a web application loaded into memory and HTTP error codes make no sense here.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slug: useRoute().params.slug,
|
||||||
|
build: config.build,
|
||||||
|
hint: 'Use the navigation bar to get back to the main page.',
|
||||||
|
}
|
||||||
|
const errorString = JSON.stringify(error, null, 2)
|
||||||
|
const buffer = ref('')
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
const dt = 20
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
|
||||||
|
function type() {
|
||||||
|
if (Date.now() > startTime + dt * i) buffer.value += errorString[i++]
|
||||||
|
if (i < errorString.length) requestAnimationFrame(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => setTimeout(type, 1500))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h3>Page not found!</h3>
|
||||||
|
<pre class="whitespace-pre-wrap break-keep font-small">{{
|
||||||
|
buffer
|
||||||
|
}}<span class="blinker">▊</span></pre>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.blinker {
|
||||||
|
animation: blinker 1s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blinker {
|
||||||
|
from {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import sky from '~/assets/images/portal/sky.png'
|
import sky from 'assets/images/textures/sky.png'
|
||||||
import particles from '~/assets/images/portal/particles.png'
|
import particles from 'assets/images/textures/particles.png'
|
||||||
|
|
||||||
|
const { $local } = useNuxtApp()
|
||||||
|
const config = useAppConfig()
|
||||||
|
const fqdn = config.url.split('//').at(1)
|
||||||
|
|
||||||
const resources: string[] = [sky, particles]
|
const resources: string[] = [sky, particles]
|
||||||
|
const animated = defineModel<boolean>({
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
layout: {
|
layout: {
|
||||||
@@ -62,7 +69,7 @@ class Canvas {
|
|||||||
compatible(
|
compatible(
|
||||||
obj: HTMLCanvasElement | HTMLElement | Element,
|
obj: HTMLCanvasElement | HTMLElement | Element,
|
||||||
): obj is HTMLCanvasElement {
|
): obj is HTMLCanvasElement {
|
||||||
return obj.tagName === 'CANVAS'
|
return obj.tagName.toLowerCase() === 'canvas'
|
||||||
}
|
}
|
||||||
|
|
||||||
query(): HTMLCanvasElement {
|
query(): HTMLCanvasElement {
|
||||||
@@ -133,7 +140,7 @@ class Portal {
|
|||||||
constructor(
|
constructor(
|
||||||
directory: string = '/',
|
directory: string = '/',
|
||||||
create: boolean = true,
|
create: boolean = true,
|
||||||
animate: boolean = true,
|
animate: boolean = false,
|
||||||
randomize: boolean = false,
|
randomize: boolean = false,
|
||||||
fade: boolean = false,
|
fade: boolean = false,
|
||||||
speed: number = 1,
|
speed: number = 1,
|
||||||
@@ -249,7 +256,7 @@ class Portal {
|
|||||||
this.animate = animate
|
this.animate = animate
|
||||||
this.randomize = randomize
|
this.randomize = randomize
|
||||||
|
|
||||||
// Set default speed.
|
// Set the default speed.
|
||||||
this.speed = speed
|
this.speed = speed
|
||||||
this.initialSpeed = this.speed
|
this.initialSpeed = this.speed
|
||||||
this.dv = 20
|
this.dv = 20
|
||||||
@@ -316,7 +323,7 @@ class Portal {
|
|||||||
|
|
||||||
notice() {
|
notice() {
|
||||||
console.log(
|
console.log(
|
||||||
`%c \u2587%c\u2587%c\u2587 %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\thttps://enderman.ch\n\thttps://go.enderman.ch/wipet`,
|
`%c \u2587%c\u2587%c\u2587 %c\u2587%c\u2587%c\u2587 %c // ECMAPortal v${this.version} by Endermanch & WiPet\n\n\t${config.url}\n\t${config.shortener}/wipet`,
|
||||||
'color: #E58EFF',
|
'color: #E58EFF',
|
||||||
'color: #D52DFF',
|
'color: #D52DFF',
|
||||||
'color: #E58EFF',
|
'color: #E58EFF',
|
||||||
@@ -327,7 +334,7 @@ class Portal {
|
|||||||
)
|
)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
'If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@enderman.ch',
|
`If any errors occur below, please send a screenshot of them my way!\n\t%ccontact@${fqdn}`,
|
||||||
'color: #87CEFA',
|
'color: #87CEFA',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -573,8 +580,8 @@ class Portal {
|
|||||||
this.clickTime = Date.now()
|
this.clickTime = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
continue() {
|
play() {
|
||||||
if (this.pauseTime === 0) return
|
if (this.pauseTime === 0) this.pauseTime = this.currentTime
|
||||||
|
|
||||||
// Add the time that has passed since the pause to the current time.
|
// Add the time that has passed since the pause to the current time.
|
||||||
this.currentTime += Date.now() - this.pauseTime
|
this.currentTime += Date.now() - this.pauseTime
|
||||||
@@ -607,47 +614,53 @@ class Portal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// If the localStorage value is null, trigger the setup.
|
||||||
|
// If the viewport is too small, disable by default.
|
||||||
|
const ecmaportal: string | null | undefined = $local.getItem('ecmaportal')
|
||||||
|
|
||||||
|
if (ecmaportal === null) {
|
||||||
|
const notMobile = window.innerWidth > 600
|
||||||
|
|
||||||
|
$local.setItem('ecmaportal', notMobile)
|
||||||
|
animated.value = notMobile
|
||||||
|
} else animated.value = ecmaportal === 'true'
|
||||||
|
|
||||||
const layout = document.querySelector(props.layout)
|
const layout = document.querySelector(props.layout)
|
||||||
const portal = new Portal(
|
const portal = new Portal(
|
||||||
props.directory,
|
props.directory,
|
||||||
props.create,
|
props.create,
|
||||||
props.animate,
|
animated.value,
|
||||||
props.randomize,
|
props.randomize,
|
||||||
props.fade,
|
props.fade,
|
||||||
props.speed,
|
props.speed,
|
||||||
)
|
)
|
||||||
|
|
||||||
layout!.addEventListener('mousedown', ((e: UIEvent) => {
|
const click: EventListener = (e: Event) => {
|
||||||
if (e.target === e.currentTarget && e.detail >= 2) {
|
if (e.target === e.currentTarget) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
portal.click()
|
portal.click()
|
||||||
}
|
}
|
||||||
}) as EventListener)
|
}
|
||||||
|
|
||||||
|
for (const event of ['dblclick', 'touchstart'])
|
||||||
|
layout?.addEventListener(event, click)
|
||||||
|
|
||||||
|
// Save the new value to local storage.
|
||||||
|
watch(animated, (newAnimate) => {
|
||||||
|
$local.setItem('ecmaportal', newAnimate)
|
||||||
|
newAnimate ? portal.play() : portal.pause()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<canvas id="ecmaportal" class="parallax">
|
<canvas
|
||||||
|
id="ecmaportal"
|
||||||
|
class="fixed top-0 left-0 opacity-0 transition-portal -z-10"
|
||||||
|
>
|
||||||
<span>
|
<span>
|
||||||
Your browser does not support the <canvas /> element, which is
|
Your browser does not support the <canvas /> element, which is
|
||||||
required for parallax animation.
|
required for parallax animation.
|
||||||
</span>
|
</span>
|
||||||
</canvas>
|
</canvas>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.parallax {
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.6942s ease-in;
|
|
||||||
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<hr class="accent-text accent-gradient border-0 h-px my-4" />
|
||||||
|
<p>
|
||||||
|
<strong>🚧 This page is currently under construction.</strong> Expect a
|
||||||
|
lot more content to be added as time passes.
|
||||||
|
<em>Please report all bugs you encounter via the link in the footer</em>,
|
||||||
|
I will make sure to sand them down.
|
||||||
|
</p>
|
||||||
|
<hr class="accent-text accent-gradient border-0 h-px my-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: 'Logo',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 100,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Title',
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: String,
|
||||||
|
default: 'Description',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
class="flex flex-row items-center text-inherit hover:text-inherit select-none sm:m-auto lm:m-0 lt:m-0 lg:m-0"
|
||||||
|
to="/"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="transition-all"
|
||||||
|
draggable="false"
|
||||||
|
:width="props.width"
|
||||||
|
:height="props.height"
|
||||||
|
:src="props.src"
|
||||||
|
:alt="props.alt"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2>{{ props.title }}</h2>
|
||||||
|
<hr class="accent-text accent-gradient border-0 h-px" />
|
||||||
|
<p>{{ props.description }}</p>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
a:hover > img {
|
||||||
|
transform: scale(105%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { usePageStore } from '~/stores/pages'
|
||||||
|
|
||||||
|
import logo from 'assets/images/logo.avif'
|
||||||
|
|
||||||
|
const pageStore = usePageStore()
|
||||||
|
const { pages } = storeToRefs(pageStore)
|
||||||
|
|
||||||
|
const links = toRaw(pages.value).filter((page) => page.path !== '/')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<nav
|
||||||
|
class="flex flex-row flex-wrap lm:flex-row lt:flex-row sm:flex-col lg:flex-row justify-around sm:justify-start lm:justify-between lt:justify-between lg:justify-between gap-2 lg:gap-4"
|
||||||
|
>
|
||||||
|
<Logo
|
||||||
|
:src="logo"
|
||||||
|
alt="Endermanch"
|
||||||
|
title="Enderman"
|
||||||
|
description="official website"
|
||||||
|
/>
|
||||||
|
<ul
|
||||||
|
class="flex flex-row flex-wrap items-center sm:items-start lm:items-center lt:items-center lg:items-center justify-center gap-4 m-2 sm:m-0 lm:mx-2 lt:mx-2 lg:mx-2 sm:my-2 lg:my-0"
|
||||||
|
>
|
||||||
|
<li v-for="(page, index) in links" :key="index" class="nav-item">
|
||||||
|
<Route
|
||||||
|
:path="page.path"
|
||||||
|
:name="page.name"
|
||||||
|
:icon="page.icon!.src"
|
||||||
|
:alt="page.icon!.alt"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
@@ -17,23 +17,32 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'Link',
|
default: 'Link',
|
||||||
},
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 32,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 32,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
class="link d-flex flex-row align-items-center gap-2 user-select-none no-decoration"
|
class="flex flex-row items-center gap-2 select-none text-inherit no-underline"
|
||||||
active-class="icon-active px-sm-3"
|
active-class="active"
|
||||||
:to="!props.external ? props.path : undefined"
|
:to="!props.external ? props.path : undefined"
|
||||||
:href="props.external ? props.path : undefined"
|
:href="props.external ? props.path : undefined"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="icon-image"
|
|
||||||
draggable="false"
|
draggable="false"
|
||||||
:src="props.icon"
|
:src="props.icon"
|
||||||
:alt="props.alt"
|
:alt="props.alt"
|
||||||
|
:width="props.width"
|
||||||
|
:height="props.height"
|
||||||
/>
|
/>
|
||||||
<span class="display-xs-none">
|
<span class="hidden lm:hidden lt:hidden sm:block">
|
||||||
<strong>
|
<strong>
|
||||||
{{ props.name }}
|
{{ props.name }}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -42,14 +51,8 @@ const props = defineProps({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.icon {
|
.active {
|
||||||
&-active {
|
@apply px-4;
|
||||||
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
|
transform: translate(2px, 2px) rotate3d(1, 1, 1, 5deg) scale(1.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
&-image {
|
|
||||||
width: 1.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import cogIcon from 'assets/images/icons/cog.avif'
|
||||||
|
import pearlIcon from 'assets/images/icons/pearl.avif'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const { animate } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
const fqdn = config.url.split('//').at(1)
|
||||||
|
const mailTemplate = `I've just found a bug on ${config.url} and would like to report it.%0D%0A%0D%0AWebsite version: ${
|
||||||
|
config.build.version
|
||||||
|
}%0D%0ABuild date: ${
|
||||||
|
config.build.date
|
||||||
|
}%0D%0ATimestamp: ${new Date().toISOString()}%0D%0A%0D%0A%0D%0ASteps to reproduce:%0D%0A{ Explain in detail what happened here, or attach a video/screenshot }%0D%0A%0D%0AAdditional information:%0D%0A{ Helpful information, such as developer console output / Leave empty if none }%0D%0A%0D%0A%0D%0A// Keep in mind that it's just a template, you can change it as you wish! :)`
|
||||||
|
|
||||||
|
const active = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 right-0 flex flex-col cursor-pointer select-none p-2"
|
||||||
|
@click="active = true"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
:src="cogIcon"
|
||||||
|
alt="Options"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="animate__animated animate__fadeIn"
|
||||||
|
leave-active-class="animate__animated animate__fadeOut"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="active"
|
||||||
|
class="fixed top-0 left-0 flex flex-row justify-center items-center w-full h-full z-20"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative flex flex-col gap-4 accent-overlay-background rounded-xl max-w-[800px] mx-4 p-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
:src="pearlIcon"
|
||||||
|
alt="Pearl"
|
||||||
|
class="absolute -top-8 -left-6 badge select-none w-16 lg:w-auto"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3 class="text-center">Site settings</h3>
|
||||||
|
|
||||||
|
<div class="overlay accent-overlay-background" />
|
||||||
|
<div class="overlay">
|
||||||
|
<p class="mb-3">
|
||||||
|
The site settings are experimental. Suggest what you want to see
|
||||||
|
here next!
|
||||||
|
<strong>Please report any bugs that may occur.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<div class="md:basis-0 flex-grow">
|
||||||
|
<strong>Theme</strong>
|
||||||
|
<p>Available in vanilla and chocolate flavors.</p>
|
||||||
|
|
||||||
|
<select
|
||||||
|
v-model="$colorMode.preference"
|
||||||
|
class="accent accent-overlay-background"
|
||||||
|
>
|
||||||
|
<option value="system">System</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="sepia">Sepia</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="md:basis-0 flex-grow">
|
||||||
|
<strong>Animation</strong>
|
||||||
|
<p>
|
||||||
|
Some computers may have issues rendering that gorgeous
|
||||||
|
background animation. Disabling it substantially decreases power
|
||||||
|
consumption, but also makes the website less cool.
|
||||||
|
</p>
|
||||||
|
<input v-model="animate" type="checkbox" color="primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="mt-3">
|
||||||
|
<EMail
|
||||||
|
:address="`contact@${fqdn}`"
|
||||||
|
:cc="`admin@${fqdn}`"
|
||||||
|
:subject="`Bug report: ${fqdn}`"
|
||||||
|
:body="mailTemplate"
|
||||||
|
>
|
||||||
|
<strong>Report a bug</strong>
|
||||||
|
</EMail>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="active = false">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.badge {
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const { pages } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const state = computed(() => {
|
||||||
|
const i = pages.value.indexOf(
|
||||||
|
pages.value.find(
|
||||||
|
(page) => page.path === '/' + route.fullPath.split('/')[1],
|
||||||
|
)!,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
current: route.fullPath,
|
||||||
|
index: i,
|
||||||
|
next: toRaw(pages.value)[(i + 1) % pages.value.length].path,
|
||||||
|
prev: toRaw(pages.value)[i - 1 < 0 ? pages.value.length - (i + 1) : i - 1]
|
||||||
|
.path,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex fixed flex-row justify-between items-center w-full h-full pass-through"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
class="opacity-25 hover:opacity-100 text-inherit no-underline transition-all"
|
||||||
|
:to="state.prev"
|
||||||
|
>
|
||||||
|
<iconify-icon icon="iconoir:nav-arrow-left" inline />
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
|
<NuxtLink class="control-icon text-inherit no-underline" :to="state.next">
|
||||||
|
<iconify-icon icon="iconoir:nav-arrow-right" inline />
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
|
||||||
|
import warningIcon from 'assets/images/icons/accent/warning.avif'
|
||||||
|
import errorIcon from 'assets/images/icons/accent/error.avif'
|
||||||
|
import infoIcon from 'assets/images/icons/accent/info.avif'
|
||||||
|
import helpIcon from 'assets/images/icons/accent/help.avif'
|
||||||
|
import checkIcon from 'assets/images/icons/accent/checkmark.avif'
|
||||||
|
import ideaIcon from 'assets/images/icons/accent/idea.avif'
|
||||||
|
|
||||||
|
type IconOptions = 'warning' | 'error' | 'info' | 'help' | 'checkmark' | 'idea'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String as PropType<IconOptions>,
|
||||||
|
default: 'warning',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Important',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const cards = {
|
||||||
|
warning: {
|
||||||
|
color: 'border-warning',
|
||||||
|
icon: {
|
||||||
|
src: warningIcon,
|
||||||
|
alt: 'Warning triangle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
color: 'border-error',
|
||||||
|
icon: {
|
||||||
|
src: errorIcon,
|
||||||
|
alt: 'Error circle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
color: 'border-info',
|
||||||
|
icon: {
|
||||||
|
src: infoIcon,
|
||||||
|
alt: 'Information circle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
color: 'border-help',
|
||||||
|
icon: {
|
||||||
|
src: helpIcon,
|
||||||
|
alt: 'Help circle',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
checkmark: {
|
||||||
|
color: 'border-checkmark',
|
||||||
|
icon: {
|
||||||
|
src: checkIcon,
|
||||||
|
alt: 'Checkmark',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
idea: {
|
||||||
|
color: 'border-idea',
|
||||||
|
icon: {
|
||||||
|
src: ideaIcon,
|
||||||
|
alt: 'Lightbulb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="tilt flex flex-row items-center gap-2 mb-0 sm:mb-2 md:mb-4">
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
:src="cards[props.type].icon.src"
|
||||||
|
:alt="cards[props.type].icon.alt"
|
||||||
|
class="w-12 h-12"
|
||||||
|
/>
|
||||||
|
<h3 class="whitespace-nowrap mb-0">{{ props.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:class="`box p-4 mb-4 ${cards[props.type].color} border-b border-r rounded-xl`"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.tilt {
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['copy'])
|
||||||
|
|
||||||
|
const clicked = ref<boolean>(false)
|
||||||
|
|
||||||
|
function p() {
|
||||||
|
emit('copy')
|
||||||
|
|
||||||
|
if (!clicked.value) setTimeout(() => (clicked.value = false), 2000)
|
||||||
|
clicked.value = true
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="absolute flex flex-col justify-start top-0 left-0 w-full h-full pointer-events-none opacity-0 hover:opacity-100 transition-ease"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row justify-end items-center p-2">
|
||||||
|
<span
|
||||||
|
class="overlay-action flex-grow-0 pointer-events-auto cursor-pointer p-2 border rounded-xl"
|
||||||
|
@click="p"
|
||||||
|
>
|
||||||
|
<Transition name="switch" mode="out-in">
|
||||||
|
<iconify-icon v-if="!clicked" icon="mdi:content-copy" inline />
|
||||||
|
<iconify-icon
|
||||||
|
v-else
|
||||||
|
icon="mdi:check-bold"
|
||||||
|
class="text-green-500"
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
</Transition>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { UseSwipeDirection } from '@vueuse/core'
|
||||||
|
import { useSwipe } from '@vueuse/core'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const card = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const { pages, reader } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
const swipe = useSwipe(card, {
|
||||||
|
passive: true,
|
||||||
|
onSwipeEnd: (_touch: TouchEvent, _direction: UseSwipeDirection) => {
|
||||||
|
if (reader.value) return
|
||||||
|
|
||||||
|
const currentPage = pages.value.find(
|
||||||
|
(page) => page.path === route.fullPath,
|
||||||
|
)!
|
||||||
|
|
||||||
|
const currentIndex = pages.value.indexOf(currentPage)
|
||||||
|
|
||||||
|
if (
|
||||||
|
card.value?.offsetWidth &&
|
||||||
|
Math.abs(swipe.lengthX.value) > 1.5 * Math.abs(swipe.lengthY.value) &&
|
||||||
|
Math.abs(swipe.lengthX.value) / card.value?.offsetWidth >= 0.1
|
||||||
|
) {
|
||||||
|
router.push(
|
||||||
|
toRaw(pages.value)[
|
||||||
|
swipe.lengthX.value > 0
|
||||||
|
? (currentIndex + 1) % pages.value.length
|
||||||
|
: currentIndex - 1 < 0
|
||||||
|
? pages.value.length - (currentIndex + 1)
|
||||||
|
: currentIndex - 1
|
||||||
|
].path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const animationComplete = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
card.value?.addEventListener(
|
||||||
|
'animationend',
|
||||||
|
() => (animationComplete.value = true),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main
|
||||||
|
ref="card"
|
||||||
|
class="dimensions accent-background transition-ease overflow-auto flex flex-col gap-4 lm:gap-0 sm:gap-2 py-2 px-6 sm:p-4"
|
||||||
|
:class="{
|
||||||
|
'animate__animated-sm animate__delay-1-5s animate__fadeInDown':
|
||||||
|
!reader && !animationComplete,
|
||||||
|
'animate__animated-sm animate__fadeIn': reader && !animationComplete,
|
||||||
|
'lm:rounded-none sm:rounded-xl sm:mt-8 lm:mt-0 lt:mt-0': !reader,
|
||||||
|
'!max-h-full h-full': reader,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
<Navigation />
|
||||||
|
<slot name="header" />
|
||||||
|
<NuxtPage
|
||||||
|
class="sm:fade-mask flex-grow overflow-y-auto h-full sm:py-4 sm:pe-4"
|
||||||
|
/>
|
||||||
|
<slot name="footer" />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const { reader } = storeToRefs(usePageStore())
|
||||||
|
|
||||||
|
reader.value = /\/blog\/(.*)/.test(to.path)
|
||||||
|
})
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const config = useAppConfig()
|
||||||
const meta = {
|
const meta = {
|
||||||
title: "Enderman's Projects",
|
title: 'Page not found',
|
||||||
description:
|
description:
|
||||||
"A display of my largest projects. Some of them aren't published on YouTube.",
|
'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
|
||||||
image: 'https://enderman.ch/images/logo.png',
|
image: `${config.url}/images/logo.png`,
|
||||||
url: 'https://enderman.ch/projects',
|
url: config.url,
|
||||||
}
|
}
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
@@ -22,9 +23,9 @@ useSeoMeta({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Projects',
|
title: 'Page not found',
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'en',
|
lang: config.locale || 'en',
|
||||||
},
|
},
|
||||||
link: [
|
link: [
|
||||||
{
|
{
|
||||||
@@ -37,9 +38,5 @@ useHead({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<NotFound />
|
||||||
<p>This page shall be added in a future release.</p>
|
|
||||||
</section>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineRouteRules } from '#imports'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const age =
|
||||||
|
new Date(Number(new Date()) - Number(new Date('2003-09-18'))).getFullYear() -
|
||||||
|
1970
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'About Me',
|
||||||
|
description: `Nice to meet you!~ I'm Andrew, a ${age}-year-old developer from Kaluga, Russia.`,
|
||||||
|
image: `${config.url}/images/logo.png`,
|
||||||
|
url: `${config.url}/about`,
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: meta.title,
|
||||||
|
description: meta.description,
|
||||||
|
ogTitle: meta.title,
|
||||||
|
ogDescription: meta.description,
|
||||||
|
ogImage: meta.image,
|
||||||
|
ogUrl: meta.url,
|
||||||
|
ogType: 'website',
|
||||||
|
twitterTitle: meta.title,
|
||||||
|
twitterDescription: meta.description,
|
||||||
|
twitterImage: meta.image,
|
||||||
|
twitterCard: 'summary',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'About',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
sitemap: {
|
||||||
|
changefreq: 'yearly',
|
||||||
|
priority: 0.6,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page">
|
||||||
|
<h3>About me</h3>
|
||||||
|
<Construction />
|
||||||
|
<p>
|
||||||
|
Nice to meet you! I'm Andrew, a {{ age }}-year-old guy from Kaluga,
|
||||||
|
Russia. I have been developing software since I was 10, and I have always
|
||||||
|
been interested in technology. I'm a middle-senior C/++ developer and a
|
||||||
|
junior full-stack engineer.
|
||||||
|
<sup>1</sup>
|
||||||
|
<br />
|
||||||
|
<small>
|
||||||
|
<sup>1</sup>
|
||||||
|
All titles are based on knowledge and confidence, not on official work
|
||||||
|
experience.
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
<p><strong>Here's a little more about myself:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>My favorite season is winter.</li>
|
||||||
|
<li>My favorite bands are Black Sabbath, Metallica and Opeth.</li>
|
||||||
|
<li>My favorite field in mathematics is Algebraic Geometry.</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
I work on all sorts of projects, most of the time simultaneously, which
|
||||||
|
doesn't help with adhering to deadlines at all. However, you can always
|
||||||
|
check them out!
|
||||||
|
</p>
|
||||||
|
<h5>FAQ</h5>
|
||||||
|
<ul class="list-style-type-faq">
|
||||||
|
<li><strong>Why are you called Endermanch?</strong></li>
|
||||||
|
<li>
|
||||||
|
There's no particular reason to it. When I was rebranding my YouTube
|
||||||
|
channel from Ender's Show to Enderman, the short URL was taken. I went
|
||||||
|
thinking for quite some time, and then concluded that adding «ch» at the
|
||||||
|
end sounded pretty good. I was 14 at the time of making that decision.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>
|
||||||
|
When will you make a new video? What happened to your schedule?
|
||||||
|
</strong>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
One day. Haven't been particularly motivated lately, but life's busy. 🙁
|
||||||
|
I'm alive and well, though.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { formatDate } from 'date-fns'
|
||||||
|
import chestAnimation from 'assets/images/chest.avif'
|
||||||
|
import { render } from 'vue'
|
||||||
|
import { CodeControls } from '#components'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
let slug = route.params.slug
|
||||||
|
|
||||||
|
if (Array.isArray(slug)) slug = slug.join('/')
|
||||||
|
|
||||||
|
const { data, status } = await useAsyncData('home', () =>
|
||||||
|
queryContent(`/blog/${slug}`).findOne(),
|
||||||
|
)
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: data.value ? data.value.title : 'Page not found',
|
||||||
|
description: data.value
|
||||||
|
? data.value.description
|
||||||
|
: 'The page you are looking for does not exist. It might have been removed, had its name changed, or is temporarily unavailable.',
|
||||||
|
url: `${config.url}/blog`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// When defineOgImage is used, useSeoMeta must exclude ogImage and twitterImage properties.
|
||||||
|
useSeoMeta({
|
||||||
|
title: meta.title,
|
||||||
|
description: meta.description,
|
||||||
|
ogTitle: meta.title,
|
||||||
|
ogDescription: meta.description,
|
||||||
|
// ogImage: EXCLUDED
|
||||||
|
ogUrl: meta.url,
|
||||||
|
ogType: 'article',
|
||||||
|
twitterTitle: meta.title,
|
||||||
|
twitterDescription: meta.description,
|
||||||
|
// twitterImage: EXCLUDED
|
||||||
|
twitterCard: 'summary_large_image',
|
||||||
|
})
|
||||||
|
|
||||||
|
const clipboard = (el: HTMLElement) => {
|
||||||
|
if (el.textContent)
|
||||||
|
navigator.clipboard.writeText(el.textContent).catch(() => {
|
||||||
|
console.error(
|
||||||
|
'Failed to copy element data to the clipboard! Element data:',
|
||||||
|
el,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = ref<Element | null>(null)
|
||||||
|
const thumbnail: string =
|
||||||
|
data.value!.thumbnail ??
|
||||||
|
`/images/blog/thumbnails/${data.value!._path!.split('/').at(-1)}.png`
|
||||||
|
|
||||||
|
// Generate the article's Open Graph image.
|
||||||
|
defineOgImageComponent(
|
||||||
|
'ArticleThumbnail',
|
||||||
|
{
|
||||||
|
title: data.value!.title,
|
||||||
|
description: data.value!.description,
|
||||||
|
logo: '/images/logo.png',
|
||||||
|
sectionLogo: '/images/chest.png',
|
||||||
|
thumbnail,
|
||||||
|
alt: data.value!.title,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fonts: ['Alexandria:700', 'Lato:700'],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Hydrate the rendered items.
|
||||||
|
onMounted(() => {
|
||||||
|
if (route.hash)
|
||||||
|
content.value
|
||||||
|
?.querySelector(route.hash.toLowerCase())
|
||||||
|
?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
|
||||||
|
content.value
|
||||||
|
?.querySelectorAll('code:not(pre *)')
|
||||||
|
.forEach((code: Element) => {
|
||||||
|
if (code instanceof HTMLElement)
|
||||||
|
code.addEventListener('click', () => clipboard(code))
|
||||||
|
})
|
||||||
|
|
||||||
|
content.value?.querySelectorAll('pre').forEach((pre: HTMLPreElement) => {
|
||||||
|
const overlay = h(CodeControls, { onCopy: () => clipboard(pre) })
|
||||||
|
|
||||||
|
render(overlay, pre)
|
||||||
|
})
|
||||||
|
|
||||||
|
content.value?.querySelectorAll('a').forEach((a: HTMLAnchorElement) => {
|
||||||
|
if (
|
||||||
|
a.hash &&
|
||||||
|
a.host === window.location.host &&
|
||||||
|
a.pathname.toLowerCase() === route.path.toLowerCase()
|
||||||
|
)
|
||||||
|
a.addEventListener('click', (e: Event) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
content.value
|
||||||
|
?.querySelector(a.hash.toLowerCase())
|
||||||
|
?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
|
||||||
|
history.replaceState(history.state, '', a.hash)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: data.value!.title,
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="!data"
|
||||||
|
class="flex flex-col items-center justify-center gap-4 w-full select-none text-center"
|
||||||
|
>
|
||||||
|
<img draggable="false" :src="chestAnimation" alt="The Ender Chest" />
|
||||||
|
<div>
|
||||||
|
<h1 class="font-enchant">Loading document...</h1>
|
||||||
|
<span class="opacity-0 hover:opacity-100 transition-ease">
|
||||||
|
<em>Loading document...</em>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article
|
||||||
|
v-else-if="data"
|
||||||
|
class="flex-grow post snap-normal fade-mask-sm flex flex-col gap-4 overflow-y-auto sm:py-4 sm:pe-4"
|
||||||
|
>
|
||||||
|
<div class="grid-thumbnail grid max-w-[768px] snap-end">
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
:src="thumbnail!"
|
||||||
|
alt="Thumbnail"
|
||||||
|
class="grid-thumbnail-image object-cover object-center rounded-xl aspect-[16/9] select-none"
|
||||||
|
/>
|
||||||
|
<NuxtLink to="/blog" class="grid-thumbnail-start button-back p-2">
|
||||||
|
<iconify-icon
|
||||||
|
icon="icon-park-solid:back"
|
||||||
|
width="2em"
|
||||||
|
height="2em"
|
||||||
|
class="drop-shadow text-pink-200"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
<NuxtLink
|
||||||
|
class="grid-thumbnail-end button-twitter p-2"
|
||||||
|
:href="`https://twitter.com/share?url=${config.url}/blog/${slug}&text=${data.title}&hashtags=${data.tags.slice(0, 3).join(',').replace(/ /g, '')}`"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<iconify-icon
|
||||||
|
icon="logos:twitter"
|
||||||
|
width="2em"
|
||||||
|
height="2em"
|
||||||
|
class="drop-shadow"
|
||||||
|
/>
|
||||||
|
</NuxtLink>
|
||||||
|
<div
|
||||||
|
class="grid-thumbnail-title accent-fade w-full rounded-b-lg p-2 mt-16"
|
||||||
|
>
|
||||||
|
<h3 class="mb-1">{{ data.title }}</h3>
|
||||||
|
<div class="flex flex-row flex-wrap gap-x-2 gap-y-0">
|
||||||
|
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
|
||||||
|
<iconify-icon icon="mdi:calendar" inline />
|
||||||
|
<strong>
|
||||||
|
{{ formatDate(data.created, 'HH:mm • LLLL do, y') }}
|
||||||
|
</strong>
|
||||||
|
</small>
|
||||||
|
<small class="flex flex-row items-center gap-1 whitespace-nowrap">
|
||||||
|
<iconify-icon icon="mdi:clock-outline" inline />
|
||||||
|
<strong>
|
||||||
|
{{ data!.readingTime.text.split(' ')[0] + ' minutes to read' }}
|
||||||
|
</strong>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section ref="content" class="page snap-start">
|
||||||
|
<ContentRenderer :value="data" />
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
<NotFound v-else />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.grid-thumbnail {
|
||||||
|
&-image {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
place-self: start center;
|
||||||
|
|
||||||
|
max-width: clamp(1px, 100%, 768px);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-title {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
place-self: end center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-start {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
place-self: start start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-end {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
|
||||||
|
place-self: start end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { formatDate } from 'date-fns'
|
||||||
|
import { defineRouteRules } from '#imports'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const meta = {
|
||||||
|
title: 'The Ender Chest',
|
||||||
|
description:
|
||||||
|
'A blog about software development, scientific research and Windows quirks.',
|
||||||
|
image: `${config.url}/images/chest.png`,
|
||||||
|
url: `${config.url}/blog`,
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: meta.title,
|
||||||
|
description: meta.description,
|
||||||
|
ogTitle: meta.title,
|
||||||
|
ogDescription: meta.description,
|
||||||
|
ogImage: meta.image,
|
||||||
|
ogUrl: meta.url,
|
||||||
|
ogType: 'website',
|
||||||
|
twitterTitle: meta.title,
|
||||||
|
twitterDescription: meta.description,
|
||||||
|
twitterImage: meta.image,
|
||||||
|
twitterCard: 'summary',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'The Ender Chest',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
sitemap: {
|
||||||
|
changefreq: 'monthly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section>
|
||||||
|
<h3 class="mb-2">The Ender Chest</h3>
|
||||||
|
<p class="mb-4">
|
||||||
|
You're browsing the Ender Chest — a blog about software development,
|
||||||
|
scientific research and Windows quirks.
|
||||||
|
</p>
|
||||||
|
<h4 class="mb-2">Recent posts</h4>
|
||||||
|
<ContentList
|
||||||
|
:query="{
|
||||||
|
path: '/blog',
|
||||||
|
where: [{ draft: false }],
|
||||||
|
limit: 3,
|
||||||
|
sort: [{ created: -1 }],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #default="{ list }">
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<NuxtLink
|
||||||
|
v-for="post in list"
|
||||||
|
:key="post._path"
|
||||||
|
:to="post._path"
|
||||||
|
class="text-inherit hover:text-inherit no-underline"
|
||||||
|
>
|
||||||
|
<article
|
||||||
|
class="post-box relative flex flex-col md:flex-row gap-4 p-2 border-2 rounded-xl transition-ease"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
draggable="false"
|
||||||
|
:src="
|
||||||
|
post.thumbnail ??
|
||||||
|
`/images/blog/thumbnails/${post._path!.split('/').at(-1)}.png`
|
||||||
|
"
|
||||||
|
:alt="post.title"
|
||||||
|
class="md:flex-grow-[2] md:basis-0 xl:flex-grow-1 object-cover object-center aspect-[16/9] w-fit md:w-0 xl:w-fit h-full max-h-[300px] rounded-xl"
|
||||||
|
/>
|
||||||
|
<div class="md:flex-grow-[3] md:basis-0 xl:flex-grow-1 w-full">
|
||||||
|
<h3 class="post-title md:line-clamp-2 md:overflow-ellipsis">
|
||||||
|
{{ post.title }}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
class="post-description mb-0 md:line-clamp-4 md:overflow-ellipsis"
|
||||||
|
>
|
||||||
|
{{ post.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap gap-2 py-2">
|
||||||
|
<small
|
||||||
|
v-for="(tag, index) in post.tags.slice(0, 3)"
|
||||||
|
:key="index"
|
||||||
|
class="post-tag px-2 py-1 rounded-xl border whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</small>
|
||||||
|
<small
|
||||||
|
v-if="post.tags.length > 3"
|
||||||
|
class="post-tag px-2 py-1 rounded-xl border whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{{ post.tags.length - 3 }} more
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="post-details py-2">
|
||||||
|
<div class="flex flex-row items-center gap-2">
|
||||||
|
<iconify-icon icon="mdi:calendar" />
|
||||||
|
<small class="whitespace-nowrap">
|
||||||
|
{{ formatDate(post.created, 'HH:mm • LLLL do, y') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="post.updated"
|
||||||
|
class="flex flex-row items-center gap-2"
|
||||||
|
>
|
||||||
|
<iconify-icon icon="mdi:pencil" />
|
||||||
|
<small class="whitespace-nowrap">
|
||||||
|
{{ formatDate(post.updated, 'HH:mm • LLLL do, y') }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="post-pocket border-l border-t absolute bottom-0 right-0 uppercase rounded-tl-xl flex flex-row items-center gap-2 p-2"
|
||||||
|
>
|
||||||
|
<iconify-icon icon="mdi:clock-outline" />
|
||||||
|
<small class="whitespace-nowrap font-mono font-small">
|
||||||
|
{{ post.readingTime.text }}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #not-found>
|
||||||
|
<div class="flex flex-col justify-center items-center gap-4">
|
||||||
|
<span>No posts found 🙁</span>
|
||||||
|
<NuxtLink to="/">Back to index</NuxtLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ContentList>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { defineRouteRules } from '#imports'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
const meta = {
|
const meta = {
|
||||||
title: 'Enderman',
|
title: 'Enderman',
|
||||||
description:
|
description: config.description,
|
||||||
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature.',
|
image: `${config.url}/images/logo.png`,
|
||||||
image: 'https://enderman.ch/images/logo.png',
|
url: config.url,
|
||||||
url: 'https://enderman.ch/',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
@@ -34,30 +36,36 @@ useHead({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
sitemap: {
|
||||||
|
changefreq: 'yearly',
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section>
|
<section class="page">
|
||||||
<h3 class="alex">Welcome 👋</h3>
|
<h3>Welcome 👋</h3>
|
||||||
<p>
|
<p>
|
||||||
I'm <strong>Enderman</strong> – a software engineer, a malware
|
I'm <strong>Enderman</strong> – a software engineer, a malware
|
||||||
enthusiast and most importantly, a weird tall creature. I have over 300K
|
enthusiast and most importantly, a weird tall creature. I have over 300K
|
||||||
subscribers on
|
subscribers on <a :href="`${config.shortener}/youtube`">YouTube</a> and
|
||||||
<a class="link-hi" href="https://go.enderman.ch/youtube">YouTube</a> and
|
over 25K followers on
|
||||||
over 20K followers on
|
<a :href="`${config.shortener}/twitter`">Twitter</a>. Sometimes I wish
|
||||||
<a class="link-hi" href="https://go.enderman.ch/twitter">Twitter</a>.
|
there were 48 hours in a day.
|
||||||
Sometimes I wish there were 48 hours in a day.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="row g-3">
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
<div class="col-12 col-md-8">
|
<div class="md:basis-0 md:flex-grow-[2]">
|
||||||
<h5 class="alex">What I do</h5>
|
<h6>What I do</h6>
|
||||||
<ul class="list-style-type-do">
|
<ul class="list-style-type-do">
|
||||||
<li>
|
<li>
|
||||||
I develop cross-platform software. My language of choice is usually
|
I develop cross-platform software. My language of choice is usually
|
||||||
<strong>C/C++</strong> for desktop applications and any common
|
<strong>C/C++</strong> for desktop applications and any common
|
||||||
backend stack with framework-loaded TypeScript for web. You can take
|
backend stack with framework-loaded TypeScript for web. You can take
|
||||||
a look at my code on
|
a look at my code on
|
||||||
<a class="link-hi" href="https://go.enderman.ch/github">GitHub</a>.
|
<a :href="`${config.shortener}/github`">GitHub</a>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
I have the most unnecessary, yet fascinating knowledge about
|
I have the most unnecessary, yet fascinating knowledge about
|
||||||
@@ -70,21 +78,22 @@ useHead({
|
|||||||
<li>
|
<li>
|
||||||
I research and analyze modern malware, educate computer users about
|
I research and analyze modern malware, educate computer users about
|
||||||
it and preserve history. The repository can be found
|
it and preserve history. The repository can be found
|
||||||
<a class="link-hi" href="https://go.enderman.ch/repository">here</a
|
<a :href="`${config.shortener}/repository`">here</a>.
|
||||||
>.
|
|
||||||
</li>
|
</li>
|
||||||
<li>I make videos for you to enjoy!</li>
|
<li>I make videos for you to enjoy!</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="md:basis-0 md:flex-grow-[1]">
|
||||||
<h5 class="alex">What I enjoy</h5>
|
<h6>What I enjoy</h6>
|
||||||
<ul class="list-style-type-enjoy">
|
<ul class="list-style-type-enjoy">
|
||||||
<li>Teaching</li>
|
<li>Teaching</li>
|
||||||
<li>Mathematics</li>
|
<li>Mathematics</li>
|
||||||
|
<li>Technology</li>
|
||||||
|
<li>Video Production</li>
|
||||||
<li>Philosophy</li>
|
<li>Philosophy</li>
|
||||||
<li>Geopolitics</li>
|
<li>Geopolitics</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="link-hi" href="https://go.enderman.ch/chess">Chess</a>
|
<a :href="`${config.shortener}/chess`">Chess</a>
|
||||||
</li>
|
</li>
|
||||||
<li>Solitude</li>
|
<li>Solitude</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -97,5 +106,3 @@ useHead({
|
|||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss"></style>
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineRouteRules } from '#imports'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const projects = [
|
||||||
|
{
|
||||||
|
name: 'MalwareWatch',
|
||||||
|
description:
|
||||||
|
'A website that tracks malware and ransomware attacks in real time.',
|
||||||
|
url: 'https://malwarewatch.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'You Are an Idiot! (dot cc)',
|
||||||
|
description: 'A recreation of the infamous "You Are an Idiot!" virus.',
|
||||||
|
url: 'https://youareanidiot.cc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'HURR-DURR (dot cc)',
|
||||||
|
description: 'A recreation of the infamous "HURR-DURR" virus.',
|
||||||
|
url: 'https://hurr-durr.cc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ClickSword',
|
||||||
|
description: 'A clicker game with a sword theme.',
|
||||||
|
url: 'https://clicksword.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Windows XP Keygen / UMSKT',
|
||||||
|
description: 'A key generator for Windows XP.',
|
||||||
|
url: `${config.shortener}/xpkeygen`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Enderman's Projects",
|
||||||
|
description:
|
||||||
|
"A display of my largest projects. Some of them aren't published on YouTube.",
|
||||||
|
image: `${config.url}/images/logo.png`,
|
||||||
|
url: `${config.url}/projects`,
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: meta.title,
|
||||||
|
description: meta.description,
|
||||||
|
ogTitle: meta.title,
|
||||||
|
ogDescription: meta.description,
|
||||||
|
ogImage: meta.image,
|
||||||
|
ogUrl: meta.url,
|
||||||
|
ogType: 'website',
|
||||||
|
twitterTitle: meta.title,
|
||||||
|
twitterDescription: meta.description,
|
||||||
|
twitterImage: meta.image,
|
||||||
|
twitterCard: 'summary',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Projects',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
sitemap: {
|
||||||
|
changefreq: 'monthly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page">
|
||||||
|
<h3>Projects</h3>
|
||||||
|
<Construction />
|
||||||
|
<p><strong>My current projects are:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, index) in projects" :key="index">
|
||||||
|
<a :href="item.url">
|
||||||
|
{{ item.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
The list above is not exhaustive and only includes the most recent and
|
||||||
|
largest projects I remembered while constructing this page.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { defineRouteRules } from '#imports'
|
||||||
|
|
||||||
|
const config = useAppConfig()
|
||||||
|
const fqdn = config.url.split('//')[1]
|
||||||
|
|
||||||
|
const socials = [
|
||||||
|
{
|
||||||
|
name: 'YouTube',
|
||||||
|
url: `${config.shortener}/youtube`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:youtube-icon',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Andrew',
|
||||||
|
url: `${config.shortener}/andrew`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:youtube-icon',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Twitch',
|
||||||
|
url: `${config.shortener}/twitch`,
|
||||||
|
icon: {
|
||||||
|
name: 'fa:twitch',
|
||||||
|
color: '#B68CFF',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TikTok',
|
||||||
|
url: `${config.shortener}/tiktok`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:tiktok-icon',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Twitter',
|
||||||
|
url: `${config.shortener}/twitter`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:twitter',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Telegram',
|
||||||
|
url: `${config.shortener}/telegram`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:telegram',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Discord',
|
||||||
|
url: `${config.shortener}/discord`,
|
||||||
|
icon: {
|
||||||
|
name: 'logos:discord-icon',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'GitHub',
|
||||||
|
url: `${config.shortener}/github`,
|
||||||
|
icon: {
|
||||||
|
name: 'fa:github',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Steam',
|
||||||
|
url: `${config.shortener}/steam`,
|
||||||
|
icon: {
|
||||||
|
name: 'fa:steam',
|
||||||
|
color: 'white',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: 'Enderman Online',
|
||||||
|
description:
|
||||||
|
'Discover me on your favorite social media and find ways to contact me here.',
|
||||||
|
image: `${config.url}/images/logo.png`,
|
||||||
|
url: `${config.url}/projects`,
|
||||||
|
}
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: meta.title,
|
||||||
|
description: meta.description,
|
||||||
|
ogTitle: meta.title,
|
||||||
|
ogDescription: meta.description,
|
||||||
|
ogImage: meta.image,
|
||||||
|
ogUrl: meta.url,
|
||||||
|
ogType: 'website',
|
||||||
|
twitterTitle: meta.title,
|
||||||
|
twitterDescription: meta.description,
|
||||||
|
twitterImage: meta.image,
|
||||||
|
twitterCard: 'summary',
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Socials',
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
defineRouteRules({
|
||||||
|
sitemap: {
|
||||||
|
changefreq: 'yearly',
|
||||||
|
priority: 0.7,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="page">
|
||||||
|
<h3>Online presence</h3>
|
||||||
|
<Construction />
|
||||||
|
<div class="flex flex-col md:flex-row gap-4">
|
||||||
|
<div class="md:basis-0 md:flex-grow-[1]">
|
||||||
|
<h5>Social media</h5>
|
||||||
|
<ul class="list-style-type-none p-0">
|
||||||
|
<li v-for="(page, index) in socials" :key="index">
|
||||||
|
<NuxtLink target="_blank" rel="noopener" :href="page.url">
|
||||||
|
<iconify-icon
|
||||||
|
:icon="page.icon.name"
|
||||||
|
:style="{ color: page.icon.color }"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
<span class="mx-2">{{ page.name }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="md:basis-0 md:flex-grow-[3]">
|
||||||
|
<h5>Contact me</h5>
|
||||||
|
<p>
|
||||||
|
Personal:
|
||||||
|
<EMail :address="`contact@${fqdn}`" subject="Hey Enderman!" /><br />
|
||||||
|
Manager: <EMail :address="`manager@${fqdn}`" /><br />
|
||||||
|
Abuse: <EMail :address="`abuse@${fqdn}`" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
local: {
|
||||||
|
getItem(item: string) {
|
||||||
|
if (import.meta.client) {
|
||||||
|
return localStorage.getItem(item)
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem(item: string, value: any) {
|
||||||
|
if (import.meta.client) {
|
||||||
|
return localStorage.setItem(item, value)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import aboutIcon from 'assets/images/icons/accent/info.avif'
|
||||||
|
import projectIcon from 'assets/images/icons/defrag.avif'
|
||||||
|
import socialIcon from 'assets/images/icons/user.avif'
|
||||||
|
import blogIcon from 'assets/images/icons/book.avif'
|
||||||
|
|
||||||
|
export const usePageStore = defineStore('page', () => {
|
||||||
|
const title: string = 'Enderman'
|
||||||
|
const description: string = 'official website'
|
||||||
|
const keywords: string =
|
||||||
|
'endermanch, enderman, developer, youtuber, filmmaker, artist, engineer'
|
||||||
|
|
||||||
|
const pages = ref([
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
path: '/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'About',
|
||||||
|
path: '/about',
|
||||||
|
icon: {
|
||||||
|
src: aboutIcon,
|
||||||
|
alt: 'Information',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Projects',
|
||||||
|
path: '/projects',
|
||||||
|
icon: {
|
||||||
|
src: projectIcon,
|
||||||
|
alt: 'Blocks',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Socials',
|
||||||
|
path: '/social',
|
||||||
|
icon: {
|
||||||
|
src: socialIcon,
|
||||||
|
alt: 'Users',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Blog',
|
||||||
|
path: '/blog',
|
||||||
|
icon: {
|
||||||
|
src: blogIcon,
|
||||||
|
alt: 'Book',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const reader = ref<boolean>(false)
|
||||||
|
const animate = ref<boolean>(false)
|
||||||
|
const dark = ref<boolean>(false)
|
||||||
|
|
||||||
|
function _autoFetchPages() {
|
||||||
|
while (pages.value.length) pages.value.pop()
|
||||||
|
|
||||||
|
useRouter()
|
||||||
|
.getRoutes()
|
||||||
|
.forEach((route) =>
|
||||||
|
pages.value.push({
|
||||||
|
name: route.path.slice(1)[0].toUpperCase() + route.path.slice(2),
|
||||||
|
path: route.path,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
keywords,
|
||||||
|
pages,
|
||||||
|
reader,
|
||||||
|
animate,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import packageJSON from './package.json'
|
||||||
|
|
||||||
|
interface TitleConfig {
|
||||||
|
full: string
|
||||||
|
short: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BuildConfig {
|
||||||
|
date?: string
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type config = {
|
||||||
|
url: string
|
||||||
|
shortener: string
|
||||||
|
name: string
|
||||||
|
title: TitleConfig
|
||||||
|
description: string
|
||||||
|
locale: string
|
||||||
|
build: BuildConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
url: 'https://enderman.ch',
|
||||||
|
shortener: 'https://go.enderman.ch',
|
||||||
|
name: packageJSON.name || 'app',
|
||||||
|
title: {
|
||||||
|
full: "Enderman's Website",
|
||||||
|
short: 'Enderman',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'A software engineer, a malware enthusiast and, most importantly, a weird tall creature. I have over 300K subscribers on YouTube and over 20K followers on Twitter.',
|
||||||
|
locale: 'en',
|
||||||
|
build: {
|
||||||
|
date: new Date().toISOString().split('T')[0],
|
||||||
|
version: packageJSON.version || '0.0.0',
|
||||||
|
},
|
||||||
|
} satisfies config as config
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineCollection, defineContentConfig } from '@nuxt/content'
|
||||||
|
import { asSitemapCollection } from '@nuxtjs/sitemap/content'
|
||||||
|
|
||||||
|
export default defineContentConfig({
|
||||||
|
collections: {
|
||||||
|
content: defineCollection(
|
||||||
|
asSitemapCollection({
|
||||||
|
type: 'page',
|
||||||
|
source: '**/*.md',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Hello World!
|
||||||
|
description: I created this post to test @nuxt/content.
|
||||||
|
authors: ['Enderman']
|
||||||
|
created: 2023-08-12T16:40:09Z
|
||||||
|
updated: 2024-02-18T20:31:54Z
|
||||||
|
draft: false
|
||||||
|
tags: ['hello', 'world']
|
||||||
|
---
|
||||||
|
|
||||||
|
# Hello!
|
||||||
|
|
||||||
|
This is an example piece of content.
|
||||||
|
|
||||||
|
Here's some code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
console.log('Hello, world!');
|
||||||
|
```
|
||||||
|
|
||||||
|
And here's a list:
|
||||||
|
* Item 1
|
||||||
|
* Item 2
|
||||||
|
* ```css
|
||||||
|
.example {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* `df -h`
|
||||||
|
|
||||||
|
And here's a link to [Nuxt.js](https://nuxtjs.org/).
|
||||||
|
|
||||||
|
And here's an image:
|
||||||
|

|
||||||
|
|
||||||
@@ -0,0 +1,714 @@
|
|||||||
|
---
|
||||||
|
title: The Windows Context Menu – Is It a Lost Cause?
|
||||||
|
description: A deep dive into the Windows context menu API and customization options it offers. Shell components for beginners and context menu cleanup.
|
||||||
|
authors: ['Enderman']
|
||||||
|
created: 2023-08-13T15:51:19Z
|
||||||
|
updated: 2024-02-18T21:32:12Z
|
||||||
|
draft: false
|
||||||
|
tags: ['windows', 'context menu', 'customization', 'registry', 'windows tweaks', 'shell', 'shell extensions', 'shell components', 'context menu handlers', 'windows 10', 'windows 11']
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Windows context menu is... poorly made.
|
||||||
|
Third-party software developers adding oil into this dumpster fire aren't helping with it.
|
||||||
|
**It's not their fault**, though,
|
||||||
|
as there is no common standard and almost no documentation for adding entries to the context menu.
|
||||||
|
|
||||||
|
It's safe to say that currently, there's no salvation for it.
|
||||||
|
At the very least, it's working and serving millions of users.
|
||||||
|
The new Windows 11 context menu shows Microsoft keeps pressing onward with their all-time classic:
|
||||||
|
writing new, less friendly, less extensible APIs from scratch,
|
||||||
|
simultaneously piling up layers upon layers of untested code atop the previous versions they can't entirely get rid of due to deep backwards compatibility problems that will inevitably arise.
|
||||||
|
|
||||||
|
I want to dedicate this article to _actually customizing_ the Windows 10
|
||||||
|
(incidentally, also 11 Legacy) context menu to your liking
|
||||||
|
instead of just complaining about its inner workings.
|
||||||
|
After reading this article completely, you should be able to bring your menu from this abomination...
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**...to this!**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Be wary — there's _plenty_ of registry digging involved. And the terrible part is...
|
||||||
|
|
||||||
|
**Forget any logic when dealing with registry keys.**
|
||||||
|
Nowadays, registry structure is the least of all concerns.
|
||||||
|
The idea of using registry as a coherent setting list has long since been abandoned,
|
||||||
|
and nobody seems to care about it, as the end user doesn't.
|
||||||
|
If someone does — they know what they're doing, and they will likely not complain,
|
||||||
|
because they have abused registry the same way at least once. 😉
|
||||||
|
|
||||||
|
Whilst there are layers of abstraction applied to the most Windows settings,
|
||||||
|
the context menu is, unfortunately, left aside.
|
||||||
|
And much to our dismay, its backend is also janky, terrible and tangled up.
|
||||||
|
The only fair question to reader from me is **«Do you really think it's worth your time?»**.
|
||||||
|
And if your answer is an astounding **«Hell yeah»**,
|
||||||
|
and you are as much of a perfectionist as I am, buckle up and prepare for several hours of pure joy.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
type: warning
|
||||||
|
title: Before we begin
|
||||||
|
---
|
||||||
|
Invoking `sfc /scannow` or `dism /restorehealth` will likely interfere with your context menu,
|
||||||
|
as Microsoft is peculiar — they had customization via direct registry manipulation in mind for more granular control,
|
||||||
|
but their tools **aren't adapted** to store so meticulously tweaked settings.
|
||||||
|
This isn't anything new — that hasn't changed ever since integrity checks were introduced in Windows XP.
|
||||||
|
_No need to worry, though!_ The aforementioned tools will keep working; however, some of your tweaks might be reset
|
||||||
|
upon execution.
|
||||||
|
::
|
||||||
|
|
||||||
|
## Context menu components
|
||||||
|
There are two main components the context menu is composed of:
|
||||||
|
|
||||||
|
* **Shell entries** — provided locally and managed by the registry;
|
||||||
|
* **Context menu handlers** — provided and managed by external software.
|
||||||
|
|
||||||
|
Neither of them is documented well, but
|
||||||
|
there is some developer documentation for handlers available [on Microsoft's website](https://go.enderman.ch/Fm4t3).
|
||||||
|
It explains how they work internally, but that doesn't really help our case.
|
||||||
|
As for the shell entries, I couldn't find anything.
|
||||||
|
|
||||||
|
Certainly a shame! Shell entries are so much more flexible and customizable.
|
||||||
|
In fact, **you don't get any customization** with context menu handlers whatsoever,
|
||||||
|
unless the developer was paid and bothered enough to read most of the documentation above.
|
||||||
|
|
||||||
|
To let you tweak context menu settings directly through their app,
|
||||||
|
they have to implement **both the handler and the endpoints** for handler settings,
|
||||||
|
which are commonly simple registry values.
|
||||||
|
Microsoft hasn't provided any convention for software developers to follow,
|
||||||
|
so you're at the mercy of them providing you with a friendly user interface to tweak the handler.
|
||||||
|
|
||||||
|
[7-Zip](https://go.enderman.ch/7-zip) serves as a neat example of great software design —
|
||||||
|
the developer here allows you to extensively tweak their handler.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[MobaXterm](https://go.enderman.ch/mobaxterm) also is an illustrative example of the case where you can customize
|
||||||
|
the shell integration from the application itself.
|
||||||
|
The developers of that software use shell entries instead of context menu handlers, which seems like an odd choice
|
||||||
|
for this case.
|
||||||
|
_It seems like someone was underpaid!_ 😊
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In conclusion, there are three types of applications in terms of friendliness towards context menu customization:
|
||||||
|
|
||||||
|
* **Apps using context menu handlers with customizable settings** — the friendliest;
|
||||||
|
* **Apps using shell entries** — customizable through registry hacks;
|
||||||
|
* **Apps using context menu handlers without settings** — the entries are uneditable.
|
||||||
|
|
||||||
|
The first case is the simplest and isn't worth talking about —
|
||||||
|
consult the documentation for the software in question to find all the necessary tweaks.
|
||||||
|
**The rest of the cases are discussed in this article.**
|
||||||
|
|
||||||
|
### Shell entries
|
||||||
|
For starters, all the tweaking happens inside `HKEY_CLASSES_ROOT` — alias for `HKEY_LOCAL_MACHINE\Software\Classes`.
|
||||||
|
That registry key contains all the data about shell components and file extension handlers.
|
||||||
|
|
||||||
|
For example, data under the `exefile` key will exclusively apply to files with the `.exe` extension.
|
||||||
|
A trusty old method [«companion» viruses](https://go.enderman.ch/Kvo3u) utilized back in the day to «accompany» their victims —
|
||||||
|
the executable (`.exe`/`.com`) files was a simple pass-through setup:
|
||||||
|
the virus executable was specified as the handler for all other executable files.
|
||||||
|
A belarusian virus from 2005, [Neshta.A](https://go.enderman.ch/2WeQ0), worked exactly this way and caused heaps of damage.
|
||||||
|
|
||||||
|
Since all the settings inside the `HKCR` key are divided into types,
|
||||||
|
editing the context menu for each file type and shell extension manually is unavoidable.
|
||||||
|
|
||||||
|
_Yeah..._
|
||||||
|
|
||||||
|
This is **abysmal**, and, unfortunately, there is no better approach.
|
||||||
|
While you're likely already reconsidering your choices, let me try to pull you back in and explain the basics.
|
||||||
|
That way you can at least tweak the menu for the extensions you work with the most
|
||||||
|
and bring the most noticeable context menu areas up to par...
|
||||||
|
|
||||||
|
There _are_ some minor generalizations present in the key.
|
||||||
|
For instance, there are wildcard entries for **every** file system object,
|
||||||
|
**every** image, **every** media file and so on.
|
||||||
|
Nonetheless, Microsoft has taken a horrible approach with the context menu from the beginning.
|
||||||
|
Even though such a disarray always annoyed me, I know full well there's no way for them to fix it now.
|
||||||
|
_That's how sometimes life is._
|
||||||
|
We'll tackle the generalized keys a bit later, let's start simple.
|
||||||
|
|
||||||
|
#### General structure
|
||||||
|
The following figure is what seems to be the general structure of an extension registry key:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The structure may vary for some extensions, but you can almost be sure you're in the right place if the key contains the `Shell` subkey.
|
||||||
|
Children of the `Shell` subkey are the «shell entries» — locally provided customizable entries of the context menu.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
type: info
|
||||||
|
title: Clearing up the confusion
|
||||||
|
---
|
||||||
|
The terminology may be a little confusing, since Microsoft hasn't ever offered a standard nomenclature for the
|
||||||
|
context menu. The article tosses plenty of loose terms around, so here's the nomenclature I have resorted to
|
||||||
|
for better understanding:
|
||||||
|
* **Shell** is the Windows Explorer;
|
||||||
|
* **Shell entry** is a singular entry within the context menu — the atomic component;
|
||||||
|
* **Shell integration** is the general footprint of software within the context menu and may consist of one or
|
||||||
|
more shell entries;
|
||||||
|
* **Shell extension** is a general term for a plugin for the Windows Explorer — the **context menu handler** is a
|
||||||
|
particular case of a shell extension;
|
||||||
|
* **Shell component** is a broad term for a component that helps the shell interact with the system — that includes
|
||||||
|
shell extensions, file extensions, protocols, etc.
|
||||||
|
|
||||||
|
If this doesn't show how big of a predicament Microsoft is in with the context menu, I don't know what does.
|
||||||
|
I tried my best to keep the technical language as clear as possible. 😒
|
||||||
|
::
|
||||||
|
|
||||||
|
#### Editing entries
|
||||||
|
Locally provided entries always available for a quick and dirty edit are stored under the `Shell` subkey.
|
||||||
|
Entries provided by software — the context menu handlers — are stored under the `ShellEx` subkey.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Since registry key names are case-insensitive by design in Windows, capitalization plays no role.
|
||||||
|
**The value names, however, are** — keep that in mind, as in some cases it might bite.
|
||||||
|
The context menu doesn't seem to care.
|
||||||
|
|
||||||
|
_Truly an amazing specification!_
|
||||||
|
|
||||||
|
|
||||||
|
#### Executing entries
|
||||||
|
A shell entry must contain the `command` subkey to execute commands.
|
||||||
|
The **default value** of the subkey controls execution on click of the shell entry and follows Batch syntax...
|
||||||
|
_to some extent_. You cannot have «extended» Batch logic in the entries,
|
||||||
|
which immeasurably disappointed me, so much so, that I wrote two neat utilities called
|
||||||
|
[quiet](https://go.enderman.ch/elevate) and [elevate](https://go.enderman.ch/elevate)<sup>sudo for Windows [anyone](https://go.enderman.ch/XuDXW)?</sup> for that specific purpose.
|
||||||
|
|
||||||
|
No matter how hard I tried, I could not find the exact specification of the incomplete Batch logic the default value uses.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
My suggestion is to treat the default `command` value as **raw command line** (e.g. Run box),
|
||||||
|
with an exception to the following two macros:
|
||||||
|
* `%1` — the absolute file path without quotes
|
||||||
|
When the shell entry is executed after a right-click on `C:\Users\bleh\p.txt`,
|
||||||
|
the `%1` in the context of the command line is equivalent to `C:\Users\bleh\p.txt`;
|
||||||
|
* `%V` — the absolute directory path without quotes.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
type: error
|
||||||
|
title: Inconsistency
|
||||||
|
---
|
||||||
|
Keep in mind that the `%1` macro is the Batch version of `argv[1]` — the first item in the C argument vector.
|
||||||
|
When a context menu is invoked from within, say, a library, the argument vector stops making sense
|
||||||
|
because a library is a Windows Shell feature, meaning it's sophisticated. Opening a library internally
|
||||||
|
**requires a [different API](https://go.enderman.ch/KuKR4)** to opening a folder, or even executing a file.
|
||||||
|
In such cases, the `%1` macro **does not exist** in the context of the command line. This is also likely
|
||||||
|
the reason why libraries are in a separate category to folders in terms of shell entries.
|
||||||
|
The directory macro `%V` also sometimes falls flat depending on the shell entry it's being invoked from.
|
||||||
|
**Brilliant consistency!**
|
||||||
|
::
|
||||||
|
|
||||||
|
On rare occasions, the default value is left unset, with `DelegateExecute` alongside it containing a GUID.
|
||||||
|
When that's the case, the command handler is implemented through the `IExecuteCommand` COM interface,
|
||||||
|
and hence cannot be customized by normal means.
|
||||||
|
|
||||||
|
**Why is that a thing?** Ask me a simpler question. It could be a compatibility layer for C, but your guess is as good as mine.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Adding entries
|
||||||
|
Making new entries isn't as bad as managing existing ones,
|
||||||
|
so I managed to scrabble it into a sort of algorithm, if that makes sense:
|
||||||
|
|
||||||
|
1. Locate the target extension.
|
||||||
|
2. **Careful!** Some extensions may be buried within other extensions.
|
||||||
|
As an example, the [Folder Background](#folder-background) extension is located under the [Folder](#folder) extension `Directory` key in the `Background` subkey.
|
||||||
|
3. Navigate to the `shell` key.
|
||||||
|
If there is none, and you're _confident_ there should be,
|
||||||
|
try creating one — **chances are high** it isn't going to break Windows.<sub>(no, seriously)</sub>
|
||||||
|
4. Create a new key, the name **must be alphanumeric**. The name is case-insensitive.
|
||||||
|
5. Create the `command` subkey.
|
||||||
|
6. Specify the command line you want to execute on click in the **default value**.
|
||||||
|
7. The entry should appear in the context menu. If it does not, restart Explorer.
|
||||||
|
If it still doesn't appear, you probably screwed up the procedure — return to step 1.
|
||||||
|
If you verified everything for sure, then the extension is simply **not registered** in the shell.
|
||||||
|
|
||||||
|
#### Nesting entries
|
||||||
|
You can nest entries too!
|
||||||
|
1. Repeat the first 4 steps from [Adding entries](#adding-entries).
|
||||||
|
Don't add the `command` key just yet.
|
||||||
|
2. Create another `shell` key.
|
||||||
|
3. Inside it, create a subkey **called anything else, but** `command`.
|
||||||
|
4. Working within the subkey, repeat steps 5–7.
|
||||||
|
5. Navigate back to the root shell entry
|
||||||
|
6. Create an empty `REG_SZ` value called `SubCommands`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It's possible to have an entry include a single nested entry as well.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To control the parent context menu entry, use the outer `Shell` subkey.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
To control its children, use the inner `Shell` subkeys.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Customizing entries further
|
||||||
|
There are a plethora of options when it comes to customizing the shell entries.
|
||||||
|
_Too bad they're all undocumented._
|
||||||
|
By default, the context menu items **borrow their names** from the respective shell entries they're owned by.
|
||||||
|
|
||||||
|
##### Name
|
||||||
|
To override the default name,
|
||||||
|
you can either use the default value or specify a `REG_SZ` value called `MUIVerb` inside the respective `shell` key
|
||||||
|
for the shell entry.
|
||||||
|
MUIVerb takes precedence over the default value.
|
||||||
|
|
||||||
|
When it comes to the name, the sky is the limit.
|
||||||
|
_Actually, Unicode is the limit._
|
||||||
|
Either way, put in anything you want; **it's going to work**.
|
||||||
|
|
||||||
|
##### Icon
|
||||||
|
To enable the icon for your shell entry,
|
||||||
|
you should use the `REG_SZ` value called `Icon` inside the respective `shell` key for the shell entry.
|
||||||
|
|
||||||
|
It uses that unmistakably distinct and clumsy **WinAPI resource syntax** to specify a direct link to the icon within
|
||||||
|
a file containing resources.
|
||||||
|
|
||||||
|
```csv
|
||||||
|
PATH_TO_FILE_WITH_ICON_RESOURCES,INDEX
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to select the first icon (in that case, the `INDEX` part is 0),
|
||||||
|
the `INDEX` part can be omitted completely:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
PATH_TO_FILE_WITH_ICON_RESOURCES
|
||||||
|
```
|
||||||
|
|
||||||
|
The following expressions are completely valid and are going to work:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
"C:\Windows\system32\cmd.exe"
|
||||||
|
C:\Windows\regedit.exe
|
||||||
|
"C:\Windows\explorer.exe",0
|
||||||
|
C:\Windows\system32\cmd.exe,0
|
||||||
|
cmd.exe
|
||||||
|
shell32.dll,3
|
||||||
|
SHELL32.DLL,10
|
||||||
|
"C:\Windows\system32\SHELL32.DLL",9
|
||||||
|
```
|
||||||
|
|
||||||
|
You can omit the quotes and absolute paths depending on the `%PATH%` variable that unholy concoction of an API uses.
|
||||||
|
**Spoiler:** they don't match against the system `%PATH%`.
|
||||||
|
Only God knows what paths it's able to parse by default.
|
||||||
|
`C:\Windows` and `C:\Windows\system32` always work for sure, though.
|
||||||
|
|
||||||
|
It's still highly recommended to supply the absolute path, the `%PATH%` variables of the shell icon picker and the
|
||||||
|
shell entry **do not seem to correspond**.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
type: idea
|
||||||
|
title: Tip
|
||||||
|
---
|
||||||
|
You can use the Explorer folder icon picker to acquire the icon DLL and visually calculate the offset
|
||||||
|
of the desired icon.
|
||||||
|
For instance, the one I selected on the screenshot has an index of `5`.
|
||||||
|
The indexing begins with 0 and **ascends in columns**.
|
||||||
|
::
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
It's also possible to pull resource strings from files containing string tables.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### Separators
|
||||||
|
Another integral part of the context menu, which is hardly documented by anyone.
|
||||||
|
Managing them is also rather quirky and takes getting used to.
|
||||||
|
Separators in shell entries are relative to the item they're being enabled for and **can overlap** without any issue.
|
||||||
|
|
||||||
|
Use `SeparatorBefore` and `SeparatorAfter` empty `REG_SZ` values
|
||||||
|
to add a separator before or after the shell entry being edited, respectively.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### Appear in the Shift+Click context menu only
|
||||||
|
It's possible to «hide» the shell entry from an ordinary context menu right-click.
|
||||||
|
Such a shell entry is called «extended».
|
||||||
|
|
||||||
|
Enable that functionality by creating an empty `REG_SZ` value called `Extended` inside the `shell` key for the
|
||||||
|
respective shell entry.
|
||||||
|
|
||||||
|
##### Hide in safe mode
|
||||||
|
To disable the shell entry in safe mode,
|
||||||
|
create an empty `REG_SZ` value called `HideInSafeMode` inside the `shell` key for the respective shell entry.
|
||||||
|
|
||||||
|
##### Never default
|
||||||
|
The bold context menu item shown at the top is considered to be the default action for the file type.
|
||||||
|
An example of a default shell entry would be «Open» for folders.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
There can be no default entry at all, and that's perfectly fine.
|
||||||
|
|
||||||
|
To disable the shell entry from becoming default,
|
||||||
|
add an empty `REG_SZ` value called `NeverDefault` inside the `shell` key for the respective shell entry.
|
||||||
|
|
||||||
|
##### Pass to context menu handler
|
||||||
|
If there is a context handler available for invocation on shell entry click, it's possible to specify a so-called Verb Handler.
|
||||||
|
|
||||||
|
Create a new `REG_SZ` value `VerbHandler` inside the `shell` key for the respective shell entry
|
||||||
|
and specify the GUID of the context menu handler in the value.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### No working directory
|
||||||
|
Disables the working directory from being added to the environmental path in the context of the application being run.
|
||||||
|
|
||||||
|
To disable pass-through of the working directory to the application,
|
||||||
|
create an empty `REG_SZ` value called `NoWorkingDirectory` inside the `shell` key for the respective shell entry.
|
||||||
|
|
||||||
|
##### Other options
|
||||||
|
Some options, though might sound obvious, have me scratching the back of my head.
|
||||||
|
Please tag me on [Twitter](https://go.enderman.ch/twitter) or [send a mail](mailto:[email protected]) my way if you
|
||||||
|
have the knowledge.
|
||||||
|
|
||||||
|
**A few examples:**
|
||||||
|
* `CanonicalName`;
|
||||||
|
* `CommandStateHandler`;
|
||||||
|
* `CommandStateSync`;
|
||||||
|
* `Description`;
|
||||||
|
* `VerbName`.
|
||||||
|
|
||||||
|
#### Deleting entries
|
||||||
|
Even simpler — it's the opposite of adding one.
|
||||||
|
|
||||||
|
1. Locate the target extension.
|
||||||
|
2. Navigate to the `shell` subkey.
|
||||||
|
3. Find the shell entry to delete. The default value of the `command` key will usually contain the
|
||||||
|
executable name, it's best to verify the target application before deletion.
|
||||||
|
4. Delete the key.
|
||||||
|
|
||||||
|
**Multiple shell entries** can sometimes correspond to the **same item** in the context menu,
|
||||||
|
which might result in the item remaining in place despite the deletion.
|
||||||
|
|
||||||
|
#### Sorting entries
|
||||||
|
That's easily the most hilarious procedure of them all. I even [tweeted](https://go.enderman.ch/3hcO9) about it.
|
||||||
|
|
||||||
|
The only way to sort the context menu items is...
|
||||||
|
**by managing the alphabetical order of their respective shell entries in the registry**.
|
||||||
|
Seriously, I am not kidding.
|
||||||
|
Let the screenshots speak for themselves.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
_Yeah... I'd rather leave all the obscene comments aside._
|
||||||
|
|
||||||
|
##### Position override
|
||||||
|
Another silly feature with a limited number of use cases
|
||||||
|
that's likely been created due to all prior mess Microsoft has created.
|
||||||
|
It's possible to manually override the position of a context menu item to be...
|
||||||
|
either at the very top, or the very bottom.
|
||||||
|
_Still doesn't top off the alphabetical sorting between the registry peers..._
|
||||||
|
|
||||||
|
In the `shell` subkey under the respective shell entry, create a `REG_SZ` value called `Position`.
|
||||||
|
To enforce the context menu item to appear at the very top,
|
||||||
|
insert `Top` into the value data, otherwise insert `Bottom`.
|
||||||
|
|
||||||
|
Sometimes directly enforcing the position happens to be necessary.
|
||||||
|
For instance, Microsoft dumps the usual sorting strategy at the very bottom of the context menu,
|
||||||
|
where the display and personalization settings reside.
|
||||||
|
They simply set the `Bottom` position for each of the entries.
|
||||||
|
|
||||||
|
**What happens when there are multiple shell entries enforced to be in a set position?**
|
||||||
|
The alphabetical sorting makes a comeback,
|
||||||
|
it occurs between the shell entries with overridden positions just the same way.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Context menu handlers
|
||||||
|
Compared to shell entries, there isn't much to be tweaked via registry in context menu handlers.
|
||||||
|
Their general location is the `ContextMenuHandlers` subkey under the `shellex` key in the respective shell entry.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Most of the time, the only piece of data they contain is a lonely GUID of the respective handler.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Removing handlers
|
||||||
|
|
||||||
|
A context menu handler always directly corresponds to some child under the `ContextMenuHandlers` key.
|
||||||
|
Therefore, all that has to be done to remove a handler is removal of the **respective child key**.
|
||||||
|
|
||||||
|
I strongly suggest writing **the GUID** and **the location** of the removed entry down somewhere,
|
||||||
|
so that there is a way to roll back any time later.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Adding handlers
|
||||||
|
|
||||||
|
Adding a custom handler is complicated.
|
||||||
|
It's not as trivial as adding a shell entry —
|
||||||
|
it requires implementing a handler interface using a language
|
||||||
|
supporting Windows API or an abstraction layer on top of it.
|
||||||
|
C# would be a decent choice precisely for this task.
|
||||||
|
|
||||||
|
However, it's just a waste of time to implement a handler for a single shell entry,
|
||||||
|
unless there's software to go along with it.
|
||||||
|
The idea behind context menu handlers is to **make native application experience nicer** for Windows users.
|
||||||
|
|
||||||
|
In other words, Microsoft failed to implement an accessible API for the context menu,
|
||||||
|
so they did what they do best — shifted the responsibility to the developers writing third-party software.
|
||||||
|
|
||||||
|
This is how a partial implementation of the `IContextMenu` interface in C++ looks like:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
STDMETHODIMP CShellExtension::InvokeCommand(LPCMINVOKECOMMANDINFO lpcmi) {
|
||||||
|
BOOL fEx = FALSE;
|
||||||
|
BOOL fUnicode = FALSE;
|
||||||
|
|
||||||
|
if(lpcmi->cbSize == sizeof(CMINVOKECOMMANDINFOEX)) {
|
||||||
|
fEx = TRUE;
|
||||||
|
if (lpcmi->fMask & CMIC_MASK_UNICODE) fUnicode = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fUnicode && HIWORD(lpcmi->lpVerb)) {
|
||||||
|
if (StrCmpIA(lpcmi->lpVerb, m_pszVerb)) return E_FAIL;
|
||||||
|
}
|
||||||
|
else if (fUnicode && HIWORD(((CMINVOKECOMMANDINFOEX *) lpcmi)->lpVerbW)) {
|
||||||
|
if (StrCmpIW(((CMINVOKECOMMANDINFOEX *)lpcmi)->lpVerbW, m_pwszVerb)) return E_FAIL;
|
||||||
|
}
|
||||||
|
else if (LOWORD(lpcmi->lpVerb) != IDM_DISPLAY) {
|
||||||
|
return E_FAIL;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
MessageBox(
|
||||||
|
lpcmi->hwnd,
|
||||||
|
"The File Name",
|
||||||
|
"File Name",
|
||||||
|
MB_OK | MB_ICONINFORMATION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `InvokeCommand()` method is called when a user clicks a context menu item.
|
||||||
|
I've taken that example from [Microsoft](https://go.enderman.ch/TArYX).
|
||||||
|
|
||||||
|
That whole thing isn't too bad — the logic is far more robust and understandable
|
||||||
|
compared to its surface-level registry counterpart, but it certainly requires getting used to WinAPI.
|
||||||
|
|
||||||
|
**By definition, it's a requirement to implement interfaces entirely.**
|
||||||
|
However, since we're dealing with Microsoft,
|
||||||
|
the «interface» in question is actually an _abstract class_ and can be implemented partially.
|
||||||
|
|
||||||
|
The `IContextMenu` interface has three methods:
|
||||||
|
* `GetCommandString()` — retrieves the canonical name of the command (optional);
|
||||||
|
* `InvokeCommand()` — handles the command (required);
|
||||||
|
* `QueryContextMenu()` — adds items to the context menu (required).
|
||||||
|
|
||||||
|
The `IContextMenu` interface is the most common one, but there are others,
|
||||||
|
such as `IContextMenu2` and `IContextMenu3`.
|
||||||
|
Essentially, this is how the context menu handlers work inside.
|
||||||
|
|
||||||
|
While all of this is just a brief overview of the API,
|
||||||
|
you can learn everything about them and more in the [Shell Developer's Guide](https://go.enderman.ch/W50xJ).
|
||||||
|
|
||||||
|
## Dealing with locked keys
|
||||||
|
If the registry key is locked, the user you're logged in as is excluded from the ACL.
|
||||||
|
Commonly, when the key is locked, it's owned by `TrustedInstaller`.
|
||||||
|
When that's the case, it's necessary to reclaim ownership of the key to allow making edits to the ACL.
|
||||||
|
There are plenty of ways to do that, I will list two common ones.
|
||||||
|
|
||||||
|
### Using external tools
|
||||||
|
The idea here is to run `regedit` — the registry editor as the `TrustedInstaller` user directly.
|
||||||
|
You can think of `TrustedInstaller` as the Windows equivalent of `root` in Unix-like systems —
|
||||||
|
the superuser.
|
||||||
|
|
||||||
|
While Windows generally has more granular control over permissions,
|
||||||
|
and the idea of having a superuser — **the ultimate authority of the system** — is less prominent and unannounced,
|
||||||
|
`TrustedInstaller` is the closest object to a superuser Windows has.
|
||||||
|
|
||||||
|
For those wondering, I've already explained [here](https://go.enderman.ch/o2KGt) why
|
||||||
|
«sudo for Windows» makes no sense.
|
||||||
|
|
||||||
|
##### Process Hacker <sub>[download](https://go.enderman.ch/process-hacker)</sub>
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### Winaero Tweaker <sub>[download](https://go.enderman.ch/winaero)</sub>
|
||||||
|

|
||||||
|
|
||||||
|
[Sergey](https://go.enderman.ch/Dr0iS) makes an entirely valid point in his Winaero Tweaker:
|
||||||
|
|
||||||
|
> **Warning!!!**
|
||||||
|
>
|
||||||
|
> Running programs under TrustedInstaller privileges, especially a file manager app or Regedit,exe can be
|
||||||
|
> VERY risky and harmful for your OS. It is like a God mode in a first-person shooter game, where nothing can
|
||||||
|
> stop your actions, so if you execute a malware infected file as TrustedInstaller, it can cause damage to the
|
||||||
|
> Windows operating system. Use this mode only if you perfectly understand what you are doing and if there is
|
||||||
|
> no simpler way than running it as Trusted Installer.
|
||||||
|
|
||||||
|
This is exactly the reason I resort to setting permissions manually.
|
||||||
|
It's a bit more time-consuming, but it's safer.
|
||||||
|
|
||||||
|
### Setting permissions manually
|
||||||
|
The trusty old way to do it is to manually set permissions for the key.
|
||||||
|
Once you get the hang of it, it really isn't that difficult.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### General procedure
|
||||||
|
1. Navigate to the key you want to claim ownership of;
|
||||||
|
2. Open the «Properties» window;
|
||||||
|
3. Head over to the «Advanced» tab;
|
||||||
|
4. The current owner is shown at the top of the «Advanced Security Settings» window. Click «Change»;
|
||||||
|
5. Type out your username in the text area with the «Enter the object name to select (examples)» label;
|
||||||
|
6. Click «OK»;
|
||||||
|
7. Tick the «Replace owner on subcontainers and objects» checkbox;
|
||||||
|
8. Tick the «Replace all child object permission entries» checkbox;
|
||||||
|
9. Click «OK»;
|
||||||
|
10. Pick `Administrators (PC_NAME\Administrators)` in the group selector and then tick «Full Control» checkbox
|
||||||
|
in the «Permissions» window at the bottom;
|
||||||
|
11. You're done! Click «OK» and dismiss any warnings.
|
||||||
|
|
||||||
|
## Common registry paths
|
||||||
|
Here is a list of the most common registry paths.
|
||||||
|
There are as many «sections» as there are extensions, but these are the most common ones.
|
||||||
|
They should cover most of your context menu.
|
||||||
|
|
||||||
|
#### Desktop Background — bottom rows
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\DesktopBackground`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Folder Background
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\Directory\Background`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Folder
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\Directory`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Folder Item
|
||||||
|
Extends [Folder](#folder).
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\Folder`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Drive
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\Drive`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Library Folder
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\LibraryFolder`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Library Background
|
||||||
|
Inherits from [Folder Background](#folder-background).
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\LibraryFolder\background`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Despite the inheritance, it cannot parse the `%V` macro, which results in the following error.
|
||||||
|
Blame Microsoft for being as consistent as ever.
|
||||||
|
To my knowledge, there is no way to fix that.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### All Files
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\*`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### All Filesystem Objects
|
||||||
|
Different from [All Files](#all-files) — it's a further generalization that includes system drives,
|
||||||
|
symbolic links and other objects.
|
||||||
|
`Computer\HKEY_CLASSES_ROOT\AllFilesystemObjects`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### File type associations
|
||||||
|
Text: `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\text`
|
||||||
|
Images: `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\image`
|
||||||
|
Documents: `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\document`
|
||||||
|
Audio: `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\audio`
|
||||||
|
Video: `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\video`
|
||||||
|
|
||||||
|
## Cleaning up the context menu
|
||||||
|
The least fan-favorite default entries can be removed using Winaero Tweaker.
|
||||||
|
I suggest using it to make the whole cleanup slightly more bearable.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Some extra common areas
|
||||||
|
The following are fairly common areas Winaero Tweaker doesn't cover.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
type: idea
|
||||||
|
title: Tip
|
||||||
|
---
|
||||||
|
As a rule of thumb, whenever you're dealing with existing shell entries, I _really_ suggest
|
||||||
|
**searching for a way to manage them online** before resorting to any measures I elaborated on in this article.
|
||||||
|
Now if you can't find anything useful online, then anything works.
|
||||||
|
::
|
||||||
|
|
||||||
|
#### Remove «Open PowerShell window here»
|
||||||
|
1. Navigate to `Computer\HKEY_CLASSES_ROOT\Directory\shell\Powershell`;
|
||||||
|
2. [Unlock the key](#dealing-with-locked-keys);
|
||||||
|
3. Create a new `REG_SZ` value called `ProgrammaticAccessOnly` and leave it empty;
|
||||||
|
4. Navigate to `Computer\HKEY_CLASSES_ROOT\Directory\Background\shell\Powershell`;
|
||||||
|
5. [Unlock the key](#dealing-with-locked-keys);
|
||||||
|
6. Create a new `REG_SZ` value called `ProgrammaticAccessOnly` and leave it empty.
|
||||||
|
|
||||||
|
#### Remove «Open Linux shell here»
|
||||||
|
1. Navigate to `Computer\HKEY_CLASSES_ROOT\Directory\shell\WSL`;
|
||||||
|
2. [Unlock the key](#dealing-with-locked-keys);
|
||||||
|
3. Remove the key;
|
||||||
|
4. Navigate to `Computer\HKEY_CLASSES_ROOT\Directory\Background\shell\WSL`;
|
||||||
|
5. [Unlock the key](#dealing-with-locked-keys);
|
||||||
|
6. Create a new `REG_SZ` value called `ProgrammaticAccessOnly` and leave it empty.
|
||||||
|
|
||||||
|
#### Edit the «Edit» text context menu command
|
||||||
|
1. Navigate to `Computer\HKEY_CLASSES_ROOT\SystemFileAssociations\text\shell\edit`;
|
||||||
|
2. Replace all mentions of the previous editor in default values with **Notepad++**
|
||||||
|
or any other text editor you prefer.
|
||||||
|
|
||||||
|
## Bottom line
|
||||||
|
The context menu is a mess, and it's not going to change.
|
||||||
|
It's a part of Windows that's been around for a long time and has been left behind by Microsoft.
|
||||||
|
Shell entries are like differential equations — there's never a general solution.
|
||||||
|
They're a pain to learn, but the acquired skill is worth it.
|
||||||
|
|
||||||
|
No matter how terrible the context menu API is in Windows, there's still a way to customize it to your liking.
|
||||||
|
Hopefully you learned something new from this article
|
||||||
|
and are now able to make your context menu **a bit** more bearable.
|
||||||
|
|
||||||
|
**Spotted a mistake?**
|
||||||
|
Want to add important details I missed or share extra knowledge on the subject?
|
||||||
|
Tag me on [Twitter](https://go.enderman.ch/twitter) or [e-mail me](mailto:[email protected])!
|
||||||
|
All contributions welcome. 😉
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
title: The Hidden Features of the Windows OOBE
|
||||||
|
description: The Windows 11 OOBE is a mess, but it's excellent for bulk deployment and has some hidden features and customization options!
|
||||||
|
authors: ['Enderman']
|
||||||
|
created: 2024-03-01T23:30:10Z
|
||||||
|
updated: 2024-03-02T15:10:58Z
|
||||||
|
draft: false
|
||||||
|
tags: ['windows', 'oobe', 'deployment', 'know how', 'secret features', 'protocols', 'features', 'windows 10', 'windows 11']
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Windows 11 OOBE is a mess...
|
||||||
|
The OOBE (Out-of-Box Experience) is the first thing a user sees when they boot up a new Windows device.
|
||||||
|
It's the first impression of the operating system, and it's important to get it right.
|
||||||
|
At first glance, it seems to be a thing Microsoft surprisingly _has_ gotten right.
|
||||||
|
However, once you take a look under the hood, you find a mess of legacy code, hidden features and undocumented APIs...
|
||||||
|
and suddenly, you take it all back.
|
||||||
|
|
||||||
|
In this article, I aim to take a deep dive into that mess and explore some hidden features and
|
||||||
|
customization options it offers.
|
||||||
|
|
||||||
|
::card
|
||||||
|
---
|
||||||
|
icon: warning
|
||||||
|
title: Unfinished article
|
||||||
|
---
|
||||||
|
This post is a **work in progress**, please check back later for more content!
|
||||||
|
::
|
||||||
|
|
||||||
|
### The OOBE protocol
|
||||||
|
|
||||||
|
The OOBE is a web application, and the `ms-cxh://` protocol is a custom handler that defines a couple of shortcuts to
|
||||||
|
various pages, seemingly, for internal use.
|
||||||
|
|
||||||
|
The list of available protocol strings can be retrieved by scanning Windows binaries for the `ms-cxh://` occurrences.
|
||||||
|
Luckily, Hexacorn did all the heavy lifting for us and shared
|
||||||
|
his findings in [his blog post](https://go.enderman.ch/18n6n).
|
||||||
|
|
||||||
|
The protocol API is **undocumented** by Microsoft, so it's not guaranteed to work in the future.
|
||||||
|
In fact, it has already been partially removed or deprecated in Windows 11 without any notice.
|
||||||
|
|
||||||
|
Microsoft used acronyms in protocol strings, and some of them are quite cryptic, but there's nothing we can't decode.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#### Out-of-Box Experience
|
||||||
|
FRX stands for «**F**irst **R**un e**X**perience»;
|
||||||
|
RDX stands for «**R**etail **D**emo e**X**perience».
|
||||||
|
|
||||||
|
* `ms-cxh://FRX/AAD`
|
||||||
|
* `ms-cxh://FRX/INCLUSIVE`
|
||||||
|
* `ms-cxh://FRX/INCLUSIVE?start=OobeProvisioningStatus`
|
||||||
|
* `ms-cxh://FRX/TEAMEDITION`
|
||||||
|
* `ms-cxh://FRXRDXINCLUSIVE`
|
||||||
|
|
||||||
|
#### Microsoft Azure
|
||||||
|
The AAD acronym is commonly used and refers to «**A**zure **A**ctive **D**irectory»;
|
||||||
|
SSPR stands for «**S**elf-**S**ervice **P**assword **R**eset».
|
||||||
|
|
||||||
|
* `ms-cxh://AADPINRESETAUTH`
|
||||||
|
* `ms-cxh://AADSSPR`
|
||||||
|
* `ms-cxh://AADWEBAUTH`
|
||||||
|
|
||||||
|
#### Modern settings
|
||||||
|
The MOSET acronym most likely refers to «**Mo**dern **set**tings»;
|
||||||
|
MAM stands for «**M**obile **A**pplication **M**anagement».
|
||||||
|
MSA stands for «**M**icro**s**oft **A**ccount».
|
||||||
|
|
||||||
|
* `ms-cxh://MOSET/AADLOCAL`
|
||||||
|
* `ms-cxh://MOSET/CONNECTTOWORK`
|
||||||
|
* `ms-cxh://mosetmamconnecttowork?mode=mdm&username=%s&servername=%s`
|
||||||
|
* `ms-cxh://mosetmdmconnecttowork`
|
||||||
|
* `ms-cxh://MOSETMSA`
|
||||||
|
* `ms-cxh://MOSETMSALOCAL`
|
||||||
|
|
||||||
|
#### Microsoft account
|
||||||
|
CFL probably stands for «**C**hange **F**irst **L**ogon»?
|
||||||
|
|
||||||
|
* `ms-cxh://MSACFLPINRESET`
|
||||||
|
* `ms-cxh://MSACFLPINRESETSIGNIN`
|
||||||
|
* `ms-cxh://MSACXSIGNINAUTHONLY`
|
||||||
|
* `ms-cxh://MSACXSIGNINPINADD`
|
||||||
|
* `ms-cxh://MSACXSIGNINPINRESET`
|
||||||
|
* `ms-cxh://MSAPINENROLL`
|
||||||
|
* `ms-cxh://MSAPINRESET`
|
||||||
|
* `ms-cxh://MSARDX`
|
||||||
|
* `ms-cxh://MSASSPR`
|
||||||
|
|
||||||
|
#### Windows Hello for Microsoft Intune
|
||||||
|
NTH stands for «i**NT**une **H**ello»;
|
||||||
|
NGC stands for «**N**ext **G**eneration **C**redential»;
|
||||||
|
ENT stands for «**Ent**erprise»;
|
||||||
|
MDM stands for «**M**obile **D**evice **M**anagement».
|
||||||
|
|
||||||
|
* `ms-cxh://NTH`
|
||||||
|
* `ms-cxh://NTH/AADRECOVERY`
|
||||||
|
* `ms-cxh://NTHAADNGCFIXME`
|
||||||
|
* `ms-cxh://NTHAADNGCONLY`
|
||||||
|
* `ms-cxh://NTHAADNGCRESET`
|
||||||
|
* `ms-cxh://NTHAADNGCRESETDESTRUCTIVE`
|
||||||
|
* `ms-cxh://NTHAADNGCRESETNONDESTRUCTIVE`
|
||||||
|
* `ms-cxh://NTHAADORMDM?ngc=enabled`
|
||||||
|
* `ms-cxh://NTHENTNGCFIXME`
|
||||||
|
* `ms-cxh://NTHENTNGCONLY`
|
||||||
|
* `ms-cxh://NTHENTNGCRESET`
|
||||||
|
* `ms-cxh://NTHENTNGCRESETDESTRUCTIVE`
|
||||||
|
* `ms-cxh://NTHENTORMDM`
|
||||||
|
* `ms-cxh://NTHENTORMDM?ngc=enabled`
|
||||||
|
* `ms-cxh://NTHNGCUPSELL`
|
||||||
|
* `ms-cxh://NTHPRIVACY`
|
||||||
|
* `ms-cxh://RDXRACSKUINCLUSIVE`
|
||||||
|
|
||||||
|
#### Second-chance OOBE
|
||||||
|

|
||||||
|
|
||||||
|
* `ms-cxh://SCOOBE`
|
||||||
|
* `ms-cxh://SCOOBE%ws`
|
||||||
|
* `ms-cxh://SCOOBE/UPGRADE`
|
||||||
|
|
||||||
|
#### Cloud settings
|
||||||
|
* `ms-cxh://SETADDLOCALONLY`
|
||||||
|
* `ms-cxh://SETADDNEWUSER`
|
||||||
|
* `ms-cxh://SETCHANGEPWD`
|
||||||
|
* `ms-cxh://SETPHONEPAIRING`
|
||||||
|
* `ms-cxh://SETPHONEPAIRING?scenarioId=SwiftKeyCloudClipboard`
|
||||||
|
* `ms-cxh://setsqsalocalonly`
|
||||||
|
|
||||||
|
#### Miscellaneous
|
||||||
|
* `ms-cxh://TSET/ADDFAMILY`
|
||||||
|
* `ms-cxh://WLT`
|
||||||
|
* `ms-cxh://WLTUC`
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt({
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': 'warn',
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/max-attributes-per-line': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
singleline: 3,
|
||||||
|
multiline: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'space-in-parens': 'off',
|
||||||
|
'computed-property-spacing': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import config from './config'
|
||||||
|
|
||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: config.locale || 'en',
|
||||||
|
},
|
||||||
|
link: [
|
||||||
|
{
|
||||||
|
rel: 'icon',
|
||||||
|
type: 'image/x-icon',
|
||||||
|
href: '/favicon.ico',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
charset: 'utf-8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'viewport',
|
||||||
|
content: 'width=device-width, initial-scale=1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'http-equiv': 'X-UA-Compatible',
|
||||||
|
content: 'ie=edge',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
|
rootId: 'ender-app',
|
||||||
|
},
|
||||||
|
|
||||||
|
colorMode: {
|
||||||
|
preference: 'system',
|
||||||
|
fallback: 'dark',
|
||||||
|
classPrefix: '',
|
||||||
|
classSuffix: '',
|
||||||
|
componentName: 'NuxtTheme',
|
||||||
|
storageKey: 'ecmatheme',
|
||||||
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2025-01-26',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
dirs: [
|
||||||
|
{
|
||||||
|
path: '~/components',
|
||||||
|
pathPrefix: false,
|
||||||
|
extensions: ['.vue'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: ['remark-reading-time'],
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
theme: {
|
||||||
|
default: 'github-dark',
|
||||||
|
light: 'github-light',
|
||||||
|
sepia: 'monokai',
|
||||||
|
},
|
||||||
|
langs: [
|
||||||
|
'shell',
|
||||||
|
'batch',
|
||||||
|
'vb',
|
||||||
|
'ini',
|
||||||
|
'asm',
|
||||||
|
'c',
|
||||||
|
'cpp',
|
||||||
|
'java',
|
||||||
|
'python',
|
||||||
|
'csv',
|
||||||
|
'xml',
|
||||||
|
'json',
|
||||||
|
'yaml',
|
||||||
|
'html',
|
||||||
|
'css',
|
||||||
|
'sass',
|
||||||
|
'php',
|
||||||
|
'js',
|
||||||
|
'ts',
|
||||||
|
'vue',
|
||||||
|
'md',
|
||||||
|
'mdc',
|
||||||
|
'pascal',
|
||||||
|
'lisp',
|
||||||
|
'sql',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/styles/main.scss'],
|
||||||
|
|
||||||
|
devtools: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
experimental: {
|
||||||
|
inlineRouteRules: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
future: {
|
||||||
|
compatibilityVersion: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
googleFonts: {
|
||||||
|
download: true,
|
||||||
|
families: {
|
||||||
|
Lato: true,
|
||||||
|
Alexandria: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
linkChecker: {
|
||||||
|
failOnError: true,
|
||||||
|
report: {
|
||||||
|
html: true,
|
||||||
|
markdown: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
modules: [
|
||||||
|
'@pinia/nuxt',
|
||||||
|
'@nuxt/image',
|
||||||
|
'@nuxtjs/seo',
|
||||||
|
'@nuxt/content',
|
||||||
|
'@nuxtjs/google-fonts',
|
||||||
|
'@nuxtjs/tailwindcss',
|
||||||
|
'@nuxtjs/color-mode',
|
||||||
|
'@nuxt/eslint',
|
||||||
|
['@nuxtjs/stylelint-module', { failOnError: true, lintOnStart: false }],
|
||||||
|
],
|
||||||
|
|
||||||
|
nitro: {
|
||||||
|
prerender: {
|
||||||
|
crawlLinks: true,
|
||||||
|
autoSubfolderIndex: true,
|
||||||
|
failOnError: true,
|
||||||
|
routes: ['/robots.txt', '/sitemap.xml'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [],
|
||||||
|
|
||||||
|
robots: {
|
||||||
|
enabled: true,
|
||||||
|
blockNonSeoBots: false,
|
||||||
|
credits: false,
|
||||||
|
debug: false,
|
||||||
|
metaTag: true,
|
||||||
|
groups: [],
|
||||||
|
disallow: [' '],
|
||||||
|
},
|
||||||
|
|
||||||
|
site: {
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
url: config.url,
|
||||||
|
name: config.name,
|
||||||
|
indexable: true,
|
||||||
|
trailingSlash: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
sitemap: {
|
||||||
|
sources: [
|
||||||
|
/*'/api/__sitemap__/content'*/
|
||||||
|
],
|
||||||
|
cacheMaxAgeSeconds: 360,
|
||||||
|
exclude: [],
|
||||||
|
credits: false,
|
||||||
|
xslColumns: [
|
||||||
|
{ label: 'URL', width: '50%' },
|
||||||
|
{ label: 'Last Modified', select: 'sitemap:lastmod', width: '25%' },
|
||||||
|
{ label: 'Priority', select: 'sitemap:priority', width: '12.5%' },
|
||||||
|
{
|
||||||
|
label: 'Change Frequency',
|
||||||
|
select: 'sitemap:changefreq',
|
||||||
|
width: '12.5%',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
defaults: {
|
||||||
|
lastmod: config.build.date,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
tailwindcss: {
|
||||||
|
exposeConfig: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
typescript: {
|
||||||
|
typeCheck: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
vue: {
|
||||||
|
compilerOptions: {
|
||||||
|
isCustomElement: (tag) => tag === 'iconify-icon',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||