Implemented share link generation and viewing
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				/ Build the server (push) Successful in 2m35s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	/ Build the server (push) Successful in 2m35s
				
			Closes #60
This commit is contained in:
		@@ -12,32 +12,32 @@
 | 
			
		||||
    "fetch-api": "openapi-typescript http://127.0.0.1:2121/openapi.json -o ./src/api/schema.d.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@iconify/json": "^2.2.132",
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte": "^2.4.2",
 | 
			
		||||
    "@tsconfig/svelte": "^5.0.0",
 | 
			
		||||
    "@types/node": "^20.8.6",
 | 
			
		||||
    "@types/qrcode-svg": "^1.1.2",
 | 
			
		||||
    "autoprefixer": "^10.4.14",
 | 
			
		||||
    "@iconify/json": "^2.2.324",
 | 
			
		||||
    "@sveltejs/vite-plugin-svelte": "^2.5.3",
 | 
			
		||||
    "@tsconfig/svelte": "^5.0.4",
 | 
			
		||||
    "@types/node": "^20.17.30",
 | 
			
		||||
    "@types/qrcode-svg": "^1.1.5",
 | 
			
		||||
    "autoprefixer": "^10.4.21",
 | 
			
		||||
    "flowbite": "^1.8.1",
 | 
			
		||||
    "flowbite-svelte": "^0.44.18",
 | 
			
		||||
    "openapi-typescript": "^7.3.3",
 | 
			
		||||
    "postcss": "^8.4.24",
 | 
			
		||||
    "postcss-load-config": "^4.0.1",
 | 
			
		||||
    "svelte": "^4.0.5",
 | 
			
		||||
    "svelte-check": "^3.4.6",
 | 
			
		||||
    "tailwindcss": "^3.3.2",
 | 
			
		||||
    "tslib": "^2.6.0",
 | 
			
		||||
    "typescript": "^5.0.2",
 | 
			
		||||
    "unplugin-icons": "^0.17.1",
 | 
			
		||||
    "vite": "^4.4.5",
 | 
			
		||||
    "vite-plugin-html": "^3.2.0",
 | 
			
		||||
    "flowbite-svelte": "^0.44.24",
 | 
			
		||||
    "openapi-typescript": "^7.6.1",
 | 
			
		||||
    "postcss": "^8.5.3",
 | 
			
		||||
    "postcss-load-config": "^4.0.2",
 | 
			
		||||
    "svelte": "^4.2.19",
 | 
			
		||||
    "svelte-check": "^3.8.6",
 | 
			
		||||
    "tailwindcss": "^3.4.17",
 | 
			
		||||
    "tslib": "^2.8.1",
 | 
			
		||||
    "typescript": "^5.8.3",
 | 
			
		||||
    "unplugin-icons": "^0.17.4",
 | 
			
		||||
    "vite": "^4.5.12",
 | 
			
		||||
    "vite-plugin-html": "^3.2.2",
 | 
			
		||||
    "vite-plugin-singlefile": "^0.13.5",
 | 
			
		||||
    "vite-plugin-tailwind-purgecss": "^0.1.3"
 | 
			
		||||
    "vite-plugin-tailwind-purgecss": "^0.1.4"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@microsoft/fetch-event-source": "^2.0.1",
 | 
			
		||||
    "filesize": "^10.1.0",
 | 
			
		||||
    "openapi-fetch": "^0.12.0",
 | 
			
		||||
    "filesize": "^10.1.6",
 | 
			
		||||
    "openapi-fetch": "^0.12.5",
 | 
			
		||||
    "qrcode-svg": "^1.1.0",
 | 
			
		||||
    "svelte-spa-router": "^3.3.0",
 | 
			
		||||
    "tailwind-merge": "^1.14.0"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1242
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1242
									
								
								frontend/pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {error_banner, info_banner, rpc, session, show_working, token, workingWrapper} from './store';
 | 
			
		||||
    import {error_banner, info_banner, globalRpc, session, show_working, token, workingWrapper} from './store';
 | 
			
		||||
    import {Banner, Navbar, NavBrand, Spinner} from 'flowbite-svelte';
 | 
			
		||||
    import Router, {replace} from 'svelte-spa-router';
 | 
			
		||||
    import Router, {push, replace} from 'svelte-spa-router';
 | 
			
		||||
    import {routes} from './routes';
 | 
			
		||||
    import {FileStorage} from './icons';
 | 
			
		||||
    import LinkButton from './components/LinkButton.svelte';
 | 
			
		||||
