feat: rewrite to solidjs

This commit is contained in:
Tholut A 2026-08-21 15:28:21 +07:00
parent 74c6215c27
commit afa2db26e7
21 changed files with 1074 additions and 1835 deletions

View file

@ -0,0 +1,48 @@
.container {
min-width: 300px;
}
ul {
padding: 0;
}
li {
list-style: none;
text-align: left;
font-size: 14pt;
}
li label {
display: block;
border: 1px solid #ffffff00;
}
li label:hover {
cursor: pointer;
border: 1px solid #ffffff;
}
li .button-delete {
display: none;
font-size: 10pt;
float: right;
height: 100%;
}
li:hover .button-delete {
display: inline-block;
}
.form-input input,
.form-input button {
width: 100%;
padding: 2px 4px;
margin-bottom: 5px;
}
.form-input input {
font-size: 12pt;
box-sizing: border-box;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
}

View file

@ -0,0 +1,89 @@
import { type Component, createResource, createSignal } from "solid-js";
import "./BlockedSite.css";
import { BLOCKED_SITES_REPO_KEY } from "@/utils/proxy-service-keys";
import type { BlockedSite } from "@/utils/types";
import { createProxyService } from "@webext-core/proxy-service";
import { v4 as uuidv4 } from "uuid";
const blockedSitesRepo = createProxyService(BLOCKED_SITES_REPO_KEY);
export const BlockedSite: Component = (props) => {
const [blockedSites, { refetch }] = createResource(blockedSitesRepo.getAll);
const [inputNewSite, setInputNewSite] = createSignal("");
const createBlockedSite = async () => {
const url = inputNewSite();
if (!url) return;
if (!url.trim()) return;
const generateTodoId = uuidv4();
await blockedSitesRepo.insert({
id: generateTodoId,
url: url,
blocked: true,
});
setInputNewSite("");
await refetch();
};
const updateBlockedSite = async (
blockedSite: BlockedSite,
isBlocked: boolean,
) => {
blockedSite.blocked = isBlocked;
await blockedSitesRepo.update(blockedSite);
await refetch();
};
const deleteBlockedSite = async (blockedSite: BlockedSite) => {
await blockedSitesRepo.delete(blockedSite);
await refetch();
};
return (
<>
<div class="container">
<h2>[blocked sites]</h2>
<div class="form-input">
<input
placeholder="ba-ka.org"
onKeyDown={(e) => {
// key code 13 is enter
if (e.keyCode === 13) {
createBlockedSite();
}
}}
onInput={(e) => setInputNewSite(e.currentTarget.value)}
type="input"
/>
<button onClick={() => createBlockedSite()}>[add site]</button>
</div>
<br />
<For each={blockedSites()}>
{(blockedSite) => {
return (
<li>
<label>
<input
type="checkbox"
checked={blockedSite.blocked}
onChange={(e) =>
updateBlockedSite(blockedSite, e.currentTarget.checked)
}
/>
{blockedSite.url}
<button
class="button-delete"
onClick={() => deleteBlockedSite(blockedSite)}
>
x
</button>
</label>
</li>
);
}}
</For>
</div>
</>
);
};

View file

