diff --git a/.github/workflows/tauri.yaml b/.github/workflows/tauri.yaml new file mode 100644 index 00000000..8d21b704 --- /dev/null +++ b/.github/workflows/tauri.yaml @@ -0,0 +1,95 @@ +name: Tauri Workflow Release Process + +on: + workflow_dispatch: + inputs: + app-slug: + type: string + description: Slug of the application + required: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CN_APP_SLUG: ${{ github.event.inputs.app-slug }} + +jobs: + draft: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: create draft release + uses: crabnebula-dev/cloud-release@v0.2.0 + with: + command: release draft ${{ env.CN_APP_SLUG }} --framework tauri + api-key: ${{ secrets.CN_API_KEY }} + + build: + needs: draft + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - name: Install stable toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + cache: true + + - name: install Linux dependencies + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y webkit2gtk-4.0 + + - name: Install x86_64-apple-darwin for mac and build Tauri binaries + if: matrix.os == 'macos-latest' + run: | + rustup target add x86_64-apple-darwin + npm ci + npm run tauri build -- --target x86_64-apple-darwin + npm run tauri build -- --target aarch64-apple-darwin + env: + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + + - name: build Tauri app for Windows, Linux + if: matrix.os != 'macos-latest' + run: | + npm ci + npm run tauri build + env: + TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + + - name: upload assets + uses: crabnebula-dev/cloud-release@v0.2.0 + with: + command: release upload ${{ env.CN_APP_SLUG }} --framework tauri + api-key: ${{ secrets.CN_API_KEY }} + path: ./src-tauri + + publish: + needs: build + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: publish release + uses: crabnebula-dev/cloud-release@v0.2.0 + with: + command: release publish ${{ env.CN_APP_SLUG }} --framework tauri + api-key: ${{ secrets.CN_API_KEY }} diff --git a/client/package.json b/client/package.json index f353fb2a..8cb0e3bd 100644 --- a/client/package.json +++ b/client/package.json @@ -35,6 +35,8 @@ "@evolu/common-web": "^8.1.1", "@evolu/react": "^8.1.0", "@evolu/react-native": "^11.0.0", + "@filebase/sdk": "^1.0.4", + "@helia/verified-fetch": "^1.5.0", "@lingui/core": "^4.11.2", "@lingui/macro": "^4.11.2", "@lingui/react": "^4.11.2", @@ -53,8 +55,10 @@ "burnt": "^0.12.2", "design": "workspace:react-exo-ui@*", "effect": "^3.5.7", + "file-type": "^19.4.1", "maplibre-gl": "^4.5.0", "openmeteo": "^1.1.4", + "pinata": "^0.2.0", "react": "18.2.0", "react-dom": "18.2.0", "react-exo": "workspace:*", @@ -69,6 +73,7 @@ "react-native-macos": "^0.73.34", "react-native-mmkv": "^2.12.2", "react-native-navigation": "7.40.1", + "react-native-readium": "^2.0.0-rc.2", "react-native-reanimated": "^3.14.0", "react-native-screens": "^3.32.0", "react-native-skottie": "^2.1.4", diff --git a/client/src/app/hooks/useCurrentResource.ts b/client/src/app/hooks/useCurrentResource.ts index a1b59254..c3a2ad30 100644 --- a/client/src/app/hooks/useCurrentResource.ts +++ b/client/src/app/hooks/useCurrentResource.ts @@ -1,10 +1,11 @@ import {useState, useEffect} from 'react'; -import {useLocation} from 'react-exo/navigation'; +import {useLocation, useNavigate} from 'react-exo/navigation'; export function useCurrentResource() { const {pathname, hash} = useLocation(); const [path, setPath] = useState(hash ? `${pathname}/${hash?.slice(1)}` : ''); const [maximized, setMaximized] = useState(true); + const navigate = useNavigate(); useEffect(() => { if (hash) { @@ -18,5 +19,11 @@ export function useCurrentResource() { } }, [pathname, hash, path]); - return {path, maximized, setPath, setMaximized}; + const close = () => { + setMaximized(false); + setPath(''); + navigate(pathname); + }; + + return {path, maximized, close}; } diff --git a/client/src/app/routes/MainLayout.tsx b/client/src/app/routes/Layout.tsx similarity index 80% rename from client/src/app/routes/MainLayout.tsx rename to client/src/app/routes/Layout.tsx index 28b00ba1..d1f56259 100644 --- a/client/src/app/routes/MainLayout.tsx +++ b/client/src/app/routes/Layout.tsx @@ -6,6 +6,7 @@ import {useDeviceSession} from 'app/hooks/useDeviceSession'; import {useProfile} from 'app/data'; import {Menu} from 'app/interface/Menu'; import {Tabs} from 'app/interface/Tabs'; +import {resolve} from 'media/utils/path'; import {CurrentFile} from 'media/stacks/CurrentFile'; import type {useAppContext} from 'app/hooks/useAppContext'; @@ -13,7 +14,7 @@ import type {useAppContext} from 'app/hooks/useAppContext'; export const APP_MENU_WIDTH = 146; export const APP_MENU_TAB_HEIGHT = 64; -export default function MainLayout() { +export default function Layout() { const {styles, theme} = useStyles(stylesheet); const resource = useCurrentResource(); const screen = useWindowDimensions(); @@ -33,6 +34,12 @@ export default function MainLayout() { device, }; + const parts = resolve(resource.path); + const [name, ext] = parts.slice(-1)[0].split('.') ?? []; + const base = parts.slice(0, -1).join('/'); + const path = parts.join('/'); + const url = `/browse/${base}#${name}.${ext}`; + return <> @@ -41,12 +48,15 @@ export default function MainLayout() { - {Boolean(resource.path) && + {Boolean(path) && resource.setPath('')} + close={resource.close} /> } @@ -58,7 +68,7 @@ const stylesheet = createStyleSheet(theme => ({ root: { flex: 1, flexDirection: 'row', - backgroundColor: theme.colors.background, + backgroundColor: theme.colors.neutral, }, rootTabs: { flexDirection: 'column-reverse', diff --git a/client/src/app/routes/index.tsx b/client/src/app/routes/Router.tsx similarity index 90% rename from client/src/app/routes/index.tsx rename to client/src/app/routes/Router.tsx index 2cab3469..fb689e01 100644 --- a/client/src/app/routes/index.tsx +++ b/client/src/app/routes/Router.tsx @@ -8,7 +8,7 @@ export function Router() { return ( <_ {...{history}}> - }> + }> {/* General */} }/> }/> @@ -16,12 +16,12 @@ export function Router() { }/> {/* Media */} }/> + }/> {/* World */} }/> }/> }/> }/> - }/> {/* Dev */} }/> }/> diff --git a/client/src/app/routes/index.ts b/client/src/app/routes/index.ts new file mode 100644 index 00000000..fc5d0aa4 --- /dev/null +++ b/client/src/app/routes/index.ts @@ -0,0 +1 @@ +export {Router} from './Router'; diff --git a/client/src/app/routes/loader.native.tsx b/client/src/app/routes/loader.native.tsx deleted file mode 100644 index cee0d40c..00000000 --- a/client/src/app/routes/loader.native.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export const Layout = { - Main: require('./MainLayout').default, -} - -export const Screen = { - // App - Settings: require('./ScreenSettings').default, - Storage: require('./ScreenStorage').default, - Teaser: require('./ScreenTeaser').default, - // General - Home: require('../../home/routes/ScreenHome').default, - // Media - Browse: require('../../media/routes/ScreenBrowse').default, - // World - World: require('../../world/routes/ScreenWorld').default, - Map: require('../../world/routes/ScreenMap').default, - Calendar: require('../../world/routes/ScreenCalendar').default, - // Dev - Design: require('../../dev/routes/ScreenDesign').default, - Library: require('../../dev/routes/ScreenLibrary').default, -} diff --git a/client/src/app/routes/loader.tsx b/client/src/app/routes/loader.tsx deleted file mode 100644 index bbfb8a4c..00000000 --- a/client/src/app/routes/loader.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {lazy} from 'react'; - -export const Layout = { - Main: lazy(() => import('./MainLayout')), -} - -export const Screen = { - // App - Settings: lazy(() => import('./ScreenSettings')), - Storage: lazy(() => import('./ScreenStorage')), - Teaser: lazy(() => import('./ScreenTeaser')), - // General - Home: lazy(() => import('../../home/routes/ScreenHome')), - // Media - Browse: lazy(() => import('../../media/routes/ScreenBrowse')), - // World - World: lazy(() => import('../../world/routes/ScreenWorld')), - Map: lazy(() => import('../../world/routes/ScreenMap')), - Calendar: lazy(() => import('../../world/routes/ScreenCalendar')), - // Dev - Design: lazy(() => import('../../dev/routes/ScreenDesign')), - Library: lazy(() => import('../../dev/routes/ScreenLibrary')), -} diff --git a/client/src/app/routes/loader/index.native.tsx b/client/src/app/routes/loader/index.native.tsx new file mode 100644 index 00000000..77ce28ca --- /dev/null +++ b/client/src/app/routes/loader/index.native.tsx @@ -0,0 +1,41 @@ +export const Layout = { + get App() { + return require('../Layout').default; + }, +} + +export const Screen = { + get Settings() { + return require('../ScreenSettings').default; + }, + get Storage() { + return require('../ScreenStorage').default; + }, + get Teaser() { + return require('../ScreenTeaser').default; + }, + get Home() { + return require('../../../home/routes/ScreenHome').default; + }, + get Browse() { + return require('../../../media/routes/ScreenBrowse').default; + }, + get View() { + return require('../../../media/routes/ScreenView').default; + }, + get World() { + return require('../../../world/routes/ScreenWorld').default; + }, + get Map() { + return require('../../../world/routes/ScreenMap').default; + }, + get Calendar() { + return require('../../../world/routes/ScreenCalendar').default; + }, + get Design() { + return require('../../../dev/routes/ScreenDesign').default; + }, + get Library() { + return require('../../../dev/routes/ScreenLibrary').default; + }, +} diff --git a/client/src/app/routes/loader/index.tsx b/client/src/app/routes/loader/index.tsx new file mode 100644 index 00000000..bb2eb378 --- /dev/null +++ b/client/src/app/routes/loader/index.tsx @@ -0,0 +1,43 @@ +import {lazy} from 'react'; + +export const Layout = { + App: lazy( + () => import('../Layout') + ), +} + +export const Screen = { + Settings: lazy( + () => import('../ScreenSettings') + ), + Storage: lazy( + () => import('../ScreenStorage') + ), + Teaser: lazy( + () => import('../ScreenTeaser') + ), + Home: lazy( + () => import('../../../home/routes/ScreenHome') + ), + Browse: lazy( + () => import('../../../media/routes/ScreenBrowse') + ), + View: lazy( + () => import('../../../media/routes/ScreenView') + ), + World: lazy( + () => import('../../../world/routes/ScreenWorld') + ), + Map: lazy( + () => import('../../../world/routes/ScreenMap') + ), + Calendar: lazy( + () => import('../../../world/routes/ScreenCalendar') + ), + Design: lazy( + () => import('../../../dev/routes/ScreenDesign') + ), + Library: lazy( + () => import('../../../dev/routes/ScreenLibrary') + ), +} diff --git a/client/src/media/file/FileAudio.tsx b/client/src/media/file/FileAudio.tsx new file mode 100644 index 00000000..12241397 --- /dev/null +++ b/client/src/media/file/FileAudio.tsx @@ -0,0 +1,37 @@ +import {Video} from 'react-exo/video'; +import {forwardRef} from 'react'; +import {useStyles, createStyleSheet} from 'react-native-unistyles'; +import {useFileUrl} from 'media/hooks/useFileUrl'; + +import type {FileProps} from 'media/file'; +import type {VideoRef} from 'react-exo/video'; + +interface FileAudio extends FileProps { + ref: React.RefObject, + name: string, + extension: string, +} + +export default forwardRef((props: FileAudio) => { + const {styles} = useStyles(stylesheet); + const audio = useFileUrl(props.path); + + return audio ? ( +