✨ Implement settings
parent
d11f6ebba0
commit
733db35dec
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
57
yarn.lock
57
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue