Implement settings

master
KaKi87 2023-04-22 08:52:53 +02:00
parent d11f6ebba0
commit 733db35dec
5 changed files with 340 additions and 3 deletions

View File

@ -4,6 +4,7 @@
"license": "MIT",
"dependencies": {
"@fontsource/ibm-plex-sans": "^4.5.13",
"axios": "^1.3.6",
"boxicons": "^2.1.4",
"destyle.css": "^3.0.2",
"pinia": "^2.0.35",

View File

@ -1,11 +1,234 @@
<script>
import { defineComponent } from 'vue';
import axios from 'axios';
export default defineComponent({});
import input from '../_input.vue';
import button from '../_button.vue';
const blobToBase64 = blob => new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.addEventListener('loadend', () => resolve(fileReader.result));
fileReader.addEventListener('error', reject);
fileReader.readAsDataURL(blob);
});
export default defineComponent({
components: {
'c-input': input,
'c-button': button
},
data: () => ({
icon: undefined,
shortcutInput: '',
urlInput: ''
}),
computed: {
searchEngines: function(){
return this.store.searchEngines;
}
},
methods: {
fetchIcon: async function(){
let hostname;
try {
({
hostname
} = new URL(this.urlInput));
}
catch {}
this.icon = hostname
? await blobToBase64((await axios({
url: `https://api.kaki87.net/favicon/${hostname}`,
responseType: 'blob'
})).data)
: undefined;
},
remove: function(item){
this.store.searchEngines.splice(this.store.searchEngines.indexOf(item), 1);
},
setDefault: function(item){
this.store.searchEngines.forEach(item => item.isDefault = false);
item.isDefault = true;
},
add: function(){
this.store.searchEngines.push({
shortcut: this.shortcutInput,
url: this.urlInput,
icon: this.icon
});
this.shortcutInput = '';
this.urlInput = '';
this.icon = undefined;
},
importSettings: async function(){
const fileElement = document.createElement('input');
fileElement.setAttribute('type', 'file');
fileElement.addEventListener(
'change',
() => {
const fileReader = new FileReader();
fileReader.addEventListener(
'load',
() => Object.assign(this.store, JSON.parse(fileReader.result))
);
fileReader.readAsText(fileElement.files[0]);
}
);
fileElement.click();
},
exportSettings: function(){
const fileElement = document.createElement('a');
fileElement.setAttribute('download', `Startpage_${new Date().toISOString()}.json`);
fileElement.setAttribute(
'href',
URL.createObjectURL(new Blob(
[JSON.stringify(this.store, null, 4)],
{ type: 'text/json' }
))
);
fileElement.click();
}
},
mounted: function(){
let urlInputTimeout;
this.$watch(
() => this.urlInput,
() => {
clearTimeout(urlInputTimeout);
urlInputTimeout = setTimeout(
() => this.fetchIcon(),
1000
);
}
);
}
});
</script>
<template>
<section
class="App__Main__Settings"
></section>
</template>
>
<h3 class="App__Main__Settings__Heading">Search engines</h3>
<div class="App__Main__Settings__SearchEnginesContainer"><table
class="App__Main__Settings__SearchEnginesContainer__SearchEngines"
>
<tr>
<th style="border-left: 0;border-top: 0"></th>
<th>Shortcut</th>
<th>URL</th>
<th></th>
<th>Default</th>
</tr>
<tr
v-for="item in searchEngines"
class="App__Main__Settings__SearchEngines__Item"
>
<td><img
class="App__Main__Settings__SearchEngines__Item__Icon"
:src="item.icon"
:alt="item.shortcut"
></td>
<td>{{ item.shortcut }}</td>
<td>{{ item.url }}</td>
<td>
<button
@click="remove(item)"
><i
class="bx bxs-minus-circle"
></i></button>
</td>
<td><input
:key="item.isDefault"
type="radio"
:checked="item.isDefault"
name="isDefault"
@click.prevent="setDefault(item)"
></td>
</tr>
<tr>
<td>
<img
class="App__Main__Settings__SearchEngines__Item__Icon"
v-if="icon"
:src="icon"
:alt="shortcutInput"
>
<span
v-else
class="App__Main__Settings__SearchEngines__Item__IconPlaceholder"
><i
class="bx bxs-image-alt"
></i></span>
</td>
<td><c-input
v-model="shortcutInput"
placeholder="b"
></c-input></td>
<td><c-input
v-model="urlInput"
placeholder="https://search.brave.com/search?q=%s"
></c-input></td>
<td><button
@click="add"
><i
class="bx bxs-plus-circle"
></i></button></td>
</tr>
</table></div>
<h3 class="App__Main__Settings__Heading">Import/Export</h3>
<p class="App__Main__Settings__ImportExport">
<c-button
label="Import settings"
@click="importSettings"
></c-button>
<c-button
label="Export settings"
@click="exportSettings"
></c-button>
</p>
</section>
</template>
<style>
.App__Main__Settings {
padding: 1rem;
display: flex;
flex-direction: column;
row-gap: 1rem;
}
.App__Main__Settings__Heading {
font-size: 1.5rem;
}
.App__Main__Settings__SearchEnginesContainer {
overflow-x: auto;
}
.App__Main__Settings__SearchEnginesContainer__SearchEngines {
border-collapse: collapse;
}
.App__Main__Settings__SearchEnginesContainer__SearchEngines th,
.App__Main__Settings__SearchEnginesContainer__SearchEngines td {
border: 1px solid var(--color-medium);
padding: 0.5rem;
}
.App__Main__Settings__SearchEnginesContainer__SearchEngines td {
vertical-align: middle;
}
.App__Main__Settings__SearchEngines__Item__Icon,
.App__Main__Settings__SearchEngines__Item__IconPlaceholder {
width: 2rem;
height: 2rem;
}
.App__Main__Settings__SearchEngines__Item__Icon {
object-fit: contain;
}
.App__Main__Settings__SearchEngines__Item__IconPlaceholder {
display: flex;
align-items: center;
justify-content: center;
}
.App__Main__Settings__ImportExport {
display: flex;
column-gap: 1rem;
}
</style>

