From 4bbf3a7eabaa729bcf58d47da86a29be707738c9 Mon Sep 17 00:00:00 2001 From: Sekwah Date: Wed, 8 Nov 2023 03:02:23 +0000 Subject: [PATCH] feat(tauri): updater window --- .github/workflows/build-tauri.yml | 30 ++- .github/workflows/release-please.yml | 54 ++--- .github/workflows/test-build-thingy.yml | 30 --- CHANGELOG.md | 7 +- app/renderer/src/App.tsx | 12 +- app/renderer/src/components/NavNotify.tsx | 18 ++ app/renderer/src/components/Navigation.tsx | 39 ++-- app/renderer/src/components/Titlebar.tsx | 5 +- app/renderer/src/components/Updater.tsx | 116 +++++++++++ app/renderer/src/components/index.ts | 2 + app/renderer/src/config.ts | 12 +- .../src/contexts/ConnnectorContext.tsx | 19 +- app/renderer/src/contexts/InvokeConnector.tsx | 6 + .../contexts/connectors/ElectronConnector.tsx | 9 + .../contexts/connectors/TauriConnector.tsx | 43 +++- app/renderer/src/routes/Settings/index.tsx | 12 +- app/renderer/src/store/settings/actions.ts | 10 + app/renderer/src/store/settings/reducer.ts | 7 + app/renderer/src/store/settings/types.ts | 5 + app/renderer/src/store/store.ts | 2 + app/renderer/src/store/update/actions.ts | 24 +++ app/renderer/src/store/update/index.ts | 3 + app/renderer/src/store/update/reducer.ts | 38 ++++ app/renderer/src/store/update/types.ts | 13 ++ .../src/styles/components/navigation.ts | 20 +- .../src/styles/routes/tasks/details.ts | 2 + app/shareables/src/index.ts | 6 +- app/tauri/.gitignore | 1 - app/tauri/Cargo.lock | 78 ++++++- app/tauri/Cargo.toml | 3 +- app/tauri/README.MD | 2 +- app/tauri/release-prep/release-prep.js | 2 +- app/tauri/release.conf.json | 4 +- app/tauri/src/commands.rs | 5 +- app/tauri/src/global_shortcuts.rs | 51 ++++- app/tauri/src/main.rs | 3 +- app/tauri/src/updater.rs | 197 ++++++++++++------ package.json | 4 +- 38 files changed, 719 insertions(+), 175 deletions(-) delete mode 100644 .github/workflows/test-build-thingy.yml create mode 100644 app/renderer/src/components/NavNotify.tsx create mode 100644 app/renderer/src/components/Updater.tsx create mode 100644 app/renderer/src/contexts/InvokeConnector.tsx create mode 100644 app/renderer/src/store/update/actions.ts create mode 100644 app/renderer/src/store/update/index.ts create mode 100644 app/renderer/src/store/update/reducer.ts create mode 100644 app/renderer/src/store/update/types.ts diff --git a/.github/workflows/build-tauri.yml b/.github/workflows/build-tauri.yml index eb15e6c7..f9beabff 100644 --- a/.github/workflows/build-tauri.yml +++ b/.github/workflows/build-tauri.yml @@ -34,7 +34,11 @@ jobs: # The only real added benefit of NSIS is that the installer can have a custom logo. # Also, nsis is the only one that currently works on arm64 # Another note is embedBootstrapper is enabled to improve support on window 7. Though windows 7 doesn't support arm64. - tauri_target: ["'aarch64-pc-windows-msvc --bundles nsis,updater'", "'x86_64-pc-windows-msvc --bundles msi,updater'"] + tauri_target: + [ + "'aarch64-pc-windows-msvc --bundles nsis,updater'", + "'x86_64-pc-windows-msvc --bundles msi,updater'", + ] steps: - uses: actions/checkout@v4 @@ -50,7 +54,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './app/tauri -> target' + workspaces: "./app/tauri -> target" - name: install dependencies (ubuntu only) if: matrix.os == 'ubuntu-22.04' run: | @@ -88,13 +92,23 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - name: Download all artifacts + # Download-artifact cannot do a wildcard and if all are downloaded it can download the results from the electron stage + # We could filter that but this is faster to avoid unneeded downloads. + - name: Download Win uses: actions/download-artifact@v3 with: - path: artifacts - - name: Display structure of downloaded files - shell: bash - run: ls -R + name: tauri-win + path: artifacts/tauri-win + - name: Download Mac + uses: actions/download-artifact@v3 + with: + name: tauri-mac + path: artifacts/tauri-mac + - name: Download Linux + uses: actions/download-artifact@v3 + with: + name: tauri-linux + path: artifacts/tauri-linux - name: Use Node.js 18.x uses: actions/setup-node@v3 with: @@ -106,4 +120,4 @@ jobs: with: name: tauri-release path: | - release/* \ No newline at end of file + release/* diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 1c41b655..a2c8497a 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -30,6 +30,7 @@ jobs: needs: release-please if: needs.release-please.outputs.release_created uses: ./.github/workflows/build-tauri.yml + secrets: inherit publish: name: Publish needs: @@ -57,28 +58,31 @@ jobs: built-*/*.exe built-*/*.snap built-*/*.blockmap - publish-to-homebrew-cask: - name: Publish to Homebrew Cask - needs: - - publish - - release-please - runs-on: macos-latest - steps: - - uses: Homebrew/actions/bump-packages@master - with: - casks: pomatez - token: ${{ secrets.GITHUB_TOKEN }} - publish-to-winget: - name: Publish to WinGet - needs: - - publish - - release-please - runs-on: windows-latest - steps: - - uses: vedantmgoyal2009/winget-releaser@v2 - with: - identifier: Zidoro.Pomatez - installers-regex: 'setup\.exe$' - max-versions-to-keep: 5 # keep only latest 5 versions - release-tag: ${{ needs.release-please.outputs.tag }} - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + tauri-release/* + +# Commented out just for test releases +# publish-to-homebrew-cask: +# name: Publish to Homebrew Cask +# needs: +# - publish +# - release-please +# runs-on: macos-latest +# steps: +# - uses: Homebrew/actions/bump-packages@master +# with: +# casks: pomatez +# token: ${{ secrets.GITHUB_TOKEN }} +# publish-to-winget: +# name: Publish to WinGet +# needs: +# - publish +# - release-please +# runs-on: windows-latest +# steps: +# - uses: vedantmgoyal2009/winget-releaser@v2 +# with: +# identifier: Zidoro.Pomatez +# installers-regex: 'setup\.exe$' +# max-versions-to-keep: 5 # keep only latest 5 versions +# release-tag: ${{ needs.release-please.outputs.tag }} +# token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test-build-thingy.yml b/.github/workflows/test-build-thingy.yml deleted file mode 100644 index 6d06e9b3..00000000 --- a/.github/workflows/test-build-thingy.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Release -on: - push: - workflow_call: - workflow_dispatch: - -permissions: - contents: write - pull-requests: write -jobs: - build-node: - name: "Build Node" - uses: ./.github/workflows/build.yml - build-tauri: - name: "Build Tauri" - uses: ./.github/workflows/build-tauri.yml - secrets: inherit - publish: - name: Publish - needs: - - build-node - - build-tauri - runs-on: ubuntu-latest - steps: - - name: Download artifact - uses: actions/download-artifact@v3 - - name: Display structure of downloaded files - shell: bash - run: ls -R - diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c9c7ef..8563ce0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,11 @@ ## [1.3.1](https://github.com/zidoro/pomatez/compare/v1.3.0...v1.3.1) (2023-10-19) - ### Bug Fixes 🐛 -* javascript error on launch ([#414](https://github.com/zidoro/pomatez/issues/414)) ([c6c18fb](https://github.com/zidoro/pomatez/commit/c6c18fb47b424be62a9b91ed64c7c95e8eaa41a3)) -* **lang:** Switch "released notes" with "release notes" ([#439](https://github.com/zidoro/pomatez/issues/439)) ([d9a3afa](https://github.com/zidoro/pomatez/commit/d9a3afa11f828084483c1d1e3693ff9b0dc1c8e1)) -* toast notification ([#382](https://github.com/zidoro/pomatez/issues/382)) ([25403d7](https://github.com/zidoro/pomatez/commit/25403d742d83d0d3654418a43bc5efe8316dc019)) +- javascript error on launch ([#414](https://github.com/zidoro/pomatez/issues/414)) ([c6c18fb](https://github.com/zidoro/pomatez/commit/c6c18fb47b424be62a9b91ed64c7c95e8eaa41a3)) +- **lang:** Switch "released notes" with "release notes" ([#439](https://github.com/zidoro/pomatez/issues/439)) ([d9a3afa](https://github.com/zidoro/pomatez/commit/d9a3afa11f828084483c1d1e3693ff9b0dc1c8e1)) +- toast notification ([#382](https://github.com/zidoro/pomatez/issues/382)) ([25403d7](https://github.com/zidoro/pomatez/commit/25403d742d83d0d3654418a43bc5efe8316dc019)) ## [1.3.0](https://github.com/zidoro/pomatez/compare/v1.2.3...v1.3.0) (2023-09-26) diff --git a/app/renderer/src/App.tsx b/app/renderer/src/App.tsx index 33191497..c3608513 100644 --- a/app/renderer/src/App.tsx +++ b/app/renderer/src/App.tsx @@ -35,6 +35,16 @@ export default function App() { document.removeEventListener("contextmenu", contextEvent); }, []); + useEffect(() => { + const middleMouseEvent = (event: MouseEvent) => { + if (event.button === 1) event.preventDefault(); + }; + window.addEventListener("auxclick", middleMouseEvent); + + return () => + window.removeEventListener("auxclick", middleMouseEvent); + }, []); + return ( @@ -54,7 +64,7 @@ export default function App() { /> ) ) - : routes.map( + : routes().map( ({ exact, path, component }, index) => ( = ({ timerType }) => { (state: AppStateTypes) => state.settings ); + const state = useSelector((state: AppStateTypes) => state); + return ( - {routes.map(({ name, icon, exact, path }, index) => ( - - - - {name} - - - ))} + {routes(state).map( + ({ name, icon, exact, path, notify }, index) => { + return ( + + + + + {notify && } + + {name} + + + ); + } + )} ); diff --git a/app/renderer/src/components/Titlebar.tsx b/app/renderer/src/components/Titlebar.tsx index e9603a66..ef4024a3 100644 --- a/app/renderer/src/components/Titlebar.tsx +++ b/app/renderer/src/components/Titlebar.tsx @@ -27,9 +27,8 @@ type Props = { }; const Titlebar: React.FC = ({ darkMode, timerType }) => { - const { onMinimizeCallback, onExitCallback } = useContext( - ConnnectorContext - ); + const { onMinimizeCallback, onExitCallback } = + useContext(ConnnectorContext); const getAppIcon = useCallback(() => { switch (timerType) { diff --git a/app/renderer/src/components/Updater.tsx b/app/renderer/src/components/Updater.tsx new file mode 100644 index 00000000..8a162db6 --- /dev/null +++ b/app/renderer/src/components/Updater.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import Header from "./Header"; +import styled from "styled-components/macro"; +import ReactMarkdown from "react-markdown"; +import { useDispatch, useSelector } from "react-redux"; +import { AppStateTypes, setIgnoreUpdate, SettingTypes } from "../store"; +import { + setUpdateBody, + setUpdateVersion, + UpdateTypes, +} from "../store/update"; +import { + StyledButtonNormal, + StyledButtonPrimary, + StyledDescriptionPreviewer, + StyledTaskForm, +} from "../styles"; +import { getInvokeConnector } from "../contexts"; +import { INSTALL_UPDATE } from "@pomatez/shareables"; +import { useNotification } from "../hooks"; +import notificationIcon from "../assets/logos/notification-dark.png"; + +const UpdateWrapper = styled.div` + display: flex; + flex-direction: column; + flex: 1; + width: 100%; + padding-left: 2rem; + padding-right: 1.4rem; +`; + +// Extend StyledDescriptionPreviewer to make it taller +const UpdateDescriptionPreviewer = styled(StyledDescriptionPreviewer)` + flex: 1 1 0; // Flex properties to allow growth and shrinkage + overflow-y: auto; // Enable vertical scrolling + height: 100%; // Set a fixed height + display: flex; + flex-direction: column; + min-height: 0; + max-height: none; +`; + +const ActionButtons = styled.div` + padding: 1rem 0; +`; +const IgnoreVersion = styled.div` + width: 100%; + text-align: center; + margin-top: 10px; + cursor: pointer; +`; + +const Updater: React.FC = () => { + const settings: SettingTypes = useSelector( + (state: AppStateTypes) => state.settings + ); + const update: UpdateTypes = useSelector( + (state: AppStateTypes) => state.update + ); + + const dispatch = useDispatch(); + + return ( + +
+ + + + + + + { + const invokeConnector = getInvokeConnector(); + new window.Notification( + "Downloading & Installing Update", + { + body: `This may take a moment to start depending on your internet speed.`, + } + ); + invokeConnector?.send(INSTALL_UPDATE); + dispatch(setUpdateVersion("")); + dispatch(setUpdateBody("")); + }} + > + Install Now + + { + dispatch(setUpdateVersion("")); + dispatch(setUpdateBody("")); + }} + > + Remind Me Later + + + { + dispatch(setIgnoreUpdate(update.updateVersion)); + dispatch(setUpdateBody("")); + }} + > + Ignore This Version + + + + ); +}; + +export default React.memo(Updater); diff --git a/app/renderer/src/components/index.ts b/app/renderer/src/components/index.ts index 13a90070..fc701198 100644 --- a/app/renderer/src/components/index.ts +++ b/app/renderer/src/components/index.ts @@ -23,6 +23,8 @@ export { default as Radio } from "./Radio"; export { default as Collapse } from "./Collapse"; export { default as Alert } from "./Alert"; +export { default as Updater } from "./Updater"; +export { default as NavNotify } from "./NavNotify"; export * from "./Popper"; export * from "./Preloader"; diff --git a/app/renderer/src/config.ts b/app/renderer/src/config.ts index a8b43bdb..19958fe2 100644 --- a/app/renderer/src/config.ts +++ b/app/renderer/src/config.ts @@ -1,6 +1,8 @@ import { SVGTypes } from "components"; import { TaskList, Config, Timer, Settings } from "routes"; import { ConfigSliderProps } from "routes"; +import { AppStateTypes, SettingTypes } from "./store"; +import { DefaultRootState } from "react-redux"; export const APP_NAME = "Pomatez"; @@ -10,15 +12,19 @@ type NavItemTypes = { exact: boolean; path: string; component: React.FC; + notify: boolean; }; -export const routes: NavItemTypes[] = [ +export const routes: (state?: AppStateTypes) => NavItemTypes[] = ( + state +) => [ { icon: "task", name: "Task List", exact: false, path: "/task-list", component: TaskList, + notify: false, }, { icon: "config", @@ -26,6 +32,7 @@ export const routes: NavItemTypes[] = [ exact: true, path: "/config", component: Config, + notify: false, }, { icon: "timer", @@ -33,6 +40,7 @@ export const routes: NavItemTypes[] = [ exact: true, path: "/", component: Timer, + notify: false, }, { icon: "settings", @@ -40,6 +48,7 @@ export const routes: NavItemTypes[] = [ exact: true, path: "/settings", component: Settings, + notify: !!state?.update?.updateBody, }, ]; @@ -50,6 +59,7 @@ export const compactRoutes: NavItemTypes[] = [ exact: false, path: "/", component: Timer, + notify: false, }, ]; diff --git a/app/renderer/src/contexts/ConnnectorContext.tsx b/app/renderer/src/contexts/ConnnectorContext.tsx index 40130d47..a270218e 100644 --- a/app/renderer/src/contexts/ConnnectorContext.tsx +++ b/app/renderer/src/contexts/ConnnectorContext.tsx @@ -1,7 +1,13 @@ import React from "react"; import isElectron from "is-electron"; -import { ElectronConnectorProvider } from "./connectors/ElectronConnector"; -import { TauriConnectorProvider } from "./connectors/TauriConnector"; +import { + ElectronConnectorProvider, + ElectronInvokeConnector, +} from "./connectors/ElectronConnector"; +import { + TauriConnectorProvider, + TauriInvokeConnector, +} from "./connectors/TauriConnector"; export type ConnectorProps = { onMinimizeCallback?: () => void; @@ -13,6 +19,15 @@ export const ConnnectorContext = React.createContext( {} ); +export function getInvokeConnector() { + if (isElectron()) { + return ElectronInvokeConnector; + } else if (window.__TAURI__) { + return TauriInvokeConnector; + } + return undefined; +} + export const ConnectorProvider: React.FC = ({ children }) => { let Connector: React.FC = () => <>{children}; if (isElectron()) { diff --git a/app/renderer/src/contexts/InvokeConnector.tsx b/app/renderer/src/contexts/InvokeConnector.tsx new file mode 100644 index 00000000..6974ac0d --- /dev/null +++ b/app/renderer/src/contexts/InvokeConnector.tsx @@ -0,0 +1,6 @@ +/** + * Explicitly for calling invokes from the trigger rather than a setting change. + */ +export type InvokeConnector = { + send: (event: string, ...payload: any) => void; +}; diff --git a/app/renderer/src/contexts/connectors/ElectronConnector.tsx b/app/renderer/src/contexts/connectors/ElectronConnector.tsx index ae4a191c..ffbd956c 100644 --- a/app/renderer/src/contexts/connectors/ElectronConnector.tsx +++ b/app/renderer/src/contexts/connectors/ElectronConnector.tsx @@ -17,6 +17,15 @@ import { } from "@pomatez/shareables"; import { encodeSvg } from "../../utils"; import { TraySVG } from "../../components"; +import { InvokeConnector } from "../InvokeConnector"; + +export const ElectronInvokeConnector: InvokeConnector = { + send: (event: string, ...payload: any) => { + const { electron } = window; + + electron.send(event, ...payload); + }, +}; export const ElectronConnectorProvider: React.FC = ({ children }) => { const { electron } = window; diff --git a/app/renderer/src/contexts/connectors/TauriConnector.tsx b/app/renderer/src/contexts/connectors/TauriConnector.tsx index 49a3ce4a..530d7ff3 100644 --- a/app/renderer/src/contexts/connectors/TauriConnector.tsx +++ b/app/renderer/src/contexts/connectors/TauriConnector.tsx @@ -1,9 +1,10 @@ import React, { useCallback, useContext, useEffect } from "react"; import { ConnnectorContext } from "../ConnnectorContext"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { AppStateTypes, SettingTypes } from "../../store"; import { CounterContext } from "../CounterContext"; import { + CHECK_FOR_UPDATES, SET_ALWAYS_ON_TOP, SET_CLOSE, SET_COMPACT_MODE, @@ -13,13 +14,26 @@ import { SET_SHOW, SET_UI_THEME, TRAY_ICON_UPDATE, + UPDATE_AVAILABLE, } from "@pomatez/shareables"; import { encodeSvg } from "../../utils"; import { TraySVG } from "../../components"; import { enable, disable } from "@tauri-apps/plugin-autostart"; import { invoke } from "@tauri-apps/api/primitives"; +import { listen } from "@tauri-apps/api/event"; +import { setUpdateBody, setUpdateVersion } from "../../store/update"; + +export const TauriInvokeConnector = { + send: (event: string, ...payload: any) => { + invoke(event.toLowerCase(), ...payload).catch((err) => + console.error(err) + ); + }, +}; export const TauriConnectorProvider: React.FC = ({ children }) => { + const dispatch = useDispatch(); + const settings: SettingTypes = useSelector( (state: AppStateTypes) => state.settings ); @@ -41,7 +55,7 @@ export const TauriConnectorProvider: React.FC = ({ children }) => { }, []); /** - * Rust uses lowercase snake_case for function names so we need to convert to lower case for the calls. + * Rust uses lowercase snake_case for function names, so we need to convert to lower case for the calls. * @param event * @param payload */ @@ -144,6 +158,31 @@ export const TauriConnectorProvider: React.FC = ({ children }) => { } }, [send, timer.playing, timerType, dashOffset]); + // Workaround to make sure it only calls once on mount + const checkUpdate = useCallback(() => { + send(CHECK_FOR_UPDATES, { + ignoreVersion: settings.ignoreUpdate || "", + }); + }, [send, settings.ignoreUpdate]); + + useEffect(() => { + checkUpdate(); + }, [checkUpdate]); + + useEffect(() => { + const unlisten = listen<{ body: string; version: string }>( + UPDATE_AVAILABLE, + (updateInfo) => { + console.log("Update Info", updateInfo.payload); + dispatch(setUpdateVersion(updateInfo?.payload?.version)); + dispatch(setUpdateBody(updateInfo?.payload?.body)); + } + ); + return () => { + unlisten.then((f) => f()); + }; + }, [dispatch]); + return ( state.update + ); + const [alert, setAlert] = useState(alertState); - return ( + return update.updateBody ? ( + + ) : ( {alert === null && ( diff --git a/app/renderer/src/store/settings/actions.ts b/app/renderer/src/store/settings/actions.ts index 3e5fcb71..5d0097c3 100644 --- a/app/renderer/src/store/settings/actions.ts +++ b/app/renderer/src/store/settings/actions.ts @@ -1,6 +1,7 @@ import { SettingTypes, SettingActionTypes, + IGNORE_UPDATE, ALWAYS_ON_TOP, RESTORE_DEFAULT_SETTINGS, ENABLE_DARK_THEME, @@ -18,6 +19,15 @@ import { OPEN_AT_LOGIN, } from "./types"; +export const setIgnoreUpdate = ( + ignoreUpdate: SettingTypes["ignoreUpdate"] +): SettingActionTypes => { + return { + type: IGNORE_UPDATE, + payload: ignoreUpdate, + }; +}; + export const setAlwaysOnTop = ( alwaysOnTop: SettingTypes["alwaysOnTop"] ): SettingActionTypes => { diff --git a/app/renderer/src/store/settings/reducer.ts b/app/renderer/src/store/settings/reducer.ts index 1580ba33..8b7d99a3 100644 --- a/app/renderer/src/store/settings/reducer.ts +++ b/app/renderer/src/store/settings/reducer.ts @@ -17,11 +17,13 @@ import { ENABLE_VOICE_ASSISTANCE, ENABLE_COMPACT_MODE, OPEN_AT_LOGIN, + IGNORE_UPDATE, } from "./types"; const defaultSettings: SettingTypes = { alwaysOnTop: false, compactMode: false, + ignoreUpdate: "", enableFullscreenBreak: false, enableStrictMode: false, enableDarkTheme: isPreferredDark(), @@ -47,6 +49,11 @@ export const settingReducer = ( action: SettingActionTypes ) => { switch (action.type) { + case IGNORE_UPDATE: + return { + ...state, + ignoreUpdate: action.payload, + }; case ALWAYS_ON_TOP: return { ...state, diff --git a/app/renderer/src/store/settings/types.ts b/app/renderer/src/store/settings/types.ts index 37b137e8..225c88ba 100644 --- a/app/renderer/src/store/settings/types.ts +++ b/app/renderer/src/store/settings/types.ts @@ -1,6 +1,7 @@ const settings = "[settings]"; export type SettingTypes = { + ignoreUpdate: string; alwaysOnTop: boolean; compactMode: boolean; enableFullscreenBreak: boolean; @@ -17,6 +18,10 @@ export type SettingTypes = { openAtLogin: boolean; }; +/** + * the reason this is stored inside the settings and not the update is because we want to actually store this and remember it + */ +export const IGNORE_UPDATE = `${settings} IGNORE_UPDATE`; export const ALWAYS_ON_TOP = `${settings} ALWAYS_ON_TOP`; export const ENABLE_DARK_THEME = `${settings} ENABLE_DARK_THEME`; diff --git a/app/renderer/src/store/store.ts b/app/renderer/src/store/store.ts index ad8c1f28..422ec887 100644 --- a/app/renderer/src/store/store.ts +++ b/app/renderer/src/store/store.ts @@ -7,12 +7,14 @@ import { configReducer } from "./config"; import { settingReducer } from "./settings"; import { timerReducer } from "./timer"; import { undoableTasksReducer } from "./tasks"; +import { updateReducer } from "./update"; const rootReducer = combineReducers({ config: configReducer, settings: settingReducer, timer: timerReducer, tasks: undoableTasksReducer, + update: updateReducer, }); export type AppStateTypes = ReturnType; diff --git a/app/renderer/src/store/update/actions.ts b/app/renderer/src/store/update/actions.ts new file mode 100644 index 00000000..0eaeb36c --- /dev/null +++ b/app/renderer/src/store/update/actions.ts @@ -0,0 +1,24 @@ +import { + UpdateTypes, + UpdateActionTypes, + UPDATE_BODY, + UPDATE_VERSION, +} from "./types"; + +export const setUpdateBody = ( + updateBody: UpdateTypes["updateBody"] +): UpdateActionTypes => { + return { + type: UPDATE_BODY, + payload: updateBody, + }; +}; + +export const setUpdateVersion = ( + updateVersion: UpdateTypes["updateVersion"] +): UpdateActionTypes => { + return { + type: UPDATE_VERSION, + payload: updateVersion, + }; +}; diff --git a/app/renderer/src/store/update/index.ts b/app/renderer/src/store/update/index.ts new file mode 100644 index 00000000..6d30c416 --- /dev/null +++ b/app/renderer/src/store/update/index.ts @@ -0,0 +1,3 @@ +export * from "./actions"; +export * from "./reducer"; +export * from "./types"; diff --git a/app/renderer/src/store/update/reducer.ts b/app/renderer/src/store/update/reducer.ts new file mode 100644 index 00000000..b762586e --- /dev/null +++ b/app/renderer/src/store/update/reducer.ts @@ -0,0 +1,38 @@ +import { getFromStorage, isPreferredDark, detectOS } from "utils"; +import { + UpdateTypes, + UpdateActionTypes, + UPDATE_BODY, + UPDATE_VERSION, +} from "./types"; + +const defaultSettings: UpdateTypes = { + updateBody: undefined, + updateVersion: "", +}; + +const settings = + (getFromStorage("state") && getFromStorage("state").settings) || + defaultSettings; + +const initialState: UpdateTypes = settings; + +export const updateReducer = ( + state = initialState, + action: UpdateActionTypes +) => { + switch (action.type) { + case UPDATE_BODY: + return { + ...state, + updateBody: action.payload, + }; + case UPDATE_VERSION: + return { + ...state, + updateVersion: action.payload, + }; + default: + return state; + } +}; diff --git a/app/renderer/src/store/update/types.ts b/app/renderer/src/store/update/types.ts new file mode 100644 index 00000000..214c0232 --- /dev/null +++ b/app/renderer/src/store/update/types.ts @@ -0,0 +1,13 @@ +const update = "[update]"; + +export type UpdateTypes = { + updateVersion: string; + updateBody: string | undefined; +}; +export const UPDATE_BODY = `${update} UPDATE_BODY`; +export const UPDATE_VERSION = `${update} UPDATE_VERSION`; + +export type UpdateActionTypes = { + type: string; + payload: any; +}; diff --git a/app/renderer/src/styles/components/navigation.ts b/app/renderer/src/styles/components/navigation.ts index aa734b29..242b11dd 100644 --- a/app/renderer/src/styles/components/navigation.ts +++ b/app/renderer/src/styles/components/navigation.ts @@ -40,17 +40,33 @@ export const StyledNavList = styled.ul` export const StyledNavListItem = styled.li` width: 100%; height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; `; type NavLinkProps = { type?: string }; +export const StyledNavIconWrapper = styled.div` + position: relative; + box-sizing: content-box; + width: fit-content; + left: 50%; + transform: translateX(-50%); + height: 14px; + margin-bottom: 0.4rem; +`; + export const StyledNavLink = styled(NavLink)` width: 100%; height: 100%; - display: grid; + display: flex; + flex-direction: column; + justify-content: center; justify-items: center; - row-gap: 0.4rem; + text-align: center; position: relative; cursor: pointer; diff --git a/app/renderer/src/styles/routes/tasks/details.ts b/app/renderer/src/styles/routes/tasks/details.ts index 8886e7d8..e324deaf 100644 --- a/app/renderer/src/styles/routes/tasks/details.ts +++ b/app/renderer/src/styles/routes/tasks/details.ts @@ -162,6 +162,8 @@ export const StyledDescriptionPreviewer = styled.div<{ p { color: ${(p) => !p.hasValue && "var(--color-disabled-text)"}; + // fixes an issue where the bullet points show not on the same line + display: inline-block; } h1 { diff --git a/app/shareables/src/index.ts b/app/shareables/src/index.ts index fba0c3c2..d5db765b 100644 --- a/app/shareables/src/index.ts +++ b/app/shareables/src/index.ts @@ -1,3 +1,4 @@ +export const CHECK_FOR_UPDATES = "CHECK_FOR_UPDATES"; export const SET_ALWAYS_ON_TOP = "SET_ALWAYS_ON_TOP"; export const SET_FULLSCREEN_BREAK = "SET_FULLSCREEN_BREAK"; export const SET_COMPACT_MODE = "SET_COMPACT_MODE"; @@ -8,6 +9,8 @@ export const SET_UI_THEME = "SET_UI_THEME"; export const SET_MINIMIZE = "SET_MINIMIZE"; export const SET_CLOSE = "SET_CLOSE"; export const SET_SHOW = "SET_SHOW"; +export const UPDATE_AVAILABLE = "UPDATE_AVAILABLE"; +export const INSTALL_UPDATE = "INSTALL_UPDATE"; export const TO_MAIN: string[] = [ SET_ALWAYS_ON_TOP, @@ -20,9 +23,10 @@ export const TO_MAIN: string[] = [ SET_MINIMIZE, SET_CLOSE, SET_SHOW, + INSTALL_UPDATE, ]; -export const FROM_MAIN: string[] = []; +export const FROM_MAIN: string[] = [UPDATE_AVAILABLE]; export const RELEASE_NOTES_LINK = "https://github.com/zidoro/pomatez/releases/latest"; diff --git a/app/tauri/.gitignore b/app/tauri/.gitignore index f4dfb82b..aba21e24 100644 --- a/app/tauri/.gitignore +++ b/app/tauri/.gitignore @@ -1,4 +1,3 @@ # Generated by Cargo # will have compiled files and executables /target/ - diff --git a/app/tauri/Cargo.lock b/app/tauri/Cargo.lock index bd58969f..233bf1b3 100644 --- a/app/tauri/Cargo.lock +++ b/app/tauri/Cargo.lock @@ -1914,6 +1914,19 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2106,6 +2119,19 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "notify-rust" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7b75c8958cb2eab3451538b32db8a7b74006abc33eb2e6a9a56d21e4775c2b" +dependencies = [ + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2187,6 +2213,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -2560,7 +2597,7 @@ dependencies = [ [[package]] name = "pomatez" -version = "1.3.0" +version = "1.4.3" dependencies = [ "base64 0.21.4", "lazy_static", @@ -2571,6 +2608,7 @@ dependencies = [ "tauri-build", "tauri-plugin-autostart", "tauri-plugin-global-shortcut", + "tauri-plugin-notification", "tauri-plugin-updater", "tauri-plugin-window", "url", @@ -2638,6 +2676,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.33" @@ -3576,6 +3623,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.0.0-alpha.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b7d3138700a1073dce4fbc2eab0399315acd1c0c9fefd5007991344c210517" +dependencies = [ + "log", + "notify-rust", + "rand 0.8.5", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-build", + "thiserror", + "time", + "url", +] + [[package]] name = "tauri-plugin-updater" version = "2.0.0-alpha.3" @@ -3692,6 +3758,16 @@ dependencies = [ "toml 0.7.8", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006851c9ccefa3c38a7646b8cec804bb429def3da10497bfa977179869c3e8e2" +dependencies = [ + "quick-xml", + "windows 0.51.1", +] + [[package]] name = "tempfile" version = "3.3.0" diff --git a/app/tauri/Cargo.toml b/app/tauri/Cargo.toml index a02cd88e..45771b0e 100644 --- a/app/tauri/Cargo.toml +++ b/app/tauri/Cargo.toml @@ -3,7 +3,7 @@ name = "pomatez" # In the current version of release please, unless the toml file is in the root of the project it cannot be updated. # https://github.com/googleapis/release-please/issues/1724 # util/cargo-version-updater.js will run to keep this value up to date before rust builds. -version = "1.3.0" +version = "1.4.3" description = "Attractive pomodoro timer for Windows, Mac, and Linux." authors = ["Roldan Montilla Jr"] license = "MIT" @@ -26,6 +26,7 @@ tauri-plugin-window = "2.0.0-alpha.2" tauri-plugin-autostart = "2.0.0-alpha.3" lazy_static = "1.4.0" base64 = { version = "0.21.4", features = [] } +tauri-plugin-notification = "2.0.0-alpha.5" # This one is for toying with global hotkeys from the browser mostly # We can use https://github.com/tauri-apps/global-hotkey directly tbh diff --git a/app/tauri/README.MD b/app/tauri/README.MD index 550332b3..be70156f 100644 --- a/app/tauri/README.MD +++ b/app/tauri/README.MD @@ -5,4 +5,4 @@ The plan is to try to make it as modular and modifiable as possible. If you have any suggestions for best practices feel free to make some comments ore start a discussion. Though keep in mind some parts may be coded in specific ways just to experiment with features of rust. -The aim is to do it in a way that it can be replaced with simpler/better solutions. \ No newline at end of file +The aim is to do it in a way that it can be replaced with simpler/better solutions. diff --git a/app/tauri/release-prep/release-prep.js b/app/tauri/release-prep/release-prep.js index 2145b540..1f2d17a5 100644 --- a/app/tauri/release-prep/release-prep.js +++ b/app/tauri/release-prep/release-prep.js @@ -3,7 +3,7 @@ const release_please_manifest = require("../../../.release-please-manifest.json" const path = require("path"); const version = `v${release_please_manifest["."]}`; -const github_url = `https://github.com/sekwah41/pomatez/releases/download/${version}/`; +const github_url = `https://github.com/zidoro/pomatez/releases/download/${version}/`; const artifactsPath = path.join( __dirname, "..", diff --git a/app/tauri/release.conf.json b/app/tauri/release.conf.json index 4453c119..3b486f21 100644 --- a/app/tauri/release.conf.json +++ b/app/tauri/release.conf.json @@ -1,9 +1,9 @@ { "plugins": { "updater": { - "dialog": true, + "dialog": false, "endpoints": [ - "https://api.github.com/repos/sekwah41/pomatez/this/is/not/a/valid/url/and/will/be/set/in/main/rs" + "https://api.github.com/repos/zidoro/pomatez/this/is/not/a/valid/url/and/will/be/set/in/rust" ] } }, diff --git a/app/tauri/src/commands.rs b/app/tauri/src/commands.rs index 6f722210..4344448e 100644 --- a/app/tauri/src/commands.rs +++ b/app/tauri/src/commands.rs @@ -9,6 +9,7 @@ use tauri::{Builder, PhysicalSize, Runtime, Wry}; struct WindowSize; use crate::system_tray; +use crate::updater; impl WindowSize { @@ -184,6 +185,8 @@ impl PomatezCommands for Builder { fn register_pomatez_commands(self) -> tauri::Builder { self.invoke_handler(tauri::generate_handler![set_show, set_always_on_top, set_fullscreen_break, set_compact_mode, set_ui_theme, set_native_titlebar, - system_tray::tray_icon_update, set_close, set_minimize]) + system_tray::tray_icon_update, set_close, set_minimize, updater::check_for_updates, + updater::install_update + ]) } } diff --git a/app/tauri/src/global_shortcuts.rs b/app/tauri/src/global_shortcuts.rs index 5c89a24b..e4c0ac69 100644 --- a/app/tauri/src/global_shortcuts.rs +++ b/app/tauri/src/global_shortcuts.rs @@ -21,16 +21,36 @@ impl PomatezGlobalShortcutsSetup for App { println!("Shortcut pressed: {:?}", shortcut); match shortcut.id() { key if SHOW_SHORTCUT.id() == key => { - window.show().expect("Failed to show window"); - window.set_focus().expect("Failed to focus window"); + match window.show() { + Ok(_) => {} + Err(e) => { + println!("Failed to show window: {:?}", e); + } + } + match window.set_focus() { + Ok(_) => {} + Err(e) => { + println!("Failed to focus window: {:?}", e); + } + } } key if HIDE_SHORTCUT.id() == key => { - window.hide().expect("Failed to hide window"); + match window.hide() { + Ok(_) => {} + Err(e) => { + println!("Failed to hide window: {:?}", e); + } + } } _ => println!("Shortcut pressed: {:?}", shortcut), } if shortcut.matches(Modifiers::ALT | Modifiers::SHIFT, Code::KeyH) { - window.hide().expect("Failed to hide window"); + match window.hide() { + Ok(_) => {} + Err(e) => { + println!("Failed to hide window: {:?}", e); + } + } } else { println!("Shortcut pressed: {:?}", shortcut); } @@ -38,7 +58,12 @@ impl PomatezGlobalShortcutsSetup for App { }; let app_handle = self.handle(); - app_handle.plugin(global_shortcut_plugin).expect("failed to register global shortcut plugin"); + match app_handle.plugin(global_shortcut_plugin) { + Ok(_) => {} + Err(e) => { + println!("Failed to register global shortcut plugin: {:?}", e); + } + } println!("Registered global shortcut plugin"); } @@ -51,9 +76,19 @@ pub trait PomatezGlobalShortcutsRegister { impl PomatezGlobalShortcutsRegister for AppHandle { fn register_global_shortcuts(&self) { let global_shortcut = self.global_shortcut(); - global_shortcut.register(SHOW_SHORTCUT.clone()).expect("failed to register global shortcut"); - global_shortcut.register(HIDE_SHORTCUT.clone()).expect("failed to register global shortcut"); + match global_shortcut.register(SHOW_SHORTCUT.clone()) { + Ok(_) => {} + Err(e) => { + println!("Failed to register global shortcut: {:?}", e); + } + }; + match global_shortcut.register(HIDE_SHORTCUT.clone()) { + Ok(_) => {} + Err(e) => { + println!("Failed to register global shortcut: {:?}", e); + } + }; println!("Registered global shortcuts"); } -} \ No newline at end of file +} diff --git a/app/tauri/src/main.rs b/app/tauri/src/main.rs index eca83c34..23648d59 100644 --- a/app/tauri/src/main.rs +++ b/app/tauri/src/main.rs @@ -18,12 +18,12 @@ mod updater; use commands::PomatezCommands; use system_tray::PomatezTray; use global_shortcuts::{PomatezGlobalShortcutsSetup, PomatezGlobalShortcutsRegister}; -use crate::updater::PomatezUpdater; fn main() { let app = tauri::Builder::default() .plugin(tauri_plugin_autostart::init(MacosLauncher::LaunchAgent, None)) .plugin(tauri_plugin_window::init()) + .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .register_pomatez_commands() .setup(|app| { @@ -31,7 +31,6 @@ fn main() { { app.setup_global_shortcuts(); app.set_pomatez_system_tray(); - app.check_for_update(); } Ok(()) diff --git a/app/tauri/src/updater.rs b/app/tauri/src/updater.rs index c1c3e39d..cfc55c16 100644 --- a/app/tauri/src/updater.rs +++ b/app/tauri/src/updater.rs @@ -1,11 +1,10 @@ -use serde::Deserialize; -use tauri::{App}; -use tauri_plugin_updater::UpdaterExt; +use std::sync::Mutex; +use serde::{Serialize, Deserialize}; +use tauri::{Manager, Runtime}; +use tauri_plugin_updater::{Update, UpdaterExt}; use url::Url; -pub trait PomatezUpdater { - fn check_for_update(&self); -} +static UPDATE_INFO: Mutex> = Mutex::new(None); #[derive(Deserialize, Debug)] struct LatestRelease { @@ -19,66 +18,142 @@ struct Asset { browser_download_url: String, } -impl PomatezUpdater for App { - fn check_for_update(&self) { - let handle = self.handle().clone(); - tauri::async_runtime::spawn(async move { - // Todo maybe check that its a supported platform first e.g. windows, macos, linux (AppImage) - - // If its not a supported platform maybe open the release page. - - println!("Checking for updates"); - // Custom configure the updater. - // If we use this endpoint even if the url changes e.g. org name change or project name change the updates should still follow. - let github_releases_endpoint = "https://api.github.com/repos/sekwah41/pomatez/releases/latest"; - let github_releases_endpoint = match Url::parse(github_releases_endpoint) { - Ok(url) => url, - Err(e) => { - println!("Failed to parse url: {:?}. Failed to check for updates", e); - return; +#[derive(Serialize, Debug, Clone)] +struct UpdateAvailable { + version: String, + body: Option +} + +#[tauri::command] +pub fn check_for_updates(ignore_version: String, window: tauri::Window) { + + let handle = window.app_handle().clone(); + + println!("Current version: {}", handle.package_info().version); + + tauri::async_runtime::spawn(async move { + println!("Searching for update file on github."); + // Custom configure the updater. + // If we use this endpoint even if the url changes e.g. org name change or project name change the updates should still follow. + let github_releases_endpoint = "https://api.github.com/repos/zidoro/pomatez/releases/latest"; + let github_releases_endpoint = match Url::parse(github_releases_endpoint) { + Ok(url) => url, + Err(e) => { + println!("Failed to parse url: {:?}. Failed to check for updates", e); + return; + } + }; + let client = reqwest::Client::new(); + let req = client.get(github_releases_endpoint.clone()) + .header("Content-Type", "application/json") + // If this is not set you will get a 403 forbidden error. + .header("User-Agent", "pomatez"); + let response = match req.send().await { + Ok(response) => response, + Err(e) => { + println!("Failed to send request: {:?}. Failed to check for updates", e); + return; + } + }; + + if response.status() != reqwest::StatusCode::OK { + println!("Non OK status code: {:?}. Failed to check for updates", response.status()); + return; + } + let latest_release = match response.json::().await { + Ok(latest_release) => latest_release, + Err(e) => { + println!("Failed to parse response: {:?}. Failed to check for updates", e); + return; + } + }; + + // Find an asset named "tauri-release.json". + let tauri_release_asset = latest_release.assets.iter().find(|asset| asset.name == "tauri-updater.json"); + + // If we found the asset, set it as the updater endpoint. + let tauri_release_asset = match tauri_release_asset { + Some(tauri_release_asset) => tauri_release_asset, + None => { + println!("Failed to find tauri-release.json asset. Failed to check for updates\n\nFound Assets are:"); + // Print a list of the assets found + for asset in latest_release.assets { + println!(" {:?}", asset.name); } - }; - let client = reqwest::Client::new(); - let req = client.get(github_releases_endpoint.clone()) - .header("Content-Type", "application/json") - // If this is not set you will get a 403 forbidden error. - .header("User-Agent", "pomatez"); - let response = req.send().await.expect("Failed to send request"); - if response.status() != reqwest::StatusCode::OK { - println!("Non OK status code: {:?}. Failed to check for updates", response.status()); return; } - let latest_release = response.json::().await.expect("Failed to parse latest release response"); - - // Find an asset named "tauri-release.json". - let tauri_release_asset = latest_release.assets.iter().find(|asset| asset.name == "tauri-release.json"); - - // If we found the asset, set it as the updater endpoint. - let tauri_release_asset = match tauri_release_asset { - Some(tauri_release_asset) => tauri_release_asset, - None => { - println!("Failed to find tauri-release.json asset. Failed to check for updates\n\nFound Assets are:"); - // Print a list of the assets found - for asset in latest_release.assets { - println!(" {:?}", asset.name); - } + }; + + let tauri_release_endpoint = match Url::parse(&tauri_release_asset.browser_download_url) { + Ok(url) => url, + Err(e) => { + println!("Failed to parse url: {:?}. Failed to check for updates", e); + return; + } + }; + let updater_builder = match handle.updater_builder() + .endpoints(vec!(tauri_release_endpoint)) + .header("User-Agent", "pomatez") { + Ok(updater_builder) => updater_builder, + Err(e) => { + println!("Failed to build updater builder: {:?}. Failed to check for updates", e); + return; + } + }; + + let updater = match updater_builder.build() { + Ok(updater) => updater, + Err(e) => { + println!("Failed to build updater: {:?}. Failed to check for updates", e); + return; + } + }; + + println!("Checking for updates"); + + let response = updater.check().await; + + println!("Update check response: {:?}", response); + + match response { + Ok(Some(update)) => { + if ignore_version == update.version { + println!("Ignoring update as user has asked to ignore this version."); return; } - }; + UPDATE_INFO.lock().unwrap().replace(update.clone()); - let tauri_release_endpoint = match Url::parse(&tauri_release_asset.browser_download_url) { - Ok(url) => url, - Err(e) => { - println!("Failed to parse url: {:?}. Failed to check for updates", e); - return; + match window.emit("UPDATE_AVAILABLE", Some(UpdateAvailable { + version: update.version, + body: update.body + })) { + Ok(_) => {}, + Err(e) => { + println!("Failed to emit update available event: {:?}", e); + } } - }; - let updater_builder = handle.updater_builder().endpoints(vec!(tauri_release_endpoint)); - let updater = updater_builder.build() - .expect("could not build updater"); - println!("Checking for updates"); - let response = updater.check().await; - println!("Update check response: {:?}", response); - }); + } + _ => {} + } + }); +} + +#[tauri::command] +pub async fn install_update(_window: tauri::Window) { + println!("Downloading and installing update!"); + + let update = match UPDATE_INFO.lock().unwrap().clone() { + Some(update) => update, + None => { + println!("No update found to install"); + return; + } + }; + + let install_response = update.download_and_install(|_,_| {}, || {}).await; + if let Err(e) = install_response { + println!("Failed to install update: {:?}", e); + } else { + println!("Update installed"); } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 8782c9a4..82c711b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "root", "private": true, - "version": "1.3.1", + "version": "1.5.0", "workspaces": [ "app/*" ], @@ -28,7 +28,7 @@ "gen:tts": "lerna run gen:tts --stream", "format": "prettier --write .", "tauri": "tauri", - "tauri:dev": "tauri dev", + "tauri:dev": "tauri dev --config ./app/tauri/release.conf.json", "tauri:updateversion": "node ./app/tauri/util/cargo-version-updater.js" }, "config": {