diff --git a/package.json b/package.json index b46c215..870509b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "blink-eye", "private": true, - "version": "1.8.1", + "version": "1.9.0", "type": "module", "displayName": "Blink Eye", "categories": ["Other"], diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index df460a3..0778e1b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "Blink-Eye" -version = "1.8.1" +version = "1.9.0" dependencies = [ "serde", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9cfcf3a..8cd0f55 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "Blink-Eye" -version = "1.8.1" +version = "1.9.0" description = "A minimalist eye care reminder app for Windows, macOS, and Linux." authors = ["Noman Dhoni"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index fdc3320..5a776d1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.6", "productName": "Blink Eye", - "version": "1.8.1", + "version": "1.9.0", "identifier": "com.blinkeye.app", "build": { "beforeDevCommand": "bun run dev", diff --git a/src/App.tsx b/src/App.tsx index ff04048..25d4966 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import "./App.css"; import { useAutoStart } from "./hooks/useAutoStart"; import { ErrorDisplay } from "./components/ErrorDisplay"; import { LoadingSpinner } from "./components/LoadingSpinner"; +import Workday from "./components/window/Workday"; // Lazy load route components const Dashboard = lazy(() => import("./components/window/Dashboard")); @@ -59,6 +60,14 @@ function App() { } /> + }> + + + } + /> }> - + } /> diff --git a/src/components/ConfigDataLoader.tsx b/src/components/ConfigDataLoader.tsx new file mode 100644 index 0000000..c781000 --- /dev/null +++ b/src/components/ConfigDataLoader.tsx @@ -0,0 +1,84 @@ +import { useEffect } from "react"; +import { BaseDirectory } from "@tauri-apps/api/path"; +import { exists } from "@tauri-apps/plugin-fs"; +import Database from "@tauri-apps/plugin-sql"; +import { load } from "@tauri-apps/plugin-store"; + +const ConfigDataLoader: React.FC = () => { + const defaultWorkday = { + Monday: { start: "09:00", end: "17:00" }, + Tuesday: { start: "09:00", end: "17:00" }, + Wednesday: { start: "09:00", end: "17:00" }, + Thursday: { start: "09:00", end: "17:00" }, + Friday: { start: "09:00", end: "17:00" }, + Saturday: null, + Sunday: null, + }; + + useEffect(() => { + const setupDatabase = async () => { + // Check if the database file exists + const dbExists = await exists("appconfig.db", { + baseDir: BaseDirectory.AppData, + }); + + // Initialize the database + + if (!dbExists) { + const db = await Database.load("sqlite:appconfig.db"); + console.log("Database does not exist. Initializing..."); + + // Create the tables + await db.execute(` + CREATE TABLE IF NOT EXISTS config ( + key TEXT PRIMARY KEY, + value TEXT + ); + `); + + // Insert default values + await db.execute(`INSERT INTO config (key, value) VALUES (?, ?);`, [ + "blinkEyeWorkday", + JSON.stringify(defaultWorkday), + ]); + await db.execute(`INSERT INTO config (key, value) VALUES (?, ?);`, [ + "isWorkdayEnabled", + "false", + ]); + await db.execute(`INSERT INTO config (key, value) VALUES (?, ?);`, [ + "isUpdateAvailable", + "false", + ]); + await db.execute(`INSERT INTO config (key, value) VALUES (?, ?);`, [ + "usingStrictMode", + "false", + ]); + console.log("Database initialized with default configuration."); + } else { + console.log("Database already exists. No action needed."); + } + + const storeExists = await exists("store.json", { + baseDir: BaseDirectory.AppData, + }); + if (!storeExists) { + const store = await load("store.json", { autoSave: false }); + await store.set("blinkEyeReminderDuration", 20); + await store.set("blinkEyeReminderInterval", 20); + await store.set( + "blinkEyeReminderScreenText", + "Look 20 feet away to protect your eyes." + ); + await store.save(); + } else { + console.log("Store already exists. No action needed."); + } + }; + + setupDatabase(); + }, []); + + return null; // This component does not render anything +}; + +export default ConfigDataLoader; diff --git a/src/components/ReminderHandler.tsx b/src/components/ReminderHandler.tsx new file mode 100644 index 0000000..4bc1afd --- /dev/null +++ b/src/components/ReminderHandler.tsx @@ -0,0 +1,145 @@ +import { useEffect, useState } from "react"; +import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; +import Database from "@tauri-apps/plugin-sql"; +import { usePremiumFeatures } from "../contexts/PremiumFeaturesContext"; +import { load } from "@tauri-apps/plugin-store"; +import { useTrigger } from "../contexts/TriggerReRender"; + +// Define the type for workday configuration +type Workday = { [day: string]: { start: string; end: string } } | null; + +const ReminderHandler = () => { + const { trigger } = useTrigger(); // Use the trigger value from the context + const [interval, setInterval] = useState(20); + const [workday, setWorkday] = useState(null); + const [isWorkdayEnabled, setIsWorkdayEnabled] = useState(false); + const { canAccessPremiumFeatures } = usePremiumFeatures(); + + // Function to open the reminder window + const openReminderWindow = () => { + console.log("Opening reminder window..."); + const webview = new WebviewWindow("ReminderWindow", { + url: "/reminder", + fullscreen: true, + alwaysOnTop: true, + title: "Take A Break Reminder - Blink Eye", + skipTaskbar: true, + }); + + webview.once("tauri://created", () => { + console.log("Reminder window created"); + }); + + webview.once("tauri://error", (e) => { + console.error("Error creating reminder window:", e); + }); + }; + + // Fetch settings when `trigger` changes + useEffect(() => { + const fetchSettings = async () => { + console.log("Fetching settings due to trigger:", trigger); + const db = await Database.load("sqlite:appconfig.db"); + const store = await load("store.json", { autoSave: false }); + + // Load the reminder interval from storage + const storedInterval = await store.get( + "blinkEyeReminderInterval" + ); + if (storedInterval) setInterval(storedInterval); + + // Fetch workday setup from the database + type ConfigResult = { value: string }; + const workdayData = (await db.select( + "SELECT value FROM config WHERE key = ?", + ["blinkEyeWorkday"] + )) as ConfigResult[]; + if (workdayData.length > 0 && workdayData[0].value) { + try { + const parsedWorkday = JSON.parse(workdayData[0].value) as Workday; + setWorkday(parsedWorkday); + } catch (error) { + console.error("Failed to parse workday data:", error); + } + } + + // Fetch whether workday is enabled + const isEnabledData = (await db.select( + "SELECT value FROM config WHERE key = ?", + ["isWorkdayEnabled"] + )) as ConfigResult[]; + + if (isEnabledData.length > 0 && isEnabledData[0].value) { + setIsWorkdayEnabled(isEnabledData[0].value === "true"); + } + console.log(workdayData, "workdayData"); + console.log(storedInterval, "storedInterval"); + console.log(isWorkdayEnabled, "isWorkdayEnabled"); + }; + + fetchSettings(); + }, [trigger]); // Refetch settings when the trigger updates + + // Handle interval-based reminder logic + useEffect(() => { + console.log("Initializing reminder logic"); + let timer: number | null = null; + + const startReminder = () => { + console.log("Starting reminder timer with interval:", interval); + timer = window.setInterval(() => { + openReminderWindow(); + }, interval * 60 * 1000); // The interval value will be non-null + }; + + const checkWorkdayAndStartTimer = () => { + const now = new Date(); + const day = now.toLocaleString("en-US", { weekday: "long" }); + const todayWorkday = workday?.[day]; + + if (canAccessPremiumFeatures && isWorkdayEnabled) { + if (todayWorkday) { + const [startHour, startMinute] = todayWorkday.start + .split(":") + .map(Number); + const [endHour, endMinute] = todayWorkday.end.split(":").map(Number); + + const startTime = new Date(); + startTime.setHours(startHour, startMinute, 0, 0); + + const endTime = new Date(); + endTime.setHours(endHour, endMinute, 0, 0); + + console.log("Start time:", startTime); + console.log("End time:", endTime); + console.log("Now:", now); + + if (now >= startTime && now <= endTime) { + console.log("Within workday window. Starting reminder."); + startReminder(); + } else { + console.log("Outside workday window. Reminder not started."); + } + } else { + console.log("No workday setup for today. Reminder skipped."); + } + } else { + console.log("Non-premium user or workday disabled. Starting reminder."); + startReminder(); + } + }; + + // Start reminder logic if necessary + if (workday || !canAccessPremiumFeatures) { + checkWorkdayAndStartTimer(); + } + + return () => { + if (timer !== null) window.clearInterval(timer); // Cleanup previous timers + }; + }, [workday, isWorkdayEnabled, canAccessPremiumFeatures, interval]); + + return null; +}; + +export default ReminderHandler; diff --git a/src/components/StrictModeToggle.tsx b/src/components/StrictModeToggle.tsx new file mode 100644 index 0000000..443220e --- /dev/null +++ b/src/components/StrictModeToggle.tsx @@ -0,0 +1,75 @@ +import { useState, useEffect } from "react"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import Database from "@tauri-apps/plugin-sql"; + +interface ConfigRow { + value: string; +} + +const StrictModeToggle = () => { + const [isStrictModeEnabled, setIsStrictModeEnabled] = useState(false); + + useEffect(() => { + const initializeStrictMode = async () => { + try { + // Load initial configuration store + const db = await Database.load("sqlite:appconfig.db"); + + // Retrieve the 'usingStrictMode' value from the config table + const result: ConfigRow[] = await db.select( + "SELECT value FROM config WHERE key = 'usingStrictMode';" + ); + + if (result.length > 0) { + setIsStrictModeEnabled(result[0].value === "true"); + } + } catch (error) { + console.error("Failed to initialize strict mode:", error); + } + }; + initializeStrictMode(); + }, []); + + const handleCheckboxChange = async (checked: boolean) => { + try { + const db = await Database.load("sqlite:appconfig.db"); + + // Use an `INSERT OR REPLACE` or `UPDATE` query + await db.execute( + ` + INSERT INTO config (key, value) VALUES ('usingStrictMode', ?) + ON CONFLICT(key) DO UPDATE SET value = excluded.value; + `, + [checked ? "true" : "false"] + ); + + setIsStrictModeEnabled(checked); + } catch (error) { + console.error("Failed to update strict mode status:", error); + } + }; + + return ( +
+
+ +

+ This will hide the 'Skip this time' button to force follow the break. +

+
+ +
+ ); +}; + +export default StrictModeToggle; diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 1b9feab..eb19f53 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -50,7 +50,7 @@ const items = [ }, { title: "Workday Setup", - url: "/soon", + url: "/workday", icon: Calendar, isPremiumFeature: true, }, diff --git a/src/components/window/AllSettings.tsx b/src/components/window/AllSettings.tsx index 0bf1106..a96df7d 100644 --- a/src/components/window/AllSettings.tsx +++ b/src/components/window/AllSettings.tsx @@ -1,4 +1,5 @@ import AutoStartToggle from "../AutoStartToggle"; +import StrictModeToggle from "../StrictModeToggle"; // import PomodoroTimerToggle from "../PomodoroTimerToggle"; const AllSettings = () => { @@ -7,6 +8,7 @@ const AllSettings = () => {
{/* */} +
); diff --git a/src/components/window/Reminder.tsx b/src/components/window/Reminder.tsx index 71d4ecb..db4d142 100644 --- a/src/components/window/Reminder.tsx +++ b/src/components/window/Reminder.tsx @@ -15,8 +15,15 @@ import PlainGradientAnimation from "../backgrounds/PlainGradientAnimation"; import StarryBackground from "../backgrounds/StarryBackground"; import { useTimeCountContext } from "../../contexts/TimeCountContext"; import { usePremiumFeatures } from "../../contexts/PremiumFeaturesContext"; +import Database from "@tauri-apps/plugin-sql"; +import toast, { Toaster } from "react-hot-toast"; +import { CloudDownload } from "lucide-react"; const appWindow = getCurrentWebviewWindow(); +// Define the expected result type for the select query +interface ConfigRow { + value: string; // The 'value' column in the config table is expected to be a string +} const Reminder: React.FC = () => { const { canAccessPremiumFeatures } = usePremiumFeatures(); @@ -25,6 +32,8 @@ const Reminder: React.FC = () => { const [timeLeft, setTimeLeft] = useState(20); const [reminderDuration, setReminderDuration] = useState(20); const [reminderText, setStoredReminderText] = useState(""); + const [isUsingStictMode, setIsUsingStrictMode] = useState(false); + useEffect(() => { const fetchReminderScreenInfo = async () => { const reminderStyleData = await load("ReminderThemeStyle.json"); @@ -48,7 +57,35 @@ const Reminder: React.FC = () => { setReminderDuration(storedDuration); setTimeLeft(storedDuration); } + // Load the database + const db = await Database.load("sqlite:appconfig.db"); + + // Retrieve the 'isUpdateAvailable' value from the config table + const updateAvailableResult: ConfigRow[] = await db.select( + "SELECT value FROM config WHERE key = 'isUpdateAvailable';" + ); + + // Show toast if the update is available + if ( + updateAvailableResult.length > 0 && + updateAvailableResult[0].value === "true" + ) { + toast.success("Update available!", { + duration: 2000, + position: "bottom-right", + icon: , + }); + } + + const strictModeResult: ConfigRow[] = await db.select( + "SELECT value FROM config WHERE key = 'usingStrictMode';" + ); + + if (strictModeResult.length > 0) { + setIsUsingStrictMode(strictModeResult[0].value === "true"); + } }; + fetchReminderScreenInfo(); }, []); @@ -118,21 +155,24 @@ const Reminder: React.FC = () => {
{reminderText || "Look 20 feet far away to protect your eyes."}
- + Skip this Time + + + + + )} + ); }; diff --git a/src/components/window/Settings.tsx b/src/components/window/Settings.tsx index be6ff48..46a2c73 100644 --- a/src/components/window/Settings.tsx +++ b/src/components/window/Settings.tsx @@ -5,13 +5,17 @@ import { load } from "@tauri-apps/plugin-store"; import { WebviewWindow } from "@tauri-apps/api/webviewWindow"; import { useState, useEffect } from "react"; import toast from "react-hot-toast"; +import { useTrigger } from "../../contexts/TriggerReRender"; + const Settings = () => { + const { triggerUpdate } = useTrigger(); // Access triggerUpdate from the context const [interval, setInterval] = useState(20); const [duration, setDuration] = useState(20); const [reminderText, setReminderText] = useState(""); + // Function to open the reminder window const openReminderWindow = () => { - console.log("Clicked"); + console.log("Opening reminder window..."); const webview = new WebviewWindow("ReminderWindow", { url: "/reminder", fullscreen: true, @@ -19,126 +23,108 @@ const Settings = () => { title: "Take A Break Reminder - Blink Eye", skipTaskbar: true, }); + webview.once("tauri://created", () => { - console.log("Webview created"); + console.log("Reminder window created"); }); webview.once("tauri://error", (e) => { - console.error("Error creating webview:", e); + console.error("Error creating reminder window:", e); }); }; - console.log(interval, duration, reminderText, "settings"); + + // Load settings from the store when the component mounts useEffect(() => { const fetchSettings = async () => { const store = await load("store.json", { autoSave: false }); const storedInterval = await store.get( "blinkEyeReminderInterval" ); - // const isPomodoroTimerEnabled = await store.get( - // "PomodoroStyleBreak" - // ); - // console.log(isPomodoroTimerEnabled, "is pomodoro timer enabled"); const storedDuration = await store.get( "blinkEyeReminderDuration" ); const storedReminderText = await store.get( "blinkEyeReminderScreenText" ); - if (typeof storedReminderText === "string") { - setReminderText(storedReminderText); - } - if (typeof storedInterval === "number") { - setInterval(storedInterval); - // if (isPomodoroTimerEnabled) { - // setInterval(25); - // } else { - // } - } - if (typeof storedDuration === "number") { - setDuration(storedDuration); - // if (isPomodoroTimerEnabled) { - // setDuration(300); - // } else { - // } - } - }; - console.log(interval, duration, reminderText, "settings 2"); - fetchSettings(); - console.log(interval, duration, reminderText, "settings3"); - }, []); - useEffect(() => { - let timer: number | null = null; - - const startTimer = () => { - timer = window.setInterval(() => { - openReminderWindow(); - }, interval * 60 * 1000); + if (storedInterval) setInterval(storedInterval); + if (storedDuration) setDuration(storedDuration); + if (storedReminderText) setReminderText(storedReminderText); }; - startTimer(); - - return () => { - if (timer !== null) window.clearInterval(timer); - }; - }, [interval]); + fetchSettings(); + }, []); + // Save settings to the store when the save button is clicked const handleSave = async () => { + // Input validation + if (interval <= 0) { + toast.error("Interval must be greater than 0 minutes."); + return; + } + if (duration <= 0) { + toast.error("Duration must be greater than 0 seconds."); + return; + } + const store = await load("store.json", { autoSave: false }); - const storee = await load("userScreenOnTime.json", { autoSave: false }); - await store.set("blinkEyeReminderInterval", interval); await store.set("blinkEyeReminderDuration", duration); await store.set("blinkEyeReminderScreenText", reminderText); - + await store.set("blinkEyeReminderInterval", interval); await store.save(); - toast.success("Successfully Saved the settings!", { + + triggerUpdate(); // Notify other components to refresh data + + toast.success("Successfully saved the settings!", { duration: 2000, position: "bottom-right", }); - const timeData = await storee.get("timeData"); - console.log("Saved settings:", { interval, duration }, timeData); + + console.log("Saved settings:", { interval, duration, reminderText }); }; return ( - <> -
-
- - setInterval(parseInt(e.target.value, 10) || 1)} - /> -
-
- - setDuration(parseInt(e.target.value, 10) || 1)} - /> -
-
- - setReminderText(e.target.value)} - /> -
+
+
+ + setInterval(Math.max(1, parseInt(e.target.value, 10) || 1)) // Enforce minimum value + } + /> +
+
+ + setDuration(Math.max(1, parseInt(e.target.value, 10) || 1)) // Enforce minimum value + } + /> +
+
+ + setReminderText(e.target.value.trim())} // Trim extra spaces + /> +
+
-
- +
); }; diff --git a/src/components/window/Workday.tsx b/src/components/window/Workday.tsx new file mode 100644 index 0000000..5d04a4e --- /dev/null +++ b/src/components/window/Workday.tsx @@ -0,0 +1,234 @@ +import { useState, useEffect } from "react"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import toast from "react-hot-toast"; +import { Label } from "../ui/label"; +import { Separator } from "../ui/separator"; +import { Clock } from "lucide-react"; +import { Switch } from "../ui/switch"; +import Database from "@tauri-apps/plugin-sql"; +import { useTrigger } from "../../contexts/TriggerReRender"; +import { usePremiumFeatures } from "../../contexts/PremiumFeaturesContext"; + +type Schedule = { start: string; end: string } | null; +type Workday = { [day: string]: Schedule }; + +const Workday = () => { + const { triggerUpdate } = useTrigger(); + const { canAccessPremiumFeatures } = usePremiumFeatures(); + + const [workday, setWorkday] = useState(null); + const [isWorkdayEnabled, setIsWorkdayEnabled] = useState(false); + + useEffect(() => { + const fetchConfig = async () => { + const db = await Database.load("sqlite:appconfig.db"); + + // Define a type for the query result + type ConfigResult = { value: string }; + + // Fetch the workday setup + const workdayData = (await db.select( + "SELECT value FROM config WHERE key = ?", + ["blinkEyeWorkday"] + )) as ConfigResult[]; // Specify the expected structure + + if (workdayData.length > 0 && workdayData[0].value) { + try { + const parsedWorkday = JSON.parse(workdayData[0].value) as Workday; + setWorkday(parsedWorkday); + } catch (error) { + console.error("Failed to parse workday data:", error); + } + } + + // Fetch the isWorkdayEnabled status + const isEnabledData = (await db.select( + "SELECT value FROM config WHERE key = ?", + ["isWorkdayEnabled"] + )) as ConfigResult[]; // Specify the expected structure + + if (isEnabledData.length > 0 && isEnabledData[0].value) { + setIsWorkdayEnabled(isEnabledData[0].value === "true"); + } + }; + + fetchConfig(); + }, []); + + const handleTimeChange = ( + day: string, + type: "start" | "end", + value: string + ) => { + if (!workday) return; + setWorkday((prev) => ({ + ...prev, + [day]: { + ...prev![day], + [type]: value, + } as Schedule, + })); + }; + + const toggleWorkingDay = (day: string) => { + if (!workday) return; + setWorkday((prev) => ({ + ...prev, + [day]: prev![day] ? null : { start: "09:00", end: "17:00" }, + })); + }; + + const handleSave = async () => { + if (!canAccessPremiumFeatures) { + toast.error("You need a valid license to save Workday settings.", { + duration: 2000, + position: "bottom-right", + }); + return; + } + + const db = await Database.load("sqlite:appconfig.db"); + if (workday) { + await db.execute( + "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", + ["blinkEyeWorkday", JSON.stringify(workday)] + ); + } + triggerUpdate(); + toast.success("Workday settings saved!", { + duration: 2000, + position: "bottom-right", + }); + }; + + const handleWorkdayToggle = async () => { + if (!canAccessPremiumFeatures) { + setIsWorkdayEnabled(false); // Ensure toggle is turned off + toast.error("You need a valid license to enable this feature.", { + duration: 2000, + position: "bottom-right", + }); + return; + } + + const newValue = !isWorkdayEnabled; + setIsWorkdayEnabled(newValue); + + const db = await Database.load("sqlite:appconfig.db"); + await db.execute( + "INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", + ["isWorkdayEnabled", newValue.toString()] + ); + + triggerUpdate(); + toast.success( + `Workday ${newValue ? "enabled" : "disabled"} successfully!`, + { duration: 2000, position: "bottom-right" } + ); + }; + + if (!workday) { + return
Loading...
; + } + + return ( +
+
+
+
+

Workday Setup

+

+ Configure your work hours for each day of the week.
Toggle + to set working or non-working days. +

+
+
+ + +
+
+
+ + {isWorkdayEnabled && ( +
+ {Object.entries(workday).map(([day, schedule]) => ( +
+
+

{day}

+
+ + toggleWorkingDay(day)} + /> +
+
+ {schedule ? ( +
+
+ +
+ + + handleTimeChange(day, "start", e.target.value) + } + /> +
+
+
+ +
+ + + handleTimeChange(day, "end", e.target.value) + } + /> +
+
+
+ ) : ( +

Non-working day

+ )} + +
+ ))} +
+ )} + +
+ ); +}; + +export default Workday; diff --git a/src/contexts/TriggerReRender.tsx b/src/contexts/TriggerReRender.tsx new file mode 100644 index 0000000..5c87100 --- /dev/null +++ b/src/contexts/TriggerReRender.tsx @@ -0,0 +1,35 @@ +import { createContext, useContext, useState, ReactNode } from "react"; + +// Define the context type +interface TriggerContextType { + trigger: number; // Use a number for incremental changes + triggerUpdate: () => void; // Function to trigger updates +} + +// Create the context +const TriggerContext = createContext(undefined); + +// Provider component +export const TriggerProvider = ({ children }: { children: ReactNode }) => { + const [trigger, setTrigger] = useState(0); + + // Function to update the trigger + const triggerUpdate = () => { + setTrigger((prev) => prev + 1); // Increment to ensure a change + }; + + return ( + + {children} + + ); +}; + +// Custom hook for consuming the context +export const useTrigger = () => { + const context = useContext(TriggerContext); + if (!context) { + throw new Error("useTrigger must be used within a TriggerProvider"); + } + return context; +}; diff --git a/src/hooks/useAutoUpdater.ts b/src/hooks/useAutoUpdater.ts index a8f6415..4965ebb 100644 --- a/src/hooks/useAutoUpdater.ts +++ b/src/hooks/useAutoUpdater.ts @@ -1,16 +1,29 @@ import { useState, useEffect } from "react"; import { check } from "@tauri-apps/plugin-updater"; import { relaunch } from "@tauri-apps/plugin-process"; +import Database from "@tauri-apps/plugin-sql"; export const useUpdater = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); useEffect(() => { const updateApp = async () => { - const update = await check(); - if (update) { - console.log(`Update available: ${update.version}`); - setIsUpdateAvailable(true); // Trigger alert dialog + try { + // Check for updates + const update = await check(); + if (update) { + console.log(`Update available: ${update.version}`); + setIsUpdateAvailable(true); + + const db = await Database.load("sqlite:appconfig.db"); + // Update the value in the database + await db.execute( + `UPDATE config SET value = ? WHERE key = 'isUpdateAvailable';`, + ["true"] + ); + } + } catch (error) { + console.error("Error during update check:", error); } }; @@ -18,29 +31,40 @@ export const useUpdater = () => { }, []); const handleUpdate = async () => { - const update = await check(); - if (update) { - let downloaded = 0; - let contentLength = 0; - - await update.downloadAndInstall((event) => { - switch (event.event) { - case "Started": - contentLength = event.data.contentLength ?? 0; - console.log(`Started downloading ${contentLength} bytes`); - break; - case "Progress": - downloaded += event.data.chunkLength ?? 0; - console.log(`Downloaded ${downloaded} of ${contentLength}`); - break; - case "Finished": - console.log("Download finished"); - break; - } - }); + try { + const update = await check(); + if (update) { + let downloaded = 0; + let contentLength = 0; + + await update.downloadAndInstall((event) => { + switch (event.event) { + case "Started": + contentLength = event.data.contentLength ?? 0; + console.log(`Started downloading ${contentLength} bytes`); + break; + case "Progress": + downloaded += event.data.chunkLength ?? 0; + console.log(`Downloaded ${downloaded} of ${contentLength}`); + break; + case "Finished": + console.log("Download finished"); + break; + } + }); - console.log("Update installed"); - await relaunch(); + // Update the database value after installation + const db = await Database.load("sqlite:appconfig.db"); + await db.execute( + `UPDATE config SET value = ? WHERE key = 'isUpdateAvailable';`, + ["false"] + ); + + console.log("Update installed"); + await relaunch(); + } + } catch (error) { + console.error("Error during update installation:", error); } }; diff --git a/src/main.tsx b/src/main.tsx index 921a357..1f2b4f3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,9 @@ import DefaultStartMinimize from "./components/DefaultStartMinimize"; import EncryptionComponent from "./components/EncryptionComponent"; import LicenseValidationComponent from "./components/LicenseValidationComponent"; import { PremiumFeaturesProvider } from "./contexts/PremiumFeaturesContext"; +import ConfigDataLoader from "./components/ConfigDataLoader"; +import ReminderHandler from "./components/ReminderHandler"; +import { TriggerProvider } from "./contexts/TriggerReRender"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( @@ -14,7 +17,11 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - + + + + + diff --git a/website/app/(home)/page.tsx b/website/app/(home)/page.tsx index e3452fb..ce8a5ff 100644 --- a/website/app/(home)/page.tsx +++ b/website/app/(home)/page.tsx @@ -1,8 +1,12 @@ -import DownloadApp from '@/components/download-app'; -import { FeatureGrid } from '@/components/features'; -import OpenSource from '@/components/open-source'; -import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'; -import Link from 'next/link'; +import DownloadApp from "@/components/download-app"; +import { FeatureGrid } from "@/components/features"; +import OpenSource from "@/components/open-source"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import Link from "next/link"; import { AudioLinesIcon, Calendar, @@ -14,9 +18,10 @@ import { Timer, ToggleRight, } from "lucide-react"; -import PricingSection from '@/components/pricing-section'; +import PricingSection from "@/components/pricing-section"; +import HowBlinkEyeWillHelp from "@/components/how-blink-eye-will-help"; const RootPage = () => { - return ( + return (