@ -1,114 +0,0 @@
<script lang="ts" setup>
import { BLOCKED_SITES_REPO_KEY } from "@/utils/proxy-service-keys";
import { BlockedSite } from "@/utils/types";
import { createProxyService } from '@webext-core/proxy-service';
import { v4 as uuidv4 } from "uuid"
const blockedSitesRepo = createProxyService(BLOCKED_SITES_REPO_KEY);
const props = defineProps<{}>()
const blockedSites = ref<BlockedSite[]>([]);
const inputNewSite = ref<string | null>();
const refreshBlockedSite = async () => {
blockedSites.value = await blockedSitesRepo.getAll();
}
onBeforeMount(async () => refreshBlockedSite())
const insertBlockedSite = async () => {
if (!inputNewSite.value) return
if (!inputNewSite.value.trim()) return
const generateTodoId = uuidv4()
await blockedSitesRepo.insert({
id: generateTodoId,
url: inputNewSite.value,
blocked: true
})
inputNewSite.value = null
refreshBlockedSite()
}
const updateBlockedSite = async (blockedSite: BlockedSite) => {
await blockedSitesRepo.update(toRaw(blockedSite))
refreshBlockedSite()
}
const deleteBlockedSite = async (blockedSite: BlockedSite) => {
await blockedSitesRepo.delete(toRaw(blockedSite))
refreshBlockedSite()
}
</script>
<template>
<div class="container">
<h2>[blocked sites]</h2>
<div class="form-input">
<input @keydown.enter="insertBlockedSite" v-model="inputNewSite" placeholder="ba-ka.org" type="input"/>
<button @click="insertBlockedSite">[add site]</button>
</div>
<br/>
<div v-if="blockedSites.length === 0">no blocked sites :(</div>
<ul v-else v-for="blockedSite in blockedSites">
<li :key="blockedSite.id">
<label>
<input type="checkbox" @change="updateBlockedSite(blockedSite)" v-model="blockedSite.blocked" :id="blockedSite.id"/>
{{ blockedSite.url }}
<button class="button-delete" @click="deleteBlockedSite(blockedSite)">x</button>
</label>
</li>
</ul>
</div>
</template>
<style scoped>
.container {
min-width: 300px;
}
ul {
padding: 0;
}
li {
list-style: none;
text-align: left;
font-size: 14pt;
}
li label {
display: block;
}
li label:hover {
cursor: pointer;
border: 1px solid #ffffff;
}
li .button-delete {
display: none;
padding: 1px 4px;
font-size: 10pt;
float: right
}
li:hover .button-delete {
display: inline-block;
}
.form-input input,
.form-input button {
width: 100%;
padding: 2px 4px;
margin-bottom: 5px;
}
.form-input input {
font-size: 12pt;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
}
</style>

15
components/MainToggle.css Normal file
View file

@ -0,0 +1,15 @@
.button-toggle {
padding: 0;
margin: 0;
border: none;
background: none;
}
.button-toggle .logo {
width: 80%;
height: 80%;
cursor: pointer;
}
.button-toggle .logo-on {
filter: drop-shadow(0 0 1em #ffffff);
}

28
components/MainToggle.tsx Normal file
View file

@ -0,0 +1,28 @@
import { createSignal, onMount } from "solid-js";
import { featureEnableBlock } from "@/utils/storage";
import "./MainToggle.css";
export const MainToggle = (props) => {
const [isEnabled, setIsEnabled] = createSignal(false);
onMount(async () => {
const getFeatureEnable = await featureEnableBlock.getValue();
setIsEnabled(getFeatureEnable);
});
const toggleOnOff = () => {
setIsEnabled(!isEnabled());
featureEnableBlock.setValue(isEnabled());
};
return (
<div>
<button class="button-toggle" onClick={() => toggleOnOff()}>
<img
src="/icon/128.png"
class={isEnabled() ? "logo logo-on" : "logo"}
alt="let me focus logo"
/>
</button>
<br />
{isEnabled() ? "[on]" : "[off]"}
</div>
);
};

128
components/Todo.tsx Normal file
View file

@ -0,0 +1,128 @@
import {
type Component,
createResource,
createSignal,
createEffect,
} from "solid-js";
import "./BlockedSite.css";
import { TODOS_REPO_KEY } from "@/utils/proxy-service-keys";
import type { Todo } from "@/utils/types";
import { createProxyService } from "@webext-core/proxy-service";
import { v4 as uuidv4 } from "uuid";
const todosRepo = createProxyService(TODOS_REPO_KEY);
export const Todo: Component = (props) => {
const [rawTodos, { refetch }] = createResource(todosRepo.getAll, []);
const [todos, setTodos] = createSignal([]);
const [inputNewTodo, setInputNewTodo] = createSignal("");
createEffect(async () => {
const allTodos = rawTodos();
if (!allTodos) {
return;
}
console.log("OI");
const filteredTodos = allTodos.filter(
(todo: Todo) =>
(props.showCompleted && todo.completed) ||
(props.showOnlyCompleted && todo.completed) ||
(!props.showOnlyCompleted && !todo.completed),
);
setTodos(filteredTodos);
});
const createTodo = async () => {
const name = inputNewTodo();
if (!name) return;
if (!name.trim()) return;
const generateTodoId = uuidv4();
await todosRepo.insert({
id: generateTodoId,
name: name,
completed: false,
});
setInputNewTodo("");
await refetch();
};
const updateTodo = async (todo: Todo, isCompleted: boolean) => {
todo.completed = isCompleted;
await todosRepo.update(todo);
await refetch();
};
const deleteTodo = async (todo: Todo) => {
await todosRepo.delete(todo);
await refetch();
};
const clearAllCompletedTodo = async () => {
const allTodos = await todosRepo.getAll();
const completedTodos = allTodos.filter((todo) => todo.completed);
const deletePromises = completedTodos.map((todo) => todosRepo.delete(todo));
await Promise.all(deletePromises);
refetch();
};
return (
<>
<div class="container">
{props.showOnlyCompleted ? (
<h2>[completed todos]</h2>
) : (
<h2>[todos]</h2>
)}
{props.showInput && (
<div class="form-input">
<input
placeholder="playing skateboard at 5am"
onKeyDown={(e) => {
// key code 13 is enter
if (e.keyCode === 13) {
createTodo();
}
}}
onInput={(e) => setInputNewTodo(e.currentTarget.value)}
type="input"
/>
<button onClick={() => createTodo()}>[add todo]</button>
</div>
)}
{props.showClearAllTodo && (
<div>
<button onClick={() => clearAllCompletedTodo()}>
[clear all completed todos]
</button>
</div>
)}
<br />
<For each={todos()}>
{(todo) => {
return (
<li>
<label>
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => updateTodo(todo, e.currentTarget.checked)}
/>
{todo.name}
{props.hoverShowDeleteButton && (
<button
class="button-delete"
onClick={() => deleteTodo(todo)}
>
x
</button>
)}
</label>
</li>
);
}}
</For>
</div>
</>
);
};

View file

@ -1,140 +0,0 @@
<script lang="ts" setup>
import { TODOS_REPO_KEY } from "@/utils/proxy-service-keys";
import { Todo } from "@/utils/types";
import { createProxyService } from '@webext-core/proxy-service';
import { v4 as uuidv4 } from "uuid"
const todosRepo = createProxyService(TODOS_REPO_KEY);
const props = defineProps<{
showCompleted?: boolean,
showOnlyCompleted?: boolean,
showInput?: boolean,
hoverShowDeleteButton?: boolean,
showClearAllTodo?: boolean
}>()
const todos = ref<Todo[]>([]);
const inputNewTodo = ref<string | null>();
const refreshTodo = async () => {
const allTodos = await todosRepo.getAll();
todos.value = allTodos.filter(
(todo: Todo) =>
(props.showCompleted && todo.completed) ||
(props.showOnlyCompleted && todo.completed) ||
(!props.showOnlyCompleted && !todo.completed)
)
}
onBeforeMount(async () => refreshTodo())
const insertTodo = async () => {
if (!inputNewTodo.value) return
if (!inputNewTodo.value.trim()) return
const generateTodoId = uuidv4()
await todosRepo.insert({
id: generateTodoId,
name: inputNewTodo.value,
completed: false
})
inputNewTodo.value = null
refreshTodo()
}
const updateTodo = async (todo: Todo) => {
await todosRepo.update(toRaw(todo))
refreshTodo()
}
const deleteTodo = async (todo: Todo) => {
await todosRepo.delete(toRaw(todo))
refreshTodo()
}
const clearAllCompletedTodo = async () => {
const allTodos = await todosRepo.getAll()
const completedTodos = allTodos.filter((todo) => todo.completed)
const deletePromises = completedTodos.map((todo) => todosRepo.delete(todo))
await Promise.all(deletePromises)
refreshTodo()
}
</script>
<template>
<div class="container">
<h2 v-if="props.showOnlyCompleted">[completed todos]</h2>
<h2 v-if="!props.showOnlyCompleted">[todos]</h2>
<div v-if="showInput" class="form-input">
<input @keydown.enter="insertTodo" v-model="inputNewTodo" placeholder="playing skateboard at 5am" type="input"/>
<button @click="insertTodo">[add todo]</button>
</div>
<div v-if="showClearAllTodo">
<button @click="clearAllCompletedTodo">[clear all completed todos]</button>
</div>
<br/>
<div v-if="todos.length === 0">no todos :D</div>
<ul v-else v-for="todo in todos">
<li :class="{ completed: todo.completed }" :key="todo.id">
<label>
<input type="checkbox" @change="updateTodo(todo)" v-model="todo.completed" :id="todo.id"/>
{{ todo.name }}
<button class="button-delete" v-if="props.hoverShowDeleteButton" @click="deleteTodo(todo)">x</button>
</label>
</li>
</ul>
</div>
</template>
<style scoped>
.container {
min-width: 300px;
}
ul {
padding: 0;
}
li {
list-style: none;
text-align: left;
font-size: 14pt;
}
li label {
display: block;
}
li label:hover {
cursor: pointer;
}
li.completed {
text-decoration: line-through;
}
li .button-delete {
display: none;
padding: 1px 4px;
font-size: 10pt;
float: right
}
li:hover .button-delete {
display: inline-block;
}
.form-input input,
.form-input button {
width: 100%;
padding: 2px 4px;
margin-bottom: 5px;
}
.form-input input {
font-size: 12pt;
box-sizing: border-box;
-webkit-box-sizing:border-box;
-moz-box-sizing: border-box;
}
</style>

View file

@ -1,54 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { featureEnableBlock } from "@/utils/storage"
const isEnabled = ref(false)
let unwatchStorage: (() => void | undefined)
onMounted(async() => {
isEnabled.value = await featureEnableBlock.getValue()
unwatchStorage = featureEnableBlock.watch((newValue) => {
if (newValue !== isEnabled.value) {
isEnabled.value = newValue
}
})
})
onUnmounted(() => {
if (unwatchStorage) unwatchStorage();
})
watch(isEnabled, async (newValue) => {
await featureEnableBlock.setValue(newValue)
})
const toggleOnOff = () => {
isEnabled.value = !isEnabled.value
}
</script>
<template>
<button @click="toggleOnOff" class="button-toggle">
<img src="/icon/128.png" class="logo" :class="{ 'logo-on': isEnabled }" alt="let me focus logo" />
</button>
<br/>
{{ isEnabled ? '[on]' : '[off]' }}
</template>
<style scoped>
.button-toggle {
padding: 0;
margin: 0;
border: none;
background: none;
}
.button-toggle .logo {
width: 80%;
cursor: pointer;
}
.button-toggle .logo-on {
filter: drop-shadow(0 0 1em #ffffff);
}
</style>

View file

@ -0,0 +1,15 @@
import stopImage from "@/assets/stop.png";
import "./style.css";
import { Todo } from "@/components/Todo";
function App() {
return (
<div class="main">
<img class="logo" src={stopImage} />
<h1 class="message">[go back to your work]</h1>
<Todo />
</div>
);
}
export default App;

View file

@ -1,8 +1,8 @@
import { ContentScriptContext } from "#imports"; import { ContentScriptContext } from "#imports";
import App from "./App.vue"; import App from "./App";
import { render } from "solid-js/web";
import { featureEnableBlock } from "@/utils/storage"; import { featureEnableBlock } from "@/utils/storage";
import { createApp } from "vue"; import { createProxyService } from "@webext-core/proxy-service";
import { createProxyService } from '@webext-core/proxy-service';
import { BlockedSite } from "@/utils/types"; import { BlockedSite } from "@/utils/types";
const blockedSitesRepo = createProxyService(BLOCKED_SITES_REPO_KEY); const blockedSitesRepo = createProxyService(BLOCKED_SITES_REPO_KEY);
@ -14,12 +14,16 @@ export default defineContentScript({
const ui = await defineOverlay(ctx); const ui = await defineOverlay(ctx);
const isBlockEnabled = await featureEnableBlock.getValue(); const isBlockEnabled = await featureEnableBlock.getValue();
const currentDomain = window.location.hostname; const currentDomain = window.location.hostname;
const blockedSites = await blockedSitesRepo.getAll() const blockedSites = await blockedSitesRepo.getAll();
function isOnBlockList() { function isOnBlockList() {
return ( return (
blockedSites.filter(function (blockedSite: BlockedSite) { blockedSites.filter(function (blockedSite: BlockedSite) {
return (blockedSite.url == currentDomain || "www." + blockedSite.url == currentDomain) && blockedSite.blocked; return (
(blockedSite.url == currentDomain ||
"www." + blockedSite.url == currentDomain) &&
blockedSite.blocked
);
}).length > 0 }).length > 0
); );
} }
@ -44,8 +48,7 @@ function defineOverlay(ctx: ContentScriptContext) {
position: "overlay", position: "overlay",
zIndex: 9999, zIndex: 9999,
onMount(container, _shadow, shadowHost) { onMount(container, _shadow, shadowHost) {
const app = createApp(App); const app = render(App, container);
app.mount(container);
shadowHost.style.pointerEvents = "none"; shadowHost.style.pointerEvents = "none";
return app; return app;
}, },

View file

@ -1,17 +1,3 @@
<script lang="ts" setup>
import stopImage from "~/assets/stop.png";
import Todo from "@/components/Todo.vue";
</script>
<template>
<div class="main">
<img class="logo" :src="stopImage" />
<h1 class="message">[go back to your work]</h1>
<Todo />
</div>
</template>
<style>
body { body {
margin: 0; margin: 0;
} }
@ -39,4 +25,3 @@ h1.message,
img.logo { img.logo {
filter: drop-shadow(0 0 1em #ffffff); filter: drop-shadow(0 0 1em #ffffff);
} }
</style>

46
entrypoints/popup/App.tsx Normal file
View file

@ -0,0 +1,46 @@
import { createSignal } from "solid-js";
import "./style.css";
import { MainToggle } from "@/components/MainToggle";
import { BlockedSite } from "@/components/BlockedSite";
import { Todo } from "@/components/Todo";
function App() {
const menus = ["main", "completed", "blocked sites"];
const [menu, setMenu] = createSignal("main");
return (
<>
<div>
<MainToggle />
<h1>let me focus</h1>
<ul class="menu">
<For each={menus}>
{(menu) => (
<li>
<button onClick={() => setMenu(menu)}>[{menu}]</button>
</li>
)}
</For>
</ul>
<Suspense>
<Switch>
<Match when={menu() === "main"}>
<Todo showInput hoverShowDeleteButton />
</Match>
<Match when={menu() === "completed"}>
<Todo showOnlyCompleted showClearAllTodo />
</Match>
<Match when={menu() === "blocked sites"}>
<BlockedSite />
</Match>
</Switch>
</Suspense>
<br />[{browser.runtime.getManifest().version}]
</div>
</>
);
}
export default App;

View file

@ -1,51 +0,0 @@
<script lang="ts" setup>
import ToggleOnOff from "@/components/ToggleOnOff.vue";
import Todo from "@/components/Todo.vue"
import BlockedSite from "@/components/BlockedSite.vue"
const currentMenu = ref("main")
const menuLists = [
"main",
"completed",
"blocked sites",
]
const changeMenu = (menu: string) => {
currentMenu.value = menu
}
</script>
<template>
<ToggleOnOff />
<h1>let me focus</h1>
<ul style="text-align: center;" v-for="menu in menuLists">
<li>
<button @click="changeMenu(menu)">[{{ menu }}]</button>
</li>
</ul>
<Todo v-if="currentMenu === 'main'" showInput hoverShowDeleteButton />
<Todo v-if="currentMenu === 'completed'" showOnlyCompleted showClearAllTodo />
<BlockedSite v-if="currentMenu === 'blocked sites'" />
<br/>
[{{browser.runtime.getManifest().version}}]
</template>
<style scoped>
ul {
padding: 0;
display: inline-block;
}
li {
list-style: none;
margin: 0px 5px;
}
li button {
cursor: pointer;
}
.logo {
height: 6em;
filter: drop-shadow(0 0 1em #ffffff);
}
</style>

View file

@ -1,13 +1,13 @@
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>let me focus</title> <title>let me focus</title>
<meta name="manifest.type" content="browser_action" /> <meta name="manifest.type" content="browser_action" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="root"></div>
<script type="module" src="./main.ts"></script> <script type="module" src="./main.tsx"></script>
</body> </body>
</html> </html>

View file

@ -1,5 +0,0 @@
import { createApp } from 'vue';
import './style.css';
import App from './App.vue';
createApp(App).mount('#app');

View file

@ -0,0 +1,6 @@
import { render } from "solid-js/web";
import "./style.css";
import App from "./App";
render(() => <App />, document.getElementById("root")!);

View file

@ -1,42 +1,54 @@
:root { :root {
font-family: monospace; font-family: monospace;
line-height: 1.5; line-height: 1.5;
font-weight: 400; font-weight: 400;
color-scheme: light dark; color-scheme: light dark;
color: #ffffff; color: #ffffff;
background-color: #222222; background-color: #222222;
} }
body { body {
margin: 0; margin: 0;
display: flex; display: flex;
place-items: center; place-items: center;
min-width: 400px; min-width: 400px;
min-height: 100vh; min-height: 100vh;
} }
h1 { h1 {
font-size: 2.4em; font-size: 2.4em;
} }
button { #root {
font-size: 1em; max-width: 400px;
font-weight: 500; margin: 0 auto;
} padding: 2rem;
text-align: center;
#app {
max-width: 400px;
margin: 0 auto;
padding: 2rem;
text-align: center;
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root { :root {
color: #222222; color: #222222;
background-color: #ffffff; background-color: #ffffff;
} }
button { button {
background-color: #f9f9f9; background-color: #f9f9f9;
} }
}
ul.menu {
padding: 0;
text-align: center;
}
ul.menu li {
list-style: none;
margin: 0px 5px;
display: inline-block;
}
ul.menu li button {
cursor: pointer;
font-size: 1em;
font-weight: 500;
} }

2033
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,14 +19,13 @@
"dependencies": { "dependencies": {
"@webext-core/proxy-service": "^3.0.2", "@webext-core/proxy-service": "^3.0.2",
"idb": "^8.0.3", "idb": "^8.0.3",
"uuid": "^14.0.1", "solid-js": "^1.9.15",
"vue": "^3.5.29" "uuid": "^14.0.1"
}, },
"devDependencies": { "devDependencies": {
"@wxt-dev/module-vue": "^1.0.3", "@wxt-dev/module-solid": "^1.1.4",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^8.1.5", "vite": "^8.1.5",
"vue-tsc": "^3.2.5",
"web-ext": "^5.5.0", "web-ext": "^5.5.0",
"wxt": "^0.20.27" "wxt": "^0.20.27"
} }

View file

@ -1,3 +1,7 @@
{ {
"extends": "./.wxt/tsconfig.json" "extends": "./.wxt/tsconfig.json",
"compilerOptiosn": {
"jsx": "preserve",
"jsxImportSource": "solid-js"
}
} }

View file

@ -2,7 +2,7 @@ import { defineConfig } from 'wxt';
// See https://wxt.dev/api/config.html // See https://wxt.dev/api/config.html
export default defineConfig({ export default defineConfig({
modules: ['@wxt-dev/module-vue'], modules: ['@wxt-dev/module-solid'],
manifest: { manifest: {
name: "let me focus", name: "let me focus",
permissions: ['storage'] permissions: ['storage']