View File

@ -0,0 +1,25 @@
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
label: undefined
}
});
</script>
<template>
<button
class="button"
>{{ label }}</button>
</template>
<style>
.button {
padding: 0.5rem 1rem;
background-color: var(--color-dark);
}
.button:hover {
background-color: var(--color-medium);
}
</style>

31
src/components/_input.vue Normal file
View File

@ -0,0 +1,31 @@
<script>
import { defineComponent } from 'vue';
export default defineComponent({
props: {
modelValue: undefined,
placeholder: undefined
}
});
</script>
<template>
<input
class="input"
type="text"
:value="modelValue"
:placeholder="placeholder"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>
<style>
.input {
width: 100%;
outline: none;
border-bottom: 1px solid var(--color-medium);
}
.input:focus-visible {
border-bottom-color: var(--color-light);
}
</style>

View File

@ -129,6 +129,20 @@
resolved "https://registry.yarnpkg.com/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.8.0.tgz#ab21f027594fa827c1889e8b646da7be27c7908a"
integrity sha512-loGD63sacRzOzSJgQnB9ZAhaQGkN7wl2Zuw7tsphI5Isa0irijrRo6EnJii/GgjGefIFO8AIO7UivzRhFaEk9w==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
axios@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.6.tgz#1ace9a9fb994314b5f6327960918406fa92c6646"
integrity sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==
dependencies:
follow-redirects "^1.15.0"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
boxicons@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/boxicons/-/boxicons-2.1.4.tgz#6b478b1657f4019c657c8050bd145c0cd0655d3c"
@ -141,11 +155,23 @@ boxicons@^2.1.4:
react-interactive "^0.8.1"
react-router-dom "^4.2.2"
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
csstype@^2.6.8:
version "2.6.18"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.18.tgz#980a8b53085f34af313410af064f2bd241784218"
integrity sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
destyle.css@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/destyle.css/-/destyle.css-3.0.2.tgz#80c331db91f02337040b62dc534ab4f3dc075546"
@ -313,6 +339,20 @@ estree-walker@^2.0.2:
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
follow-redirects@^1.15.0:
version "1.15.2"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
@ -385,6 +425,18 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.4"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12:
version "2.1.35"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
nanoid@^3.1.30:
version "3.1.30"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362"
@ -457,6 +509,11 @@ prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
object-assign "^4.1.1"
react-is "^16.13.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
react-dom@^16.0.0:
version "16.14.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89"