Blink Eye
A @@ -106,6 +111,7 @@ const RootPage = () => { ]} /> +

diff --git a/website/app/howblinkeyehelps/page.tsx b/website/app/howblinkeyehelps/page.tsx new file mode 100644 index 0000000..b73428a --- /dev/null +++ b/website/app/howblinkeyehelps/page.tsx @@ -0,0 +1,14 @@ +import HowBlinkEyeWillHelp from "@/components/how-blink-eye-will-help"; +import { Metadata } from "next"; +export const metadata: Metadata = { + title: "How Blink Eye Helps", +}; +const HowBlinkEyeHelpsPage = () => { + return ( + <> + + + ); +}; + +export default HowBlinkEyeHelpsPage; diff --git a/website/app/sitemap.ts b/website/app/sitemap.ts index 3e2ddfc..4cfb10a 100644 --- a/website/app/sitemap.ts +++ b/website/app/sitemap.ts @@ -34,6 +34,7 @@ const sitemap = async (): Promise => { "/privacy", "/pricing", "/changelog", + "/howblinkeyehelps", ]; // Fetch release data from GitHub API diff --git a/website/bun.lockb b/website/bun.lockb index 70b03ca..b2def9c 100755 Binary files a/website/bun.lockb and b/website/bun.lockb differ diff --git a/website/components/download-app.tsx b/website/components/download-app.tsx index 45bc73c..c08be69 100644 --- a/website/components/download-app.tsx +++ b/website/components/download-app.tsx @@ -33,8 +33,8 @@ const DownloadApp = async () => { return (
-