@@ -10,14 +10,15 @@
 | 
			
		||||
    const s = session.s;
 | 
			
		||||
 | 
			
		||||
    async function leaveSudo() {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.unSudo());
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.unSudo());
 | 
			
		||||
        await session.update($token);
 | 
			
		||||
        await replace('/admin');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function logout() {
 | 
			
		||||
        await rpc.logout();
 | 
			
		||||
        await globalRpc.logout();
 | 
			
		||||
        token.set(null);
 | 
			
		||||
        await push('/login');
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -49,6 +50,10 @@
 | 
			
		||||
                <A href="#/profile">Profile</A>
 | 
			
		||||
                <LinkButton on:click={logout}>Logout</LinkButton>
 | 
			
		||||
            </div>
 | 
			
		||||
        {:else}
 | 
			
		||||
            <div class="flex md:order-2 gap-x-2">
 | 
			
		||||
                <A href="#/login">Login</A>
 | 
			
		||||
            </div>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </Navbar>
 | 
			
		||||
    <span class="grid justify-items-center mt-10">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,8 @@
 | 
			
		||||
import type {paths, components} from './schema';
 | 
			
		||||
import createClient from 'openapi-fetch';
 | 
			
		||||
import {fetchEventSource} from '@microsoft/fetch-event-source';
 | 
			
		||||
 | 
			
		||||
const client = createClient<paths>();
 | 
			
		||||
client.use({
 | 
			
		||||
    onRequest({ schemaPath, request }) {
 | 
			
		||||
        if (schemaPath.startsWith('/api/public') || rpc.token == '')
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        request.headers.set('Authorization', `Bearer ${rpc.token}`);
 | 
			
		||||
        return request;
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
import {token} from '../store';
 | 
			
		||||
import { replace } from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
export type Session = components['schemas']['de.mattv.fileserver.Response$Session'];
 | 
			
		||||
export type Node = components['schemas']['de.mattv.fileserver.Response$Node'];
 | 
			
		||||
@@ -19,64 +10,99 @@ export type PathSegment = components['schemas']['de.mattv.fileserver.Response$Pa
 | 
			
		||||
export type CreateNodeInfo = components['schemas']['de.mattv.fileserver.Response$CreateNodeInfo'];
 | 
			
		||||
export type UserInfo = components['schemas']['de.mattv.fileserver.Response$UserInfo'];
 | 
			
		||||
 | 
			
		||||
export const rpc = {
 | 
			
		||||
    token: '',
 | 
			
		||||
const _getRpcClient = () => {
 | 
			
		||||
    let obj = {
 | 
			
		||||
        token: '',
 | 
			
		||||
        client: createClient<paths>(),
 | 
			
		||||
        share_root: null,
 | 
			
		||||
 | 
			
		||||
    signup: (username: string, password: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
 | 
			
		||||
    login: (username: string, password: string, otp?: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/login', { body: { username, password, otp } }).then(v => v.data),
 | 
			
		||||
        signup: (username: string, password: string) =>
 | 
			
		||||
            obj.client.POST('/api/public/auth/signup', { body: { username, password } }).then(v => v.data),
 | 
			
		||||
        login: (username: string, password: string, otp?: string) =>
 | 
			
		||||
            obj.client.POST('/api/public/auth/login', { body: { username, password, otp } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    send_recovery_key: (username: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/send_recovery_key', { body: username }).then(v => v.data),
 | 
			
		||||
    reset_password: (key: string, password: string) =>
 | 
			
		||||
        client.POST('/api/public/auth/reset_password', { body: { key, password } }).then(v => v.data),
 | 
			
		||||
        send_recovery_key: (username: string) =>
 | 
			
		||||
            obj.client.POST('/api/public/auth/send_recovery_key', { body: username }).then(v => v.data),
 | 
			
		||||
        reset_password: (key: string, password: string) =>
 | 
			
		||||
            obj.client.POST('/api/public/auth/reset_password', { body: { key, password } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    change_password: (oldPassword: string, newPassword: string) =>
 | 
			
		||||
        client.POST('/api/user/auth/change_password', { body: { oldPassword, newPassword } }).then(v => v.data),
 | 
			
		||||
        change_password: (oldPassword: string, newPassword: string) =>
 | 
			
		||||
            obj.client.POST('/api/user/auth/change_password', { body: { oldPassword, newPassword } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    logout: () => client.POST('/api/user/auth/logout').then(v => v.data),
 | 
			
		||||
    logoutAll: () => client.POST('/api/user/auth/logout_all').then(v => v.data),
 | 
			
		||||
    deleteAccount: () => client.POST('/api/user/auth/delete').then(v => v.data),
 | 
			
		||||
    sessionInfo: () => client.POST('/api/user/session').then(v => v.data),
 | 
			
		||||
        logout: () => obj.client.POST('/api/user/auth/logout').then(v => v.data),
 | 
			
		||||
        logoutAll: () => obj.client.POST('/api/user/auth/logout_all').then(v => v.data),
 | 
			
		||||
        deleteAccount: () => obj.client.POST('/api/user/auth/delete').then(v => v.data),
 | 
			
		||||
        sessionInfo: () => obj.client.POST('/api/user_share/session').then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    tfaSetupMail: () => client.POST('/api/user/tfa/setup_mail').then(v => v.data),
 | 
			
		||||
    tfaSetupTotp: () => client.POST('/api/user/tfa/setup_totp').then(v => v.data),
 | 
			
		||||
    tfaComplete: (code: string) => client.POST('/api/user/tfa/complete', { body: code }).then(v => v.data),
 | 
			
		||||
    tfaDisable: () => client.POST('/api/user/tfa/disable').then(v => v.data),
 | 
			
		||||
        tfaSetupMail: () => obj.client.POST('/api/user/tfa/setup_mail').then(v => v.data),
 | 
			
		||||
        tfaSetupTotp: () => obj.client.POST('/api/user/tfa/setup_totp').then(v => v.data),
 | 
			
		||||
        tfaComplete: (code: string) => obj.client.POST('/api/user/tfa/complete', { body: code }).then(v => v.data),
 | 
			
		||||
        tfaDisable: () => obj.client.POST('/api/user/tfa/disable').then(v => v.data),
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    getNode: (node: number) => client.POST('/api/user/fs/node', { body: node }).then(v => v.data),
 | 
			
		||||
    getPath: (node: number) => client.POST('/api/user/fs/path', { body: node }).then(v => v.data),
 | 
			
		||||
    getNodesSize: (nodes: number[]) => client.POST('/api/user/fs/size', { body: nodes }).then(v => v.data),
 | 
			
		||||
    getMime: (node: number) => client.POST('/api/user/fs/mime', { body: node }).then(v => v.data),
 | 
			
		||||
    downloadPreview: (node: number) => client.POST('/api/user/fs/preview', { body: node }).then(v => v.data),
 | 
			
		||||
        getNode: (node: number) => obj.client.POST('/api/user_share/fs/node', { body: node }).then(v => v.data),
 | 
			
		||||
        getPath: (node: number) => obj.client.POST('/api/user_share/fs/path', { body: node }).then(v => v.data),
 | 
			
		||||
        getNodesSize: (nodes: number[]) => obj.client.POST('/api/user_share/fs/size', { body: nodes }).then(v => v.data),
 | 
			
		||||
        getMime: (node: number) => obj.client.POST('/api/user_share/fs/mime', { body: node }).then(v => v.data),
 | 
			
		||||
        downloadPreview: (node: number) => obj.client.POST('/api/user_share/fs/preview', { body: node }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    createNode: (name: string, parent: number, file: boolean) =>
 | 
			
		||||
        client.POST('/api/user/fs/create', { body: { name, parent, file } }).then(v => v.data),
 | 
			
		||||
        createNode: (name: string, parent: number, file: boolean) =>
 | 
			
		||||
            obj.client.POST('/api/user/fs/create', { body: { name, parent, file } }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
    deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        body: JSON.stringify(nodes),
 | 
			
		||||
        headers: {
 | 
			
		||||
            'Authorization': 'Bearer ' + rpc.token,
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
        },
 | 
			
		||||
        onmessage: v => cbk(v.data),
 | 
			
		||||
        onerror: e => { throw e; },
 | 
			
		||||
        onclose: () => cbk(null)
 | 
			
		||||
    }),
 | 
			
		||||
        deleteNodes: (nodes: number[], cbk: (v: string|null) => void) => fetchEventSource('/api/user/fs/delete', {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(nodes),
 | 
			
		||||
            headers: {
 | 
			
		||||
                'Authorization': 'Bearer ' + obj.token,
 | 
			
		||||
                'Content-Type': 'application/json'
 | 
			
		||||
            },
 | 
			
		||||
            onmessage: v => cbk(v.data),
 | 
			
		||||
            onerror: e => { throw e; },
 | 
			
		||||
            onclose: () => cbk(null)
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
    admin: {
 | 
			
		||||
        listUsers: () => client.POST('/api/admin/users').then(v => v.data),
 | 
			
		||||
        setEnabled: (id: number, state: boolean) => client.POST('/api/admin/user/set_enabled', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
        setAdmin: (id: number, state: boolean) => client.POST('/api/admin/user/set_admin', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
        sudo: (id: number) => client.POST('/api/admin/user/sudo', { body: id }).then(v => v.data),
 | 
			
		||||
        logout: (id: number) => client.POST('/api/admin/user/logout', { body: id }).then(v => v.data),
 | 
			
		||||
        disableTfa: (id: number) => client.POST('/api/admin/user/disable_tfa', { body: id }).then(v => v.data),
 | 
			
		||||
        deleteUser: (id: number) => client.POST('/api/admin/user/delete', { body: id }).then(v => v.data),
 | 
			
		||||
        unSudo: () => client.POST('/api/admin/un_sudo').then(v => v.data),
 | 
			
		||||
        shutdown: () => client.POST('/api/admin/shutdown').then(v => v.data),
 | 
			
		||||
        shareNode: (node: number) => obj.client.POST('/api/user/fs/share', { body: node }).then(v => v.data),
 | 
			
		||||
        unshareNode: (node: number) => obj.client.POST('/api/user/fs/unshare', { body: node }).then(v => v.data),
 | 
			
		||||
 | 
			
		||||
        admin: {
 | 
			
		||||
            listUsers: () => obj.client.POST('/api/admin/users').then(v => v.data),
 | 
			
		||||
            setEnabled: (id: number, state: boolean) => obj.client.POST('/api/admin/user/set_enabled', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
            setAdmin: (id: number, state: boolean) => obj.client.POST('/api/admin/user/set_admin', { body: { id, state } }).then(v => v.data),
 | 
			
		||||
            sudo: (id: number) => obj.client.POST('/api/admin/user/sudo', { body: id }).then(v => v.data),
 | 
			
		||||
            logout: (id: number) => obj.client.POST('/api/admin/user/logout', { body: id }).then(v => v.data),
 | 
			
		||||
            disableTfa: (id: number) => obj.client.POST('/api/admin/user/disable_tfa', { body: id }).then(v => v.data),
 | 
			
		||||
            deleteUser: (id: number) => obj.client.POST('/api/admin/user/delete', { body: id }).then(v => v.data),
 | 
			
		||||
            unSudo: () => obj.client.POST('/api/admin/un_sudo').then(v => v.data),
 | 
			
		||||
            shutdown: () => obj.client.POST('/api/admin/shutdown').then(v => v.data),
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    obj.client.use({
 | 
			
		||||
        onRequest({ schemaPath, request }) {
 | 
			
		||||
            if (schemaPath.startsWith('/api/public') || obj.token == '')
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            request.headers.set('Authorization', `Bearer ${obj.token}`);
 | 
			
		||||
            return request;
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type RpcClient = Omit<ReturnType<typeof _getRpcClient>, 'share_root'> & { share_root: null | number };
 | 
			
		||||
export const getRpcClient: () => RpcClient = () => _getRpcClient();
 | 
			
		||||
export const globalRpc = getRpcClient();
 | 
			
		||||
 | 
			
		||||
globalRpc.client.use({
 | 
			
		||||
    onResponse({ schemaPath, response }) {
 | 
			
		||||
        if (schemaPath.startsWith('/api/public') || schemaPath == '/api/user/session')
 | 
			
		||||
            return response;
 | 
			
		||||
        if (response.status >= 400 && response.status != 404) {
 | 
			
		||||
            token.set(null);
 | 
			
		||||
            replace('/login');
 | 
			
		||||
        }
 | 
			
		||||
        return response;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										399
									
								
								frontend/src/api/schema.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										399
									
								
								frontend/src/api/schema.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -4,6 +4,102 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export interface paths {
 | 
			
		||||
    "/api/user_share/session": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["session"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user_share/fs/size": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["size"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user_share/fs/preview": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["preview"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user_share/fs/path": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["path"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user_share/fs/node": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["nodeInfo"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user_share/fs/mime": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["mime"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/tfa/setup_totp": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
@@ -68,7 +164,7 @@ export interface paths {
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/session": {
 | 
			
		||||
    "/api/user/fs/unshare": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
@@ -77,14 +173,14 @@ export interface paths {
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["session"];
 | 
			
		||||
        post: operations["path_1"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/fs/size": {
 | 
			
		||||
    "/api/user/fs/share": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
@@ -93,71 +189,7 @@ export interface paths {
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["size"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/fs/preview": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["preview"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/fs/path": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["path"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/fs/node": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["nodeInfo"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
        patch?: never;
 | 
			
		||||
        trace?: never;
 | 
			
		||||
    };
 | 
			
		||||
    "/api/user/fs/mime": {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        get?: never;
 | 
			
		||||
        put?: never;
 | 
			
		||||
        post: operations["mime"];
 | 
			
		||||
        post: operations["share"];
 | 
			
		||||
        delete?: never;
 | 
			
		||||
        options?: never;
 | 
			
		||||
        head?: never;
 | 
			
		||||
@@ -472,15 +504,13 @@ export interface paths {
 | 
			
		||||
export type webhooks = Record<string, never>;
 | 
			
		||||
export interface components {
 | 
			
		||||
    schemas: {
 | 
			
		||||
        "de.mattv.fileserver.ResponseJava.lang.String": {
 | 
			
		||||
            e?: string;
 | 
			
		||||
            o?: string;
 | 
			
		||||
        };
 | 
			
		||||
        "de.mattv.fileserver.Response$Session": {
 | 
			
		||||
            name: string;
 | 
			
		||||
            tfaEnabled: boolean;
 | 
			
		||||
            admin: boolean;
 | 
			
		||||
            sudo: boolean;
 | 
			
		||||
            /** Format: int64 */
 | 
			
		||||
            shareRoot?: number;
 | 
			
		||||
        };
 | 
			
		||||
        "de.mattv.fileserver.Response$PathSegment": {
 | 
			
		||||
            name: string;
 | 
			
		||||
@@ -493,12 +523,17 @@ export interface components {
 | 
			
		||||
            name: string;
 | 
			
		||||
            file: boolean;
 | 
			
		||||
            preview: boolean;
 | 
			
		||||
            shareName?: string;
 | 
			
		||||
            /** Format: int64 */
 | 
			
		||||
            size?: number;
 | 
			
		||||
            /** Format: int64 */
 | 
			
		||||
            parent?: number;
 | 
			
		||||
            children?: components["schemas"]["de.mattv.fileserver.Response$Node"][];
 | 
			
		||||
        };
 | 
			
		||||
        "de.mattv.fileserver.ResponseJava.lang.String": {
 | 
			
		||||
            e?: string;
 | 
			
		||||
            o?: string;
 | 
			
		||||
        };
 | 
			
		||||
        "org.springframework.web.servlet.mvc.method.annotation.SseEmitter": {
 | 
			
		||||
            /** Format: int64 */
 | 
			
		||||
            timeout?: number;
 | 
			
		||||
@@ -566,88 +601,6 @@ export interface components {
 | 
			
		||||
}
 | 
			
		||||
export type $defs = Record<string, never>;
 | 
			
		||||
export interface operations {
 | 
			
		||||
    setupTfaTotp: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": components["schemas"]["de.mattv.fileserver.ResponseJava.lang.String"];
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    setupTfaMail: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": string;
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    disableTfa: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content?: never;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    setupComplete: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": string;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": string;
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    session: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
@@ -788,16 +741,146 @@ export interface operations {
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    delete: {
 | 
			
		||||
    setupTfaTotp: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query: {
 | 
			
		||||
                ids: number[];
 | 
			
		||||
            };
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": components["schemas"]["de.mattv.fileserver.ResponseJava.lang.String"];
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    setupTfaMail: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": string;
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    disableTfa: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody?: never;
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content?: never;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    setupComplete: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": string;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": string;
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    path_1: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": number;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content?: never;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    share: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": number;
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
                headers: {
 | 
			
		||||
                    [name: string]: unknown;
 | 
			
		||||
                };
 | 
			
		||||
                content: {
 | 
			
		||||
                    "*/*": string;
 | 
			
		||||
                };
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    delete: {
 | 
			
		||||
        parameters: {
 | 
			
		||||
            query?: never;
 | 
			
		||||
            header?: never;
 | 
			
		||||
            path?: never;
 | 
			
		||||
            cookie?: never;
 | 
			
		||||
        };
 | 
			
		||||
        requestBody: {
 | 
			
		||||
            content: {
 | 
			
		||||
                "application/json": number[];
 | 
			
		||||
            };
 | 
			
		||||
        };
 | 
			
		||||
        responses: {
 | 
			
		||||
            /** @description OK */
 | 
			
		||||
            200: {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {rpc, show_working} from '../store';
 | 
			
		||||
    import {globalRpc, show_working} from '../store';
 | 
			
		||||
    import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
 | 
			
		||||
    import {afterUpdate, createEventDispatcher} from 'svelte';
 | 
			
		||||
 | 
			
		||||
@@ -18,7 +18,8 @@
 | 
			
		||||
        show_working.set(true);
 | 
			
		||||
 | 
			
		||||
        await new Promise<void>((resolve) => {
 | 
			
		||||
            rpc.deleteNodes(nodes, v => {
 | 
			
		||||
            
 | 
			
		||||
            globalRpc.deleteNodes(nodes, v => {
 | 
			
		||||
                if (v == null)
 | 
			
		||||
                    resolve();
 | 
			
		||||
                else {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,14 +22,18 @@
 | 
			
		||||
        Tooltip
 | 
			
		||||
    } from 'flowbite-svelte';
 | 
			
		||||
    import {filesize} from 'filesize';
 | 
			
		||||
    import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd} from '../icons';
 | 
			
		||||
    import {api, download, rpc, workingWrapperR, error_banner} from '../store';
 | 
			
		||||
    import {Folder, FolderParent, DocumentBlank, CaretLeft, FolderAdd, Share} from '../icons';
 | 
			
		||||
    import {api, download, workingWrapperR, error_banner, type RpcClient, info_banner} from '../store';
 | 
			
		||||
    import LinkButton from './LinkButton.svelte';
 | 
			
		||||
    import DeleteModal from './DeleteModal.svelte';
 | 
			
		||||
    import A from './A.svelte';
 | 
			
		||||
    import {createEventDispatcher} from 'svelte';
 | 
			
		||||
 | 
			
		||||
    export let node: api.Node;
 | 
			
		||||
    export let rpc: RpcClient
 | 
			
		||||
    export let path_prefix: string;
 | 
			
		||||
 | 
			
		||||
    const not_share = rpc.share_root == null;
 | 
			
		||||
 | 
			
		||||
    const dispatch = createEventDispatcher<{reload_node: null}>();
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +83,6 @@
 | 
			
		||||
    $: ctx_style = `top: ${ctx_y}px; left: ${ctx_x}px; position: fixed;`;
 | 
			
		||||
 | 
			
		||||
    function onCtxMenu(node: api.Node, e: MouseEvent) {
 | 
			
		||||
        console.log(e);
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (!ctx_hidden)
 | 
			
		||||
            return ctx_hidden = true;
 | 
			
		||||
@@ -93,15 +96,31 @@
 | 
			
		||||
    const selectFolders = () => selected = dirs.map(v => v.id);
 | 
			
		||||
    const selectFiles = () => selected = files.map(v => v.id);
 | 
			
		||||
    const selectNone = () => selected = [];
 | 
			
		||||
    const downloadSelected = () => download(nodes.filter(v => selected.includes(v.id)));
 | 
			
		||||
    const downloadSelected = () => download(rpc, nodes.filter(v => selected.includes(v.id)));
 | 
			
		||||
 | 
			
		||||
    let del: (nodes: number[]) => Promise<void>; // bound to DeleteModal
 | 
			
		||||
    const deleteSelected = () => del(selected);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    const onCtxDownload = () => download([ctx_node]);
 | 
			
		||||
 | 
			
		||||
    let del: (nodes: number[]) => Promise<void>;
 | 
			
		||||
    const onCtxDelete = () => del([ctx_node.id]);
 | 
			
		||||
 | 
			
		||||
    const onCtxDownload = () => download(rpc, [ctx_node]);
 | 
			
		||||
 | 
			
		||||
    const shareCopyLink = (id: string) => {
 | 
			
		||||
        let link = `${location.origin}/#/share/${id}`;
 | 
			
		||||
        navigator.clipboard.writeText(link);
 | 
			
		||||
        info_banner.set('Copied link to clipboard');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onCtxShare = async () => {
 | 
			
		||||
        let id = await rpc.shareNode(ctx_node.id);
 | 
			
		||||
        setTimeout(() => shareCopyLink(id!), 75);
 | 
			
		||||
        dispatch('reload_node');
 | 
			
		||||
    };
 | 
			
		||||
    const onCtxCopyShare = () => shareCopyLink(ctx_node.shareName!);
 | 
			
		||||
    const onCtxUnshare = async () => {
 | 
			
		||||
        await rpc.unshareNode(ctx_node.id);
 | 
			
		||||
        dispatch('reload_node');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onShowPreview = (e: Event) => { show_preview.set((e.target as HTMLInputElement).checked); }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -123,7 +142,7 @@
 | 
			
		||||
            <TableBodyRow>
 | 
			
		||||
                <TableBodyCell class="!p-4"></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="px-2 w-0"><FolderParent /></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0"><A href={'#/view/' + node.parent}>..</A></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0"><A href={path_prefix + node.parent}>..</A></TableBodyCell>
 | 
			
		||||
                <TableBodyCell></TableBodyCell>
 | 
			
		||||
            </TableBodyRow>
 | 
			
		||||
        {/if}
 | 
			
		||||
@@ -131,7 +150,10 @@
 | 
			
		||||
            <TableBodyRow on:contextmenu={onCtxMenu.bind(null, node)}>
 | 
			
		||||
                <TableBodyCell class="p-2 pl-4 w-0 h-0"><Checkbox bind:group={selected} value={node.id}/></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="px-2 w-0"><Folder /></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0 flex flex-row gap-2">
 | 
			
		||||
                    {#if node.shareName && not_share}<Share/>{/if}
 | 
			
		||||
                    <A href={path_prefix + node.id}>{node.name}</A>
 | 
			
		||||
                </TableBodyCell>
 | 
			
		||||
                <TableBodyCell></TableBodyCell>
 | 
			
		||||
            </TableBodyRow>
 | 
			
		||||
        {/each}
 | 
			
		||||
@@ -149,7 +171,10 @@
 | 
			
		||||
                        <DocumentBlank />
 | 
			
		||||
                    {/if}
 | 
			
		||||
                </TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0"><A href={'#/view/' + node.id}>{node.name}</A></TableBodyCell>
 | 
			
		||||
                <TableBodyCell class="pl-0 flex flex-row gap-2">
 | 
			
		||||
                    {#if node.shareName && not_share}<Share/>{/if}
 | 
			
		||||
                    <A href={path_prefix + node.id}>{node.name}</A>
 | 
			
		||||
                </TableBodyCell>
 | 
			
		||||
                <TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
 | 
			
		||||
            </TableBodyRow>
 | 
			
		||||
        {/each}
 | 
			
		||||
@@ -157,10 +182,10 @@
 | 
			
		||||
    <tfoot class="text-gray-700 bg-gray-50">
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td class="px-6 py-3" colspan="3">
 | 
			
		||||
                <LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>
 | 
			
		||||
                {#if not_share}<LinkButton on:click={() => (show_new_folder = true)} class="mr-3">New folder</LinkButton>{/if}
 | 
			
		||||
                {#if selected.length > 0}
 | 
			
		||||
                    <LinkButton on:click={downloadSelected}>Download</LinkButton>
 | 
			
		||||
                    <LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>
 | 
			
		||||
                    {#if not_share}<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>{/if}
 | 
			
		||||
                {/if}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
 | 
			
		||||
@@ -189,7 +214,17 @@
 | 
			
		||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
 | 
			
		||||
<div style={ctx_style} hidden={ctx_hidden} class="z-50 shadow-md rounded-lg border-gray-100 bg-white" on:contextmenu={() => (ctx_hidden = true)}>
 | 
			
		||||
    <ul class="py-1">
 | 
			
		||||
        {#if not_share}
 | 
			
		||||
            {#if ctx_node?.shareName}
 | 
			
		||||
                <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxCopyShare}>Copy share link</button></li>
 | 
			
		||||
                <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxUnshare}>Unshare</button></li>
 | 
			
		||||
            {:else}
 | 
			
		||||
                <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxShare}>Share</button></li>
 | 
			
		||||
            {/if}
 | 
			
		||||
        {/if}
 | 
			
		||||
        <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxDownload}>Download</button></li>
 | 
			
		||||
        <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
 | 
			
		||||
        {#if not_share}
 | 
			
		||||
            <li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
 | 
			
		||||
        {/if}
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, Spinner} from 'flowbite-svelte';
 | 
			
		||||
    import {Download} from '../icons';
 | 
			
		||||
    import {api, download, rpc, token, workingWrapper} from '../store';
 | 
			
		||||
    import {api, download, workingWrapper, type RpcClient} from '../store';
 | 
			
		||||
    import {onDestroy} from 'svelte';
 | 
			
		||||
 | 
			
		||||
    export let node: api.Node;
 | 
			
		||||
    export let rpc: RpcClient;
 | 
			
		||||
 | 
			
		||||
    let src = '';
 | 
			
		||||
    let loading = false;
 | 
			
		||||
@@ -25,7 +26,7 @@
 | 
			
		||||
        const resp = await fetch('/api/public/download', {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
 | 
			
		||||
            body: `token=${$token ?? ''}&node=${node.id}`
 | 
			
		||||
            body: `token=${rpc.token}&node=${node.id}`
 | 
			
		||||
        });
 | 
			
		||||
        if (resp.status != 200)
 | 
			
		||||
            return;
 | 
			
		||||
@@ -38,7 +39,7 @@
 | 
			
		||||
    onDestroy(() => { if (src.startsWith('blob')) URL.revokeObjectURL(src); });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<Button class="w-full mb-6" on:click={() => download([node])}><Download />Download</Button>
 | 
			
		||||
<Button class="w-full mb-6" on:click={() => download(rpc, [node])}><Download />Download</Button>
 | 
			
		||||
{#if can_display && !loading && src === ''}
 | 
			
		||||
    <Button class="w-full" outline on:click={load}>Load</Button>
 | 
			
		||||
{:else if loading}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ export {default as Password} from '~icons/carbon/Password';
 | 
			
		||||
export {default as CloudUpload} from '~icons/carbon/CloudUpload';
 | 
			
		||||
export {default as Checkmark} from '~icons/carbon/Checkmark';
 | 
			
		||||
export {default as Error} from '~icons/carbon/Error';
 | 
			
		||||
export {default as Share} from '~icons/carbon/Share';
 | 
			
		||||
 | 
			
		||||
export {default as CaretLeft} from '~icons/ph/CaretLeft';
 | 
			
		||||
export {default as OTP} from '~icons/ph/Password';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,5 @@
 | 
			
		||||
import "./app.pcss";
 | 
			
		||||
import App from "./App.svelte";
 | 
			
		||||
import {token} from './store';
 | 
			
		||||
import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
token.subscribe(v => {
 | 
			
		||||
    if (v == null) replace('/login').then()
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const app = new App({
 | 
			
		||||
    target: document.getElementById("app") as any,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								frontend/src/pages/Home.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/pages/Home.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { replace } from 'svelte-spa-router';
 | 
			
		||||
  import { globalRpc } from '../api';
 | 
			
		||||
 | 
			
		||||
    globalRpc.sessionInfo()
 | 
			
		||||
        .then(v => {
 | 
			
		||||
            if (v)
 | 
			
		||||
                replace('/view/0');
 | 
			
		||||
            else
 | 
			
		||||
                replace('/login');
 | 
			
		||||
        });
 | 
			
		||||
</script>
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
    import {Breadcrumb, Dropzone, Modal, Progressbar} from 'flowbite-svelte';
 | 
			
		||||
    import {writable} from 'svelte/store';
 | 
			
		||||
    import {CloudUpload} from '../icons';
 | 
			
		||||
    import {api, rpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
 | 
			
		||||
    import {api, globalRpc, token, type UploadFile, workingWrapper, workingWrapperR} from '../store';
 | 
			
		||||
    import DirViewer from '../components/DirViewer.svelte';
 | 
			
		||||
    import UploadModal from '../components/UploadModal.svelte';
 | 
			
		||||
    import FileViewer from '../components/FileViewer.svelte';
 | 
			
		||||
@@ -25,10 +25,10 @@
 | 
			
		||||
 | 
			
		||||
    const data = writable<Data>({node: null, segments: []});
 | 
			
		||||
    async function updateData(id: number) {
 | 
			
		||||
        let node = await workingWrapper(() => rpc.getNode(id));
 | 
			
		||||
        let node = await workingWrapper(() => globalRpc.getNode(id));
 | 
			
		||||
        if (!node)
 | 
			
		||||
            return;
 | 
			
		||||
        let segments = await workingWrapper(() => rpc.getPath(id));
 | 
			
		||||
        let segments = await workingWrapper(() => globalRpc.getPath(id));
 | 
			
		||||
        if (!segments)
 | 
			
		||||
            return;
 | 
			
		||||
        data.set({node: node as Data['node'], segments });
 | 
			
		||||
@@ -50,7 +50,7 @@
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(entry.name, parent, false));
 | 
			
		||||
        const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(entry.name, parent, false));
 | 
			
		||||
        if (!resp) return [];
 | 
			
		||||
        if (resp.isFile) return [];
 | 
			
		||||
        const reader = (entry as FileSystemDirectoryEntry).createReader();
 | 
			
		||||
@@ -109,7 +109,7 @@
 | 
			
		||||
        upload_progress_data.current = 0;
 | 
			
		||||
        const upload_files: UploadFile[] = [];
 | 
			
		||||
        for (const file of files) {
 | 
			
		||||
            const resp = await workingWrapperR<api.CreateNodeInfo>(() => rpc.createNode(file.name, file.id, true));
 | 
			
		||||
            const resp = await workingWrapperR<api.CreateNodeInfo>(() => globalRpc.createNode(file.name, file.id, true));
 | 
			
		||||
            if (resp && resp.isFile)
 | 
			
		||||
                upload_files.push({ ...file, id: resp.id, overwrite: resp.exists });
 | 
			
		||||
            upload_progress_data.current++;
 | 
			
		||||
@@ -145,9 +145,9 @@
 | 
			
		||||
    {#if $data.node === null}
 | 
			
		||||
        <!-- Waiting for data -->
 | 
			
		||||
    {:else if $data.node.file}
 | 
			
		||||
        <FileViewer node={$data.node} />
 | 
			
		||||
        <FileViewer rpc={globalRpc} node={$data.node} />
 | 
			
		||||
    {:else}
 | 
			
		||||
        <DirViewer node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
 | 
			
		||||
        <DirViewer rpc={globalRpc} path_prefix="#/view/" node={$data.node} on:reload_node={() => updateData($data.node?.id ?? 0)} />
 | 
			
		||||
    {/if}
 | 
			
		||||
</div>
 | 
			
		||||
<UploadModal bind:upload={real_upload} on:reload_node={() => updateData($data.node?.id ?? 0)} />
 | 
			
		||||
@@ -159,4 +159,4 @@
 | 
			
		||||
        </div>
 | 
			
		||||
        <Progressbar class="!mt-0" size="h-4" bind:progress={upload_progress} />
 | 
			
		||||
    </Modal>
 | 
			
		||||
{/if}
 | 
			
		||||
{/if}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Email, OTP, Password} from '../icons';
 | 
			
		||||
    import {rpc, token, workingWrapperR} from '../store';
 | 
			
		||||
    import {Email, OTP, Password} from '../../icons';
 | 
			
		||||
    import {globalRpc, token, workingWrapperR} from '../../store';
 | 
			
		||||
    import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
    let ask_tfa = false;
 | 
			
		||||
    let username = '', password = '', tfa = '';
 | 
			
		||||
 | 
			
		||||
    async function login() {
 | 
			
		||||
        const resp = await workingWrapperR(() => rpc.login(username, password, ask_tfa ? tfa : undefined));
 | 
			
		||||
        const resp = await workingWrapperR(() => globalRpc.login(username, password, ask_tfa ? tfa : undefined));
 | 
			
		||||
        if (!resp) return;
 | 
			
		||||
        if (resp.otpNeeded) {
 | 
			
		||||
            ask_tfa = true;
 | 
			
		||||
@@ -45,7 +45,7 @@
 | 
			
		||||
        <ButtonGroup class="w-full flex flex-nowrap">
 | 
			
		||||
            <Button class="flex-1 flex-grow" color="primary" outline href="#/signup">Signup</Button>
 | 
			
		||||
            <Button class="flex-1 flex-grow" color="primary" on:click={login}>Login</Button>
 | 
			
		||||
            <Button class="flex-1 flex-grow" color="primary" outline href="#/reset_pw">Forget password</Button>
 | 
			
		||||
            <Button class="flex-1 flex-grow" color="primary" outline href="#/reset_pw">Forgot password</Button>
 | 
			
		||||
        </ButtonGroup>
 | 
			
		||||
    {/if}
 | 
			
		||||
</Card>
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Email, EmailNew, Password} from '../icons';
 | 
			
		||||
    import {error_banner, info_banner, rpc, workingWrapper, workingWrapperO} from '../store';
 | 
			
		||||
    import {Email, EmailNew, Password} from '../../icons';
 | 
			
		||||
    import {error_banner, info_banner, globalRpc, workingWrapper, workingWrapperO} from '../../store';
 | 
			
		||||
    import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
    let enter_key = false;
 | 
			
		||||
    let username = '', key = '', password = '', password2 = '';
 | 
			
		||||
 | 
			
		||||
    async function sendKey() {
 | 
			
		||||
        await workingWrapper(() => rpc.send_recovery_key(username));
 | 
			
		||||
        await workingWrapper(() => globalRpc.send_recovery_key(username));
 | 
			
		||||
        info_banner.set('A message has been sent');
 | 
			
		||||
        enter_key = true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -19,7 +19,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (await workingWrapperO(() => rpc.reset_password(key, password)))
 | 
			
		||||
        if (await workingWrapperO(() => globalRpc.reset_password(key, password)))
 | 
			
		||||
            await replace('/login');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, ButtonGroup, Card, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Email, Password} from '../icons';
 | 
			
		||||
    import {error_banner, info_banner, rpc, workingWrapperO} from '../store';
 | 
			
		||||
    import {Email, Password} from '../../icons';
 | 
			
		||||
    import {error_banner, info_banner, globalRpc, workingWrapperO} from '../../store';
 | 
			
		||||
    import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
    let username = '', username2 = '', password = '', password2 = '';
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.signup(username, password));
 | 
			
		||||
        const resp = await workingWrapperO(() => globalRpc.signup(username, password));
 | 
			
		||||
 | 
			
		||||
        if (resp) {
 | 
			
		||||
            info_banner.set('Account created, please wait till an administrator approves it');
 | 
			
		||||
@@ -1,51 +1,51 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {api, rpc, session, token, workingWrapper} from '../store';
 | 
			
		||||
    import {api, globalRpc, session, token, workingWrapper} from '../../store';
 | 
			
		||||
    import {Checkbox, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell} from 'flowbite-svelte';
 | 
			
		||||
    import {Checkmark, Error} from '../icons';
 | 
			
		||||
    import LinkButton from '../components/LinkButton.svelte';
 | 
			
		||||
    import {Checkmark, Error} from '../../icons';
 | 
			
		||||
    import LinkButton from '../../components/LinkButton.svelte';
 | 
			
		||||
    import {replace} from 'svelte-spa-router';
 | 
			
		||||
 | 
			
		||||
    let users: api.UserInfo[] = [];
 | 
			
		||||
 | 
			
		||||
    async function fetchUsers() {
 | 
			
		||||
        const resp = await workingWrapper(() => rpc.admin.listUsers());
 | 
			
		||||
        const resp = await workingWrapper(() => globalRpc.admin.listUsers());
 | 
			
		||||
        users = resp || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function changeEnabled(user: number, target: boolean) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.setEnabled(user, target));
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.setEnabled(user, target));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function changeAdmin(user: number, target: boolean) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.setAdmin(user, target));
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.setAdmin(user, target));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function sudo(user: number) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.sudo(user))
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.sudo(user))
 | 
			
		||||
        await session.update('');
 | 
			
		||||
        await replace('/view/0');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function logout(user: number) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.logout(user));
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.logout(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function removeTfa(user: number) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.disableTfa(user));
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.disableTfa(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function deleteUser(user: number) {
 | 
			
		||||
        await workingWrapper(() => rpc.admin.deleteUser(user));
 | 
			
		||||
        await workingWrapper(() => globalRpc.admin.deleteUser(user));
 | 
			
		||||
        await fetchUsers();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function shutdown() {
 | 
			
		||||
        if (confirm('Do you really want to shutdown the server?')) {
 | 
			
		||||
            await rpc.admin.shutdown();
 | 
			
		||||
            await globalRpc.admin.shutdown();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {error_banner, rpc, session, token, workingWrapper, workingWrapperO} from '../store';
 | 
			
		||||
    import {error_banner, globalRpc, session, token, workingWrapper, workingWrapperO} from '../../store';
 | 
			
		||||
    import {Accordion, AccordionItem, Button, ButtonGroup, Input, InputAddon} from 'flowbite-svelte';
 | 
			
		||||
    import {Password} from '../icons';
 | 
			
		||||
    import {info_banner} from '../store.js';
 | 
			
		||||
    import {Password} from '../../icons';
 | 
			
		||||
    import {info_banner} from '../../store.js';
 | 
			
		||||
 | 
			
		||||
    const s = session.s;
 | 
			
		||||
    const tfa_enabled: boolean = $s?.tfaEnabled ?? false;
 | 
			
		||||
@@ -15,7 +15,7 @@
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const resp = await workingWrapperO(() => rpc.change_password(old, password));
 | 
			
		||||
        const resp = await workingWrapperO(() => globalRpc.change_password(old, password));
 | 
			
		||||
        if (resp) {
 | 
			
		||||
            info_banner.set('Changed password');
 | 
			
		||||
            change_pw_data.o = '';
 | 
			
		||||
@@ -26,18 +26,18 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function disableTfa() {
 | 
			
		||||
        await workingWrapper(() => rpc.tfaDisable());
 | 
			
		||||
        await workingWrapper(() => globalRpc.tfaDisable());
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function logoutAll() {
 | 
			
		||||
        await workingWrapper(() => rpc.logoutAll());
 | 
			
		||||
        await workingWrapper(() => globalRpc.logoutAll());
 | 
			
		||||
        token.set(null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function deleteAccount() {
 | 
			
		||||
        if (confirm("Do your really want to delete your account?")) {
 | 
			
		||||
            await workingWrapper(() => rpc.deleteAccount());
 | 
			
		||||
            await workingWrapper(() => globalRpc.deleteAccount());
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
    import {Button, ButtonGroup, Card, Input, InputAddon, StepIndicator, Tooltip} from 'flowbite-svelte';
 | 
			
		||||
    import {OTP} from '../icons';
 | 
			
		||||
    import {info_banner, rpc, session, token, workingWrapperO, workingWrapperR} from '../store';
 | 
			
		||||
    import {OTP} from '../../icons';
 | 
			
		||||
    import {info_banner, globalRpc, session, token, workingWrapperO, workingWrapperR} from '../../store';
 | 
			
		||||
    import QRCode from 'qrcode-svg';
 | 
			
		||||
 | 
			
		||||
    const s = session.s;
 | 
			
		||||
@@ -13,13 +13,13 @@
 | 
			
		||||
 | 
			
		||||
    async function startSetup(mail: boolean) {
 | 
			
		||||
        if (mail) {
 | 
			
		||||
            const resp = await workingWrapperO(() => rpc.tfaSetupMail());
 | 
			
		||||
            const resp = await workingWrapperO(() => globalRpc.tfaSetupMail());
 | 
			
		||||
            if (resp) {
 | 
			
		||||
                secret = null;
 | 
			
		||||
                step = 2;
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const resp = await workingWrapperR<string>(() => rpc.tfaSetupTotp());
 | 
			
		||||
            const resp = await workingWrapperR<string>(() => globalRpc.tfaSetupTotp());
 | 
			
		||||
            if (resp != null) {
 | 
			
		||||
                secret = resp.replaceAll('=', '');
 | 
			
		||||
                secret_qr_code = new QRCode({
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function completeSetup() {
 | 
			
		||||
        if (await workingWrapperO(() => rpc.tfaComplete(code))) {
 | 
			
		||||
        if (await workingWrapperO(() => globalRpc.tfaComplete(code))) {
 | 
			
		||||
            info_banner.set("Successfully set up two factor authentication");
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        }
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/src/pages/share/ShareHome.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/pages/share/ShareHome.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { replace } from 'svelte-spa-router';
 | 
			
		||||
  import { getRpcClient } from '../../api';
 | 
			
		||||
 | 
			
		||||
    export let params: {sid?: string} | undefined = {};
 | 
			
		||||
 | 
			
		||||
    let sid = params?.sid ?? '';
 | 
			
		||||
    if (sid.length != 30) {
 | 
			
		||||
        replace('/');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let rpc = getRpcClient();
 | 
			
		||||
    rpc.token = sid;
 | 
			
		||||
    rpc.sessionInfo().then(v => {
 | 
			
		||||
        let root = v?.shareRoot;
 | 
			
		||||
        if (!root) replace('/');
 | 
			
		||||
        else replace(`/share/${sid}/${root}`);
 | 
			
		||||
    })
 | 
			
		||||
</script>
 | 
			
		||||
							
								
								
									
										77
									
								
								frontend/src/pages/share/ShareView.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								frontend/src/pages/share/ShareView.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
  import { replace } from 'svelte-spa-router';
 | 
			
		||||
  import { getRpcClient } from '../../api';
 | 
			
		||||
  import { writable } from 'svelte/store';
 | 
			
		||||
  import { api, workingWrapper } from '../../store';
 | 
			
		||||
  import { Breadcrumb } from 'flowbite-svelte';
 | 
			
		||||
  import A from '../../components/A.svelte';
 | 
			
		||||
  import FileViewer from '../../components/FileViewer.svelte';
 | 
			
		||||
  import DirViewer from '../../components/DirViewer.svelte';
 | 
			
		||||
 | 
			
		||||
    export let params: {sid?: string, id?: string} | undefined = {};
 | 
			
		||||
 | 
			
		||||
    let sid = params?.sid ?? '';
 | 
			
		||||
    if (sid.length != 30) {
 | 
			
		||||
        replace('/');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let base_link = `#/share/${sid}/`;
 | 
			
		||||
 | 
			
		||||
    let rpc = getRpcClient();
 | 
			
		||||
    rpc.token = sid;
 | 
			
		||||
 | 
			
		||||
    $: {
 | 
			
		||||
        let id = 0;
 | 
			
		||||
        if (params && params.id) {
 | 
			
		||||
            id = parseInt(params.id);
 | 
			
		||||
            if (id >= 0)
 | 
			
		||||
                updateData(id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    interface Data {
 | 
			
		||||
        node: api.Node | null,
 | 
			
		||||
        segments: api.PathSegment[],
 | 
			
		||||
        root: number,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const data = writable<Data>({node: null, segments: [], root: 0});
 | 
			
		||||
    async function updateData(id: number) {
 | 
			
		||||
        let root = await workingWrapper(() => rpc.sessionInfo().then(v => v?.shareRoot));
 | 
			
		||||
        if (!root)
 | 
			
		||||
            return replace('/');
 | 
			
		||||
        let node = await workingWrapper(() => rpc.getNode(id));
 | 
			
		||||
        if (!node)
 | 
			
		||||
            return;
 | 
			
		||||
        let segments = await workingWrapper(() => rpc.getPath(id));
 | 
			
		||||
        if (!segments)
 | 
			
		||||
            return;
 | 
			
		||||
        rpc.share_root = root;
 | 
			
		||||
        data.set({node: node as Data['node'], segments, root });
 | 
			
		||||
    }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="w-full max-w-4xl">
 | 
			
		||||
    <div class="w-full flex mb-2 h-16">
 | 
			
		||||
        <Breadcrumb>
 | 
			
		||||
            {#each $data.segments as segment, i}
 | 
			
		||||
                {#if i > 0}<li class="inline-flex items-center">/</li>{/if}
 | 
			
		||||
                <li class="inline-flex items-center">
 | 
			
		||||
                    {#if segment.id !== null}
 | 
			
		||||
                        <A href={base_link + segment.id}>{segment.id === $data.root ? 'Share' : segment.name}</A>
 | 
			
		||||
                    {:else}
 | 
			
		||||
                        <span style="padding: 0 0.25em;">{segment.name}</span>
 | 
			
		||||
                    {/if}
 | 
			
		||||
                </li>
 | 
			
		||||
            {/each}
 | 
			
		||||
        </Breadcrumb>
 | 
			
		||||
        <span class="flex-1"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
    {#if $data.node === null}
 | 
			
		||||
        <!-- Waiting for data -->
 | 
			
		||||
    {:else if $data.node.file}
 | 
			
		||||
        <FileViewer rpc={rpc} node={$data.node} />
 | 
			
		||||
    {:else}
 | 
			
		||||
        <DirViewer rpc={rpc} path_prefix={base_link} node={$data.node} on:reload_node={() => updateData($data.node?.id ?? $data.root)} />
 | 
			
		||||
    {/if}
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,17 +1,27 @@
 | 
			
		||||
import Login from './pages/Login.svelte';
 | 
			
		||||
import Signup from './pages/Signup.svelte';
 | 
			
		||||
import ResetPassword from './pages/ResetPassword.svelte';
 | 
			
		||||
import Profile from './pages/Profile.svelte';
 | 
			
		||||
import TfaSetup from './pages/TfaSetup.svelte';
 | 
			
		||||
import Admin from './pages/Admin.svelte';
 | 
			
		||||
import Home from './pages/Home.svelte';
 | 
			
		||||
import Login from './pages/login/Login.svelte';
 | 
			
		||||
import Signup from './pages/login/Signup.svelte';
 | 
			
		||||
import ResetPassword from './pages/login/ResetPassword.svelte';
 | 
			
		||||
import Profile from './pages/profile/Profile.svelte';
 | 
			
		||||
import TfaSetup from './pages/profile/TfaSetup.svelte';
 | 
			
		||||
import Admin from './pages/profile/Admin.svelte';
 | 
			
		||||
import View from './pages/View.svelte';
 | 
			
		||||
import ShareHome from './pages/share/ShareHome.svelte';
 | 
			
		||||
import ShareView from './pages/share/ShareView.svelte';
 | 
			
		||||
 | 
			
		||||
export const routes = {
 | 
			
		||||
    '/': Home,
 | 
			
		||||
 | 
			
		||||
    '/login': Login,
 | 
			
		||||
    '/signup': Signup,
 | 
			
		||||
    '/reset_pw': ResetPassword,
 | 
			
		||||
 | 
			
		||||
    '/profile': Profile,
 | 
			
		||||
    '/tfa': TfaSetup,
 | 
			
		||||
    '/admin': Admin,
 | 
			
		||||
    '/view/:id': View
 | 
			
		||||
 | 
			
		||||
    '/view/:id': View,
 | 
			
		||||
 | 
			
		||||
    '/share/:sid': ShareHome,
 | 
			
		||||
    '/share/:sid/:id': ShareView
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import {type Session, rpc} from './api';
 | 
			
		||||
import {type Session, type RpcClient, globalRpc} from './api';
 | 
			
		||||
import {type Writable, writable} from 'svelte/store';
 | 
			
		||||
import {filesize} from 'filesize';
 | 
			
		||||
 | 
			
		||||
export * as api from './api';
 | 
			
		||||
export {rpc} from './api';
 | 
			
		||||
export {globalRpc, type RpcClient} from './api';
 | 
			
		||||
 | 
			
		||||
export interface UploadFile {
 | 
			
		||||
    id: number,
 | 
			
		||||
@@ -25,14 +25,11 @@ export const session: { s: Writable<Session|null>, update: (token: string|null)
 | 
			
		||||
            session.s.set(null);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const s = await rpc.sessionInfo();
 | 
			
		||||
        if (!s)
 | 
			
		||||
            token.set(null);
 | 
			
		||||
        else
 | 
			
		||||
            session.s.set(s);
 | 
			
		||||
        const s = await globalRpc.sessionInfo();
 | 
			
		||||
        session.s.set(s || null);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
token.subscribe(t => rpc.token = t ?? '');
 | 
			
		||||
token.subscribe(t => globalRpc.token = t ?? '');
 | 
			
		||||
token.subscribe(t => session.update(t));
 | 
			
		||||
 | 
			
		||||
token.subscribe(v => {
 | 
			
		||||
@@ -76,7 +73,7 @@ export async function workingWrapperR<T>(fn: () => Promise<{
 | 
			
		||||
    return resp.o as unknown as T;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function download<T extends {id:number, file:boolean}>(nodes: T[]) {
 | 
			
		||||
export async function download<T extends {id:number, file:boolean}>(rpc: RpcClient, nodes: T[]) {
 | 
			
		||||
    const form = document.createElement('form');
 | 
			
		||||
    form.method = 'POST';
 | 
			
		||||
    form.target = '_blank';
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user