[READ-ONLY] Mirror of https://github.com/probablykasper/time-machine-inspector. Time Machine backup size inspector app
backup macos tauri time-machine
0

Configure Feed

Select the types of activity you want to include in your feed.

Add sidebar folder explorer

Kasper (Jan 3, 2022, 10:23 PM +0100) 3e089ff4 0ec748d8

+197 -37
+103 -37
src/App.svelte
··· 1 1 <script lang="ts"> 2 - import { runCmd } from './lib/general' 2 + import { runCmd } from './general' 3 + import Item, { ItemClickEvent } from './Item.svelte' 4 + import { page } from './page' 5 + 6 + let rootPath = '/' 7 + let backups: DirMap | null = null 3 8 4 - let backups: string[] | null = [] 9 + type DirMap = { 10 + '/': string[] 11 + [name: string]: string[] 12 + } 13 + 14 + function parseStdout(stdout: string) { 15 + const paths = stdout.split('\n').filter((path) => path !== '') 16 + const dirmap: DirMap = { '/': [] } 17 + 18 + paths.forEach((path) => { 19 + let base = path.substring(path.lastIndexOf('/') + 1) 20 + let parent = path.substring(0, path.lastIndexOf('/')) 21 + do { 22 + if (parent === '') { 23 + dirmap['/'].push(base) 24 + break 25 + } else if (dirmap[parent] === undefined) { 26 + dirmap[parent] = [base] 27 + } else { 28 + dirmap[parent].push(base) 29 + break 30 + } 31 + base = parent.substring(parent.lastIndexOf('/') + 1) 32 + parent = parent.substring(0, parent.lastIndexOf('/')) 33 + } while (dirmap[parent] === undefined) 34 + }) 35 + 36 + console.log(dirmap) 37 + return dirmap 38 + } 39 + 40 + let stdout: string | null = null 41 + $: stdoutUpdate(stdout) 42 + function stdoutUpdate(stdout: string | null) { 43 + if (stdout === null) { 44 + backups = null 45 + } else { 46 + backups = parseStdout(stdout) 47 + if (backups['/'].length === 1 && backups['/'][0] === 'Volumes') { 48 + rootPath = '/Volumes' 49 + } else { 50 + rootPath = '/' 51 + } 52 + } 53 + } 5 54 6 55 let loading = false 7 56 async function loadBackups() { 8 - if (!loading) { 9 - loading = true 10 - const stdout: string | null = await runCmd('load_backups') 11 - if (stdout === null) { 12 - backups = null 13 - } else { 14 - backups = stdout.split('\n') 57 + if (loading) { 58 + return 59 + } 60 + loading = true 61 + stdout = (await runCmd('load_backups')) as string | null 62 + await new Promise((resolve) => setTimeout(resolve, 1000)) 63 + loading = false 64 + } 65 + 66 + function sidebarClick(e: ItemClickEvent) { 67 + if (e.detail.isFolder) { 68 + e.detail.toggleChildren() 69 + } else { 70 + $page = { 71 + name: e.detail.name, 72 + fullPath: e.detail.fullPath, 15 73 } 16 - await new Promise((resolve) => setTimeout(resolve, 1000)) 17 - loading = false 18 74 } 19 75 } 20 76 </script> 21 77 22 - <div class="border" /> 23 78 <div class="sidebar"> 24 - <div class="button" on:click={loadBackups} class:disabled={loading} tabindex="0"> 79 + <button on:click={loadBackups} class:disabled={loading} tabindex="0"> 25 80 {#if loading} 26 81 Loading... 27 82 {:else} 28 83 Load Backups 29 84 {/if} 30 - </div> 85 + </button> 31 86 {#if backups !== null} 32 - {#each backups as backup} 33 - <div class="backup">{backup}</div> 87 + {#each backups[rootPath] as child} 88 + {#if rootPath === '/'} 89 + <Item 90 + map={backups} 91 + selectedPath={$page.fullPath + '/' + $page.name} 92 + name={child} 93 + fullPath={'/' + child} 94 + on:click={sidebarClick} /> 95 + {:else} 96 + <Item 97 + map={backups} 98 + selectedPath={$page.fullPath + '/' + $page.name} 99 + name={child} 100 + fullPath={rootPath + '/' + child} 101 + on:click={sidebarClick} /> 102 + {/if} 34 103 {/each} 35 104 {/if} 36 105 </div> ··· 52 121 border-top: 0px 53 122 background-image: radial-gradient(circle at 50% 27%, hsl(230, 65%, 33%), transparent 100%),radial-gradient(circle at 0% 90%, hsl(230, 53%, 38%), transparent 40%),radial-gradient(circle at 5% 1%, hsl(230, 71%, 4%), transparent 100%),radial-gradient(circle at 50% 50%, #000000, #000000 100%) 54 123 .sidebar 55 - width: 200px 56 124 flex-shrink: 0 125 + flex-grow: 1 126 + min-width: 220px 127 + max-width: 35% 57 128 height: 100% 58 129 box-sizing: border-box 59 130 overflow: auto 60 - padding: 15px 61 131 backdrop-filter: brightness(80%) 62 - color: hsla(0, 0%, 0%, 0.7) 63 132 border: 0px 64 - border-right: 1px solid hsla(230, 100%, 93%, 0.5) 65 - border-image: radial-gradient(ellipse at bottom right, hsla(230, 100%, 93%, 0.1), transparent) 1 / 1px 66 - border-image-outset: 1px 133 + border-right: 1px solid hsla(230, 100%, 85%, 0.12) 67 134 display: flex 68 135 flex-direction: column 69 - align-items: center 70 - .button 71 - margin-bottom: 15px 72 136 .page 73 137 width: 100% 74 138 height: 100% 75 - .button 139 + button 76 140 user-select: none 77 141 cursor: default 78 142 outline: none 79 - width: 100% 80 143 box-sizing: border-box 81 144 padding: 9px 18px 82 145 text-align: center 83 146 font-size: 13px 84 - border: 1px solid hsla(230, 100%, 70%, 0.1) 147 + margin: 15px 148 + border: 1px solid hsla(230, 100%, 80%, 0.1) 85 149 border-radius: 5px 86 - background-color: hsla(230, 80%, 70%, 0.1) 87 - color: hsla(216, 100%, 60%, 0.6) 88 - font-weight: 600 89 - text-shadow: 0 0.1rem 0.2rem hsl(270, 58%, 54%, 0.36) 90 - transition: all 360ms $easing 91 - &.disabled 150 + background-color: hsla(230, 80%, 75%, 0.2) 151 + color: hsla(230, 100%, 90%, 0.8) 152 + font-weight: 500 153 + text-shadow: 0px 0.1em 0.8em hsla(230, 30%, 7%, 1) 154 + transition: all 240ms $easing 155 + &:focus 156 + box-shadow: 0px 0px 0px 2px hsla(230, 100%, 80%, 0.5) 157 + &.disabled, &:hover 158 + font-weight: 700 159 + letter-spacing: 0.02em 160 + &.disabled, &:active 92 161 opacity: 0.75 93 - box-shadow: 0px 4px 30px 0px hsla(270, 30%, 35%, 0) 94 - .backup 95 - font-size: 13px 96 162 </style>
+88
src/Item.svelte
··· 1 + <script lang="ts" context="module"> 2 + export type ItemClickEventDetail = { 3 + name: string 4 + fullPath: string 5 + isFolder: boolean 6 + toggleChildren: () => void 7 + } 8 + export type ItemClickEvent = CustomEvent<ItemClickEventDetail> 9 + </script> 10 + 11 + <script lang="ts"> 12 + import { createEventDispatcher } from 'svelte' 13 + 14 + type DirMap = { 15 + '/': string[] 16 + [name: string]: string[] 17 + } 18 + 19 + export let map: DirMap 20 + 21 + export let name: string 22 + export let fullPath: string 23 + $: isFolder = map[fullPath] !== undefined 24 + 25 + export let selectedPath: string 26 + export let open = true 27 + 28 + export let indentLevel = 0 29 + 30 + const dispatch = createEventDispatcher<{ click: ItemClickEventDetail }>() 31 + 32 + function itemClick() { 33 + dispatch('click', { 34 + name, 35 + fullPath, 36 + isFolder, 37 + toggleChildren: () => { 38 + open = !open 39 + }, 40 + }) 41 + } 42 + </script> 43 + 44 + <div 45 + class="item" 46 + class:open 47 + on:click={itemClick} 48 + style={`padding-left: ${14 * indentLevel + 10}px`} 49 + class:selected={selectedPath === fullPath + '/' + name}> 50 + {#if isFolder} 51 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" 52 + ><path d="M21 12l-18 12v-24z" /></svg> 53 + {/if} 54 + {name} 55 + </div> 56 + <div class="children"> 57 + {#if open && isFolder} 58 + {#each map[fullPath] as child} 59 + <svelte:self 60 + {map} 61 + {selectedPath} 62 + name={child} 63 + fullPath={fullPath + '/' + child} 64 + on:click 65 + indentLevel={indentLevel + 1} /> 66 + {/each} 67 + {/if} 68 + </div> 69 + 70 + <style lang="sass"> 71 + .item 72 + font-size: 14px 73 + color: hsla(216, 50%, 70%, 0.75) 74 + cursor: default 75 + user-select: none 76 + padding: 4px 0px 77 + box-sizing: border-box 78 + width: 100% 79 + &.selected 80 + background-color: hsla(216, 70%, 70%, 0.2) 81 + &.open svg 82 + transform: rotate(90deg) 83 + svg 84 + fill: hsla(216, 50%, 70%, 0.75) 85 + width: 10px 86 + height: 10px 87 + margin-right: 1px 88 + </style>
src/lib/general.ts src/general.ts
+6
src/page.ts
··· 1 + import { writable } from 'svelte/store' 2 + 3 + export const page = writable({ 4 + fullPath: '', 5 + name: '', 6 + })