- Download Now +

+ Download Free & Start Now

{/* Platform Row */} diff --git a/website/components/features.tsx b/website/components/features.tsx index a652de0..98fce7d 100644 --- a/website/components/features.tsx +++ b/website/components/features.tsx @@ -31,10 +31,10 @@ export function FeatureGrid(props: { className="container space-y-6 py-8 md:py-12 lg:py-24" >
-

+

{props.title}

-

+

{props.subtitle}

diff --git a/website/components/how-blink-eye-will-help.tsx b/website/components/how-blink-eye-will-help.tsx new file mode 100644 index 0000000..794ef70 --- /dev/null +++ b/website/components/how-blink-eye-will-help.tsx @@ -0,0 +1,239 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + EyeIcon, + ActivityIcon, + BrainIcon, + ZapIcon, + HeartIcon, + ClockIcon, + SettingsIcon, + BookOpenIcon, + SunIcon, +} from "lucide-react"; + +export default function HowBlinkEyeWillHelp() { + // const [openAccordion, setOpenAccordion] = useState(null); + + const benefits = [ + { + title: "Eye Care and Protection", + description: + "Protect your eyes from digital strain, reduce blue light exposure, and prevent Computer Vision Syndrome (CVS).", + icon: EyeIcon, + details: [ + { + subtitle: "Prevent Digital Eye Strain", + content: + "Scheduled breaks reduce symptoms like dry eyes, blurry vision, and headaches, ensuring long-term eye health.", + }, + { + subtitle: "Blue Light Protection", + content: + "Rest your eyes regularly with prompts designed to combat the harmful effects of blue light exposure.", + }, + ], + }, + { + title: "Physical Health", + description: + "Improve posture, prevent Repetitive Strain Injuries (RSI), and reduce physical discomfort.", + icon: ActivityIcon, + details: [ + { + subtitle: "Prevent Repetitive Strain Injuries (RSI)", + content: + "Regular breaks minimize strain on your hands and wrists, preventing long-term damage from repetitive tasks.", + }, + { + subtitle: "Support Better Posture", + content: + "Encourages stretching and posture checks to avoid issues like tech neck and back pain.", + }, + ], + }, + { + title: "Mental Wellness", + description: + "Boost your focus, mindfulness, and reduce stress effortlessly.", + icon: BrainIcon, + details: [ + { + subtitle: "Enhance Focus and Mindfulness", + content: + "Regular breaks help you stay sharp and focused while reducing mental fatigue and burnout.", + }, + { + subtitle: "Stress Management", + content: + "Use break intervals to practice relaxation techniques, reducing work-induced stress and tension.", + }, + ], + }, + { + title: "Boosted Productivity", + description: + "Work smarter, not harder, with productivity-focused methods.", + icon: ZapIcon, + details: [ + { + subtitle: "Pomodoro Technique Integration", + content: + "Break tasks into manageable intervals for efficient work sessions, improving task completion rates.", + }, + { + subtitle: "Cognitive Efficiency", + content: + "Strategic breaks enhance your mental performance, helping you achieve more with less effort.", + }, + ], + }, + { + title: "Healthy Screen-Time Habits", + description: + "Develop disciplined screen-time practices and minimize unnecessary distractions.", + icon: HeartIcon, + details: [ + { + subtitle: "Structured Screen-Time", + content: + "Build healthy digital habits with well-timed reminders for work and relaxation balance.", + }, + { + subtitle: "Reduce Multitasking", + content: + "Focus fully on tasks by minimizing distractions during your work and relaxation cycles.", + }, + ], + }, + { + title: "Sustainable Long-Term Benefits", + description: + "Avoid chronic issues and maintain balance for years to come.", + icon: ClockIcon, + details: [ + { + subtitle: "Prevent Chronic Vision Problems", + content: + "Take proactive measures to reduce risks of long-term digital vision problems caused by screen overuse.", + }, + { + subtitle: "Achieve Work-Life Balance", + content: + "Create sustainable habits for work and personal life with a balanced approach to productivity.", + }, + ], + }, + { + title: "Flexible and Customizable", + description: + "Tailor your experience to fit your lifestyle, profession, and individual needs.", + icon: SettingsIcon, + details: [ + { + subtitle: "Customizable Break Timers", + content: + "Set intervals that match your workflow, ensuring the app complements your unique schedule.", + }, + { + subtitle: "Built for Everyone", + content: + "Whether you’re a student, developer, designer, or remote worker, the app adapts to your needs.", + }, + ], + }, + { + title: "Research-Backed Benefits", + description: + "Enjoy features grounded in productivity and wellness studies.", + icon: BookOpenIcon, + details: [ + { + subtitle: "Evidence-Based Features", + content: + "The app’s design is inspired by research in productivity, focus, and digital well-being.", + }, + { + subtitle: "Trusted by Users", + content: + "Real user testimonials and success stories validate its effectiveness and value.", + }, + ], + }, + { + title: "Enhanced Energy and Mood", + description: + "Feel energized and stay positive throughout the day with balanced work breaks.", + icon: SunIcon, + details: [ + { + subtitle: "Boost Daily Energy", + content: + "Short, regular breaks help recharge your mind and body, keeping you energized for the day ahead.", + }, + { + subtitle: "Maintain a Positive Mood", + content: + "Break the monotony of continuous screen time to reduce frustration and enhance your overall mood.", + }, + ], + }, + ]; + + return ( +
+
+

+ How Blink Eye Will Help You +

+
+

+ Take control of your well-being with Blink Eye. Discover how + scheduled breaks can improve eye health, enhance focus, boost + productivity, and promote long-term wellness for a balanced digital + lifestyle. +

+
+ {benefits.map((benefit, index) => ( + + +
+ +
+ {benefit.title} + {benefit.description} +
+ + + + Learn More + + {benefit.details.map((detail, detailIndex) => ( +
+

+ {detail.subtitle} +

+

{detail.content}

+
+ ))} +
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/website/components/layout/footer.tsx b/website/components/layout/footer.tsx index f5b8209..bd846fa 100644 --- a/website/components/layout/footer.tsx +++ b/website/components/layout/footer.tsx @@ -12,6 +12,7 @@ const routes = [ "/goodbye", "/pricing", "/changelog", + "/howblinkeyehelps", ]; export const Footer = () => { const currentYear = getCurrentYear(); diff --git a/website/components/pricing-section.tsx b/website/components/pricing-section.tsx index 0719672..6cc0ac1 100644 --- a/website/components/pricing-section.tsx +++ b/website/components/pricing-section.tsx @@ -154,9 +154,9 @@ export default function PricingSection({ return (
-

+

Choose the right plan for you -

+

Choose an affordable plan that's packed with the best features for diff --git a/website/components/ui/accordion.tsx b/website/components/ui/accordion.tsx new file mode 100644 index 0000000..6d6ec0e --- /dev/null +++ b/website/components/ui/accordion.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/utils/cn" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +

{children}
+ +)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/website/components/ui/card.tsx b/website/components/ui/card.tsx new file mode 100644 index 0000000..b874cd7 --- /dev/null +++ b/website/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/utils/cn" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/website/configs/seo.ts b/website/configs/seo.ts index 228f86b..dc3edaf 100644 --- a/website/configs/seo.ts +++ b/website/configs/seo.ts @@ -1,8 +1,12 @@ export const SEO = { - title: "Blink Eye - Best Free Eye Care & Break Timer for Mac, Windows, Linux", + title: "Blink Eye - Best Eye Care & Break Timer for Mac, Windows, Linux", description: "A minimalist eye care reminder app to reduce eye strain, featuring customizable timers, full-screen popups, audio mute.", keywords: [ + "prevent rsi app", + "prevent cvs app", + "rsi app", + "cvs app", "best eye care app 2024", "screen break reminder app", "free eye health app download", @@ -37,9 +41,7 @@ export const SEO = { "eye care reminder app for Linux", "blink eye", "blink eye for windows", - "Noman Dhoni", "Noman Dhoni Made App", - "nomandhoni", "eye care app for windows", "20 20 20 rule app for windows", "eye care", @@ -53,44 +55,17 @@ export const SEO = { "eye care tips", "eye exercises app", "eye protection app", - "blink eye alternative", - "eye care app for android", - "eye care app for ios", "eye care app for pc", "eye care app for laptop", "eye care app for desktop", - "eye care app for chrome", - "eye care app for firefox", - "eye care app for safari", - "eye care app for edge", "eye care app for windows 10", "eye care app for windows 11", - "eye care app for windows 8", - "eye care app for windows 7", - "eye care app for windows xp", - "eye care app for windows vista", - "eye care app for windows 2000", - "eye care app for windows 98", - "eye care app for windows 95", - "eye care app for windows 3.1", - "eye care app for windows nt", - "eye care app for windows me", - "eye care app for windows ce", "blink eye download", "blink eye review", "eye care app for windows", "20 20 20 rule app for windows", - "eye care reminder app", - "eye care software", - "eye care tips", - "eye exercises app", - "eye protection app", - "eye care", - "eye health app", - "20 20 20 rule app", ], - thumb: - "https://repository-images.githubusercontent.com/749625079/db502010-82d3-4004-8e01-283d20915ee0", + thumb: "https://utfs.io/f/93hqarYp4cDdRfdEFOs3IvZkCG1g7rYl8WhFVBbNozK265eA", url: "https://blinkeye.vercel.app", twitter: "@blinkeyeapp", }; diff --git a/website/package.json b/website/package.json index 8c6151c..71d59fd 100644 --- a/website/package.json +++ b/website/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@next/third-parties": "^15.0.3", + "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", "@radix-ui/react-select": "^2.1.2", diff --git a/website/tailwind.config.ts b/website/tailwind.config.ts index f407f23..fd80e11 100644 --- a/website/tailwind.config.ts +++ b/website/tailwind.config.ts @@ -5,70 +5,78 @@ const config = { content: ['./pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}'], prefix: '', theme: { - container: { - center: true, - padding: '2rem', - screens: { - '2xl': '1400px', - }, - }, - extend: { - colors: { - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - keyframes: { - 'accordion-down': { - from: { height: '0' }, - to: { height: 'var(--radix-accordion-content-height)' }, - }, - 'accordion-up': { - from: { height: 'var(--radix-accordion-content-height)' }, - to: { height: '0' }, - }, - }, - animation: { - 'accordion-down': 'accordion-down 0.2s ease-out', - 'accordion-up': 'accordion-up 0.2s ease-out', - }, - }, - }, + container: { + center: true, + padding: '2rem', + screens: { + '2xl': '1400px' + } + }, + extend: { + colors: { + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + keyframes: { + 'accordion-down': { + from: { + height: '0' + }, + to: { + height: 'var(--radix-accordion-content-height)' + } + }, + 'accordion-up': { + from: { + height: 'var(--radix-accordion-content-height)' + }, + to: { + height: '0' + } + } + }, + animation: { + 'accordion-down': 'accordion-down 0.2s ease-out', + 'accordion-up': 'accordion-up 0.2s ease-out' + } + } + }, plugins: [require('tailwindcss-animate')], } satisfies Config;