Browse Source

Apps UI (#272)

0.3.14
Mayank Chhabra 4 years ago
committed by GitHub
parent
commit
57ae2a9cb1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      src/assets/app-store/dependencies/bitcoind.svg
  2. 14
      src/assets/app-store/dependencies/electrum.svg
  3. 14
      src/assets/app-store/dependencies/lnd.svg
  4. 115
      src/components/AuthenticatedVerticalNavbar.vue
  5. 93
      src/components/InstalledApp.vue
  6. 58
      src/global-styles/custom.scss
  7. 100
      src/global-styles/responsive.scss
  8. 2
      src/helpers/api.js
  9. 36
      src/layouts/DashboardLayout.vue
  10. 49
      src/router/index.js
  11. 4
      src/store/index.js
  12. 126
      src/store/modules/apps.js
  13. 9
      src/store/modules/user.js
  14. 127
      src/views/AppStore.vue
  15. 268
      src/views/AppStoreApp.vue
  16. 80
      src/views/Apps.vue

15
src/assets/app-store/dependencies/bitcoind.svg

@ -0,0 +1,15 @@
<svg width="66" height="66" viewBox="0 0 66 66" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="66" height="66" rx="12" fill="url(#paint0_linear)"/>
<g clip-path="url(#clip0)">
<path d="M45.6091 36.9016C44.9803 35.5355 43.8114 34.427 42.1024 33.5767C44.6772 32.9335 46.1499 31.173 46.5216 28.295C46.6642 27.2531 46.5864 26.3037 46.2877 25.4453C45.9888 24.5865 45.4909 23.8368 44.7918 23.1951C44.0935 22.553 43.2995 22.0036 42.4123 21.5464C41.5248 21.0892 40.4826 20.6653 39.2863 20.2753L40.6354 14.4317L37.0646 13.6073L35.753 19.2885C35.1347 19.1458 34.1882 18.9435 32.9135 18.6818L34.2359 12.9543L30.6647 12.1298L29.3156 17.9733C28.8017 17.871 28.0502 17.7058 27.0609 17.4774L22.1504 16.3191L21.2724 20.1221L23.8462 20.7163C25.0211 20.9876 25.5876 21.6721 25.5459 22.7696L24.0094 29.4248C24.1642 29.4606 24.2859 29.497 24.3752 29.5339L24.004 29.4482L21.852 38.7696C21.5465 39.5294 21.0072 39.8204 20.2341 39.6419L17.6606 39.0477L15.9619 43.1254L20.5997 44.1961C20.8934 44.2639 21.3174 44.3659 21.8725 44.5014C22.4272 44.6377 22.8435 44.7379 23.1218 44.8021L21.7567 50.7151L25.3285 51.5397L26.6775 45.6963C27.323 45.862 28.2642 46.0872 29.5009 46.3727L28.1573 52.1928L31.728 53.0172L33.0932 47.1042C34.3753 47.3352 35.5265 47.4795 36.5471 47.5354C37.5682 47.5914 38.5691 47.542 39.5494 47.3854C40.5295 47.2293 41.3837 46.9499 42.1114 46.548C42.8393 46.1467 43.4963 45.5734 44.084 44.8296C44.6703 44.0856 45.1449 43.1775 45.5062 42.1045C46.204 40.0029 46.2379 38.2683 45.6091 36.9016ZM32.0195 22.8712C32.1278 22.8962 32.434 22.9628 32.9382 23.071C33.4427 23.1791 33.8617 23.2678 34.1958 23.3367C34.53 23.4058 34.9739 23.5287 35.5276 23.7054C36.0812 23.882 36.5421 24.0537 36.9107 24.2201C37.2792 24.3864 37.6753 24.6122 38.0976 24.897C38.5208 25.1818 38.8415 25.4839 39.0606 25.8031C39.2802 26.1224 39.4409 26.5016 39.5428 26.9404C39.6453 27.3792 39.6375 27.8537 39.5198 28.3637C39.4198 28.7966 39.2594 29.1749 39.0383 29.4986C38.8175 29.8221 38.5395 30.0714 38.2063 30.2469C37.8729 30.4223 37.5382 30.5651 37.2034 30.6749C36.8689 30.7848 36.4607 30.8413 35.9798 30.8441C35.499 30.8471 35.0813 30.8403 34.7272 30.8235C34.3728 30.8069 33.9379 30.7514 33.4223 30.6567C32.9067 30.5619 32.5165 30.4884 32.2521 30.4354C31.9874 30.3824 31.6281 30.2994 31.1736 30.1865C30.7191 30.0735 30.4537 30.0079 30.3762 29.99L32.0198 22.8712L32.0195 22.8711L32.0195 22.8712ZM38.125 40.9136C37.8956 41.2354 37.6351 41.5011 37.3427 41.7106C37.05 41.9193 36.6909 42.0809 36.266 42.1942C35.8411 42.3081 35.4362 42.3853 35.0523 42.427C34.6679 42.469 34.2139 42.4776 33.6905 42.4548C33.166 42.4314 32.7106 42.3951 32.3225 42.3465C31.935 42.2978 31.4759 42.2242 30.9447 42.1262C30.4141 42.0277 30.0014 41.9448 29.7078 41.877C29.414 41.8092 29.0439 41.7193 28.5973 41.6083C28.1504 41.4975 27.8656 41.4276 27.7418 41.399L29.5514 33.561C29.6749 33.5895 30.0439 33.6667 30.6583 33.792C31.2727 33.9174 31.7727 34.0252 32.1592 34.1145C32.5456 34.2037 33.0739 34.3497 33.7436 34.5535C34.4127 34.7565 34.9688 34.9578 35.4115 35.1577C35.8537 35.3577 36.3266 35.6172 36.8311 35.9375C37.3352 36.2573 37.7255 36.5957 38.0014 36.9525C38.2772 37.3097 38.4811 37.7307 38.6129 38.2172C38.7454 38.7036 38.7471 39.2252 38.6185 39.7818C38.5184 40.2151 38.3542 40.5918 38.125 40.9136Z" fill="white"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="0" y1="0" x2="66" y2="66" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFBE40"/>
<stop offset="1" stop-color="#FF9C40"/>
</linearGradient>
<clipPath id="clip0">
<rect x="18.1479" y="9.24023" width="39.6" height="39.6" transform="rotate(13 18.1479 9.24023)" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

14
src/assets/app-store/dependencies/electrum.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

14
src/assets/app-store/dependencies/lnd.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

115
src/components/AuthenticatedVerticalNavbar.vue

@ -4,7 +4,7 @@
<div class="balance-container w-100 px-3 pt-4 pb-3 mb-3">
<p class="text-muted">
Balance
<span style="cursor: pointer;" @click="toggleBalance">
<span style="cursor: pointer" @click="toggleBalance">
<!-- <b-icon :icon="state.showBalance ? 'eye-slash-fill' : 'eye-fill'"></b-icon> -->
</span>
</p>
@ -15,7 +15,7 @@
<CountUp
:value="{
endVal: walletBalance,
decimalPlaces: unit === 'sats' ? 0 : 5
decimalPlaces: unit === 'sats' ? 0 : 5,
}"
/>
</h3>
@ -34,7 +34,7 @@
</div>
<!-- <div class="py-2"></div> -->
<b-nav vertical class="px-1">
<b-nav-item to="/dashboard" class="my-1" exact-active-class="active">
<b-nav-item to="/dashboard" class="my-1" active-class="active">
<svg
width="24"
height="24"
@ -53,7 +53,7 @@
</svg>
Home
</b-nav-item>
<b-nav-item to="/bitcoin" class="my-1" exact-active-class="active">
<b-nav-item to="/bitcoin" class="my-1" active-class="active">
<svg
width="24"
height="24"
@ -69,7 +69,7 @@
</svg>
Bitcoin
</b-nav-item>
<b-nav-item to="/lightning" class="my-1" exact-active-class="active">
<b-nav-item to="/lightning" class="my-1" active-class="active">
<svg
width="24"
height="24"
@ -86,11 +86,69 @@
Lightning
</b-nav-item>
<b-nav-item to="/apps" class="my-1" active-class="active">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="mr-2"
>
<path
d="M8.82158 6.21425H18.7859V5.89282C18.7859 5.36029 18.3541 4.92853 17.8216 4.92853H6.893C6.36047 4.92853 5.92871 5.36029 5.92871 5.89282V18.1071C5.92871 18.6397 6.36047 19.0714 6.893 19.0714H7.21443V7.82139C7.21553 6.93417 7.93435 6.21534 8.82158 6.21425Z"
fill="#C3C6D1"
/>
<path
d="M20.7141 9.42853H7.85693V20.0357C7.85693 20.5682 8.2887 21 8.82122 21H19.7498C20.2824 21 20.7141 20.5682 20.7141 20.0357V9.42853Z"
fill="#C3C6D1"
/>
<path
d="M6.89287 4.28572H16.8572V3.96429C16.8572 3.43176 16.4254 3 15.8929 3H4.96429C4.43176 3 4 3.43176 4 3.96429V16.1786C4 16.7111 4.43176 17.1429 4.96429 17.1429H5.28572V5.89287C5.28682 5.00564 6.00564 4.28682 6.89287 4.28572Z"
fill="#C3C6D1"
/>
<path
d="M20.7141 7.82147C20.7141 7.28894 20.2824 6.85718 19.7498 6.85718H8.82122C8.2887 6.85718 7.85693 7.28894 7.85693 7.82147V8.78576H20.7141V7.82147ZM9.11691 7.94357C9.10059 7.9825 9.07768 8.01844 9.04942 8.04967C8.95745 8.14164 8.81871 8.16848 8.69912 8.11716C8.65972 8.10162 8.62378 8.0787 8.59302 8.04967C8.56477 8.01844 8.54185 7.9825 8.52553 7.94357C8.49132 7.86573 8.49132 7.77721 8.52553 7.69936C8.54107 7.65997 8.56398 7.62403 8.59302 7.59326C8.72187 7.4743 8.92057 7.4743 9.04942 7.59326C9.07846 7.62403 9.10137 7.65997 9.11691 7.69936C9.15128 7.77721 9.15128 7.86573 9.11691 7.94357ZM10.0812 7.94357C10.0649 7.9825 10.042 8.01844 10.0137 8.04967C9.92174 8.14164 9.783 8.16848 9.66341 8.11716C9.62448 8.10083 9.58854 8.07808 9.55731 8.04967C9.52906 8.01844 9.50614 7.9825 9.48982 7.94357C9.47177 7.90543 9.46298 7.86369 9.46408 7.82147C9.46455 7.7794 9.47334 7.73797 9.48982 7.69936C9.50536 7.65997 9.52827 7.62403 9.55731 7.59326C9.68616 7.4743 9.88486 7.4743 10.0137 7.59326C10.0427 7.62403 10.0657 7.65997 10.0812 7.69936C10.1156 7.77721 10.1156 7.86573 10.0812 7.94357ZM10.978 8.04967C10.886 8.14243 10.7471 8.1702 10.6266 8.12014C10.5059 8.07007 10.4276 7.95205 10.4284 7.82147C10.4273 7.77925 10.4361 7.7375 10.4541 7.69936C10.4696 7.65997 10.4926 7.62403 10.5216 7.59326C10.6505 7.4743 10.8491 7.4743 10.978 7.59326C11.007 7.62403 11.03 7.65997 11.0455 7.69936C11.0968 7.81896 11.07 7.9577 10.978 8.04967Z"
fill="#C3C6D1"
/>
</svg>
Apps
</b-nav-item>
<b-nav-item to="/app-store" class="my-1" active-class="active">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="mr-2"
>
<path
d="M4.58203 3C3.7097 3 3 3.7097 3 4.58203V11.0156H11.0156V3H4.58203ZM8.23828 7.53516H7.53516V8.23828C7.53516 8.52952 7.29905 8.76563 7.00781 8.76563C6.71658 8.76563 6.48047 8.52952 6.48047 8.23828V7.53516H5.77734C5.48611 7.53516 5.25 7.29905 5.25 7.00781C5.25 6.71658 5.48611 6.48047 5.77734 6.48047H6.48047V5.77734C6.48047 5.48611 6.71658 5.25 7.00781 5.25C7.29905 5.25 7.53516 5.48611 7.53516 5.77734V6.48047H8.23828C8.52952 6.48047 8.76563 6.71658 8.76563 7.00781C8.76563 7.29905 8.52952 7.53516 8.23828 7.53516Z"
fill="#C3C6D1"
/>
<path
d="M19.7989 11.0156H20.0859V4.58203C20.0859 3.7097 19.3762 3 18.5039 3H12.0703V10.8254C13.114 10.0402 14.4108 9.57422 15.8145 9.57422C17.328 9.57422 18.7172 10.116 19.7989 11.0156ZM14.3203 7.00781C14.3203 6.71658 14.5564 6.48047 14.8477 6.48047H17.3086C17.5998 6.48047 17.8359 6.71658 17.8359 7.00781C17.8359 7.29905 17.5998 7.53516 17.3086 7.53516H14.8477C14.5564 7.53516 14.3203 7.29905 14.3203 7.00781Z"
fill="#C3C6D1"
/>
<path
d="M10.8254 12.0703H3V18.5039C3 19.3762 3.7097 20.0859 4.58203 20.0859H11.0156V19.7989C10.116 18.7172 9.57422 17.328 9.57422 15.8145C9.57422 14.4108 10.0402 13.114 10.8254 12.0703ZM8.25959 16.5842C8.46553 16.7901 8.46553 17.124 8.25959 17.3299C8.15665 17.4329 8.02168 17.4844 7.88672 17.4844C7.75175 17.4844 7.61679 17.4329 7.51385 17.3299L7.00781 16.8239L6.50177 17.3299C6.39884 17.4329 6.26387 17.4844 6.12891 17.4844C5.99394 17.4844 5.85898 17.4329 5.75604 17.3299C5.55009 17.124 5.55009 16.7901 5.75604 16.5841L6.26204 16.0781L5.756 15.5721C5.55006 15.3661 5.55006 15.0323 5.756 14.8263C5.96191 14.6204 6.29583 14.6204 6.50177 14.8263L7.00781 15.3324L7.51385 14.8263C7.71976 14.6204 8.05368 14.6204 8.25962 14.8263C8.46557 15.0323 8.46557 15.3661 8.25962 15.5721L7.75358 16.0781L8.25959 16.5842Z"
fill="#C3C6D1"
/>
<path
d="M15.8145 10.6289C12.9551 10.6289 10.6289 12.9551 10.6289 15.8145C10.6289 18.6738 12.9551 21 15.8145 21C18.6738 21 21 18.6738 21 15.8145C21 12.9551 18.6738 10.6289 15.8145 10.6289ZM17.0449 17.2207H14.584C14.2928 17.2207 14.0566 16.9846 14.0566 16.6934C14.0566 16.4021 14.2928 16.166 14.584 16.166H17.0449C17.3362 16.166 17.5723 16.4021 17.5723 16.6934C17.5723 16.9846 17.3362 17.2207 17.0449 17.2207ZM17.0449 15.4629H14.584C14.2928 15.4629 14.0566 15.2268 14.0566 14.9355C14.0566 14.6443 14.2928 14.4082 14.584 14.4082H17.0449C17.3362 14.4082 17.5723 14.6443 17.5723 14.9355C17.5723 15.2268 17.3362 15.4629 17.0449 15.4629Z"
fill="#C3C6D1"
/>
</svg>
App Store
</b-nav-item>
<b-nav-item
to="/settings"
class="my-1"
v-if="isMobileMenu"
exact-active-class="active"
active-class="active"
>
<svg
width="24"
@ -115,7 +173,7 @@
@click="logout"
class="my-1"
v-if="isMobileMenu"
exact-active-class="active"
active-class="active"
>
<svg
width="24"
@ -136,12 +194,7 @@
</svg>
Log out
</b-nav-item>
<b-nav-item
to="/settings"
class="my-1"
v-else
exact-active-class="active"
>
<b-nav-item to="/settings" class="my-1" v-else active-class="active">
<svg
width="24"
height="24"
@ -159,6 +212,15 @@
</b-nav-item>
</b-nav>
</div>
<!-- Preload all app icons to cache them locally -->
<div class="d-none">
<img
v-for="app in appStore"
:key="app.id"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/icon.svg`"
class="d-none"
/>
</div>
</div>
</template>
@ -172,15 +234,19 @@ export default {
data() {
return {
state: {
showBalance: true
}
showBalance: true,
},
};
},
props: {
isMobileMenu: Boolean,
},
computed: {
...mapState({
btcBalance: state => state.bitcoin.balance.total,
lightningBalance: state => state.lightning.balance.total,
unit: state => state.system.unit
btcBalance: (state) => state.bitcoin.balance.total,
lightningBalance: (state) => state.lightning.balance.total,
unit: (state) => state.system.unit,
appStore: (state) => state.apps.store,
}),
walletBalance() {
return this.unit === "sats"
@ -189,7 +255,7 @@ export default {
},
balanceLoaded() {
return this.btcBalance >= 0 && this.lightningBalance >= 0;
}
},
},
methods: {
logout() {
@ -197,15 +263,16 @@ export default {
},
toggleBalance() {
return (this.state.showBalance = !this.state.showBalance);
}
},
},
props: {
isMobileMenu: Boolean
created() {
this.$store.dispatch("apps/getInstalledApps");
this.$store.dispatch("apps/getAppStore");
},
components: {
CountUp,
SatsBtcSwitch
}
SatsBtcSwitch,
},
};
</script>

93
src/components/InstalledApp.vue

@ -0,0 +1,93 @@
<template>
<div class="pb-3 mb-2 installed-app d-flex flex-column align-items-center">
<a
class="d-block mb-3 installed-app-link"
:href="url"
target="_blank"
:class="isUninstalling ? 'fade-in-out' : ''"
:disabled="isUninstalling"
><img
class="installed-app-icon app-icon"
:alt="name"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${id}/icon.svg`"
/></a>
<span class="text-center text-truncate mb-1">{{
isUninstalling ? "Uninstalling..." : name
}}</span>
<b-button
class="uninstall-btn"
v-if="showUninstallButton && !isUninstalling"
variant="outline-danger"
size="sm"
@click="uninstall(name, id)"
><small><b-icon icon="trash"></b-icon> Uninstall</small></b-button
>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
props: {
id: String,
name: String,
hiddenService: String,
port: Number,
path: String,
showUninstallButton: {
type: Boolean,
default: false,
},
isUninstalling: {
type: Boolean,
default: false,
},
},
data() {
return {};
},
computed: {
...mapState({
installedApps: (state) => state.apps.installed,
}),
url: function () {
if (window.location.origin.indexOf(".onion") > 0) {
return `http://${this.hiddenService}${this.path}`;
} else {
return `http://${window.location.hostname}:${this.port}${this.path}`;
}
},
},
methods: {
uninstall(name, appId) {
if (
!window.confirm(
`Are you sure you want to uninstall ${name}? This is will also delete all of its data.`
)
) {
return;
}
this.$store.dispatch("apps/uninstall", appId);
},
},
components: {},
};
</script>
<style lang="scss" scoped>
.installed-app {
width: 200px;
position: relative;
.installed-app-link {
text-decoration: none;
.installed-app-icon {
box-shadow: 0px 10px 30px rgba(209, 213, 223, 0.5);
}
}
.uninstall-btn {
position: absolute;
bottom: 0;
}
}
</style>

58
src/global-styles/custom.scss

@ -54,9 +54,12 @@ a {
&.btn-danger,&.btn-warning {
color: #fff;
}
&.btn-outline-danger:hover {
color: #fff;
&.btn-outline-danger {
&:hover,&:active {
color: #fff !important;
}
}
}
.card {
@ -457,4 +460,53 @@ body {
// Helper class to support text with "\n" newline characters
.text-newlines {
white-space: pre-line;
}
}
// Helper class to truncate text with "..."
.text-truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
// Helper class for fade in and out animation
.fade-in-out {
animation: fadeInOut 1s infinite linear;
}
@keyframes fadeInOut {
0%,
100% {
opacity: 0.6;
}
50% {
opacity: 0.3;
}
}
.app-icon {
height: 100px;
width: 100px;
border-radius: 20px;
object-fit: contain;
align-items: flex-start;
}
.app-gallery {
display: flex;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
margin-left: calc(-15px - 1.5rem);
margin-right: calc(-15px - 1.5rem);
padding-left: calc(30px + 0.75rem);
padding-right: calc(30px + 0.75rem);
.app-gallery-screen {
height: 350px;
width: auto;
display: block;
box-shadow: 0px 10px 30px rgba(209, 213, 223, 0.8);
border-radius: 5px;
object-fit: contain;
}
}

100
src/global-styles/responsive.scss

@ -3,4 +3,102 @@
font-size: $font-size-base * 4;
line-height: $font-size-base * 4;
}
}
}
.card-columns {
column-count: 1;
@include media-breakpoint-up(md) {
column-count: 2;
}
column-gap: 30px;
}
.app-name {
@include media-breakpoint-down(md) {
font-size: 1.4rem;
}
@include media-breakpoint-only(lg) {
font-size: 1.6rem;
}
}
@include media-breakpoint-only(xs) {
.app-icon {
height: 66px !important;
width: 66px !important;
border-radius: 12px !important;
}
.installed-app {
width: calc(50% - 1rem) !important;
}
.w-xs-100 {
width: 100%;
}
.app-gallery {
margin-left: -15px;
margin-right: -15px;
padding-left: 15px;
padding-right: 15px;
.app-gallery-screen {
height: auto; // so it doesn't break on Chrome (i.e. show white padding on top/bottom)
height: intrinsic; // so it doesn't break on Safari (i.e. show white padding on top/bottom) https://stackoverflow.com/a/64108625
width: calc(100vw);
}
}
}
@include media-breakpoint-only(sm) {
.app-icon {
height: 70px;
width: 70px;
border-radius: 14px;
}
.app-gallery {
margin-left: calc(-15px - 0.75rem);
margin-right: calc(-15px - 0.75rem);
padding-left: 30px;
padding-right: 30px;
.app-gallery-screen {
height: 280px;
}
}
}
@include media-breakpoint-only(md) {
.app-icon {
height: 80px;
width: 80px;
border-radius: 16px;
}
.app-gallery {
margin-left: calc(-15px - 0.75rem);
margin-right: calc(-15px - 0.75rem);
padding-left: 30px;
padding-right: 30px;
.app-gallery-screen {
height: 300px;
}
}
}
@include media-breakpoint-only(lg) {
.app-gallery {
margin-left: calc(-15px - 0.75rem);
margin-right: calc(-15px - 0.75rem);
padding-left: 30px;
padding-right: 30px;
}
.app-icon {
height: 80px;
width: 80px;
border-radius: 16px;
}
}
@include media-breakpoint-up(lg) {
.app-icon.app-icon-lg {
height: 140px;
width: 140px;
border-radius: 24px;
}
}

2
src/helpers/api.js

@ -122,7 +122,7 @@ const API = {
},
// Wrap a post call
async post(url, data, auth = true) {
async post(url, data = {}, auth = true) {
const requestOptions = {
method: "post",
url,

36
src/layouts/DashboardLayout.vue

@ -149,7 +149,7 @@
</div>
</div>
</b-modal>
<div class="pr-xl-2">
<div class="pr-xl-2 content-container">
<b-alert
class="mt-4 mb-0"
variant="success"
@ -158,9 +158,7 @@
>
<b-icon icon="bell-fill" class="mr-2"></b-icon>
<a
:href="
`https://github.com/getumbrel/umbrel/releases/tag/v${availableUpdate.version}`
"
:href="`https://github.com/getumbrel/umbrel/releases/tag/v${availableUpdate.version}`"
target="_blank"
class="alert-link"
>Umbrel v{{ availableUpdate.version }}</a
@ -191,6 +189,8 @@
<p>
<small>
<a href="https://getumbrel.com" target="_blank">getumbrel.com</a>
|
<a href="https://t.me/getumbrel" target="_blank">chat</a>
</small>
</p>
</footer>
@ -207,21 +207,21 @@ import AuthenticatedVerticalNavbar from "@/components/AuthenticatedVerticalNavba
export default {
data() {
return {
isUpdating: false
isUpdating: false,
};
},
computed: {
...mapState({
name: state => state.user.name,
chain: state => state.bitcoin.chain,
availableUpdate: state => state.system.availableUpdate,
updateStatus: state => state.system.updateStatus,
showUpdateConfirmationModal: state =>
state.system.showUpdateConfirmationModal
name: (state) => state.user.name,
chain: (state) => state.bitcoin.chain,
availableUpdate: (state) => state.system.availableUpdate,
updateStatus: (state) => state.system.updateStatus,
showUpdateConfirmationModal: (state) =>
state.system.showUpdateConfirmationModal,
}),
isMobileMenuOpen() {
return this.$store.getters.isMobileMenuOpen;
}
},
},
methods: {
logout() {
@ -280,10 +280,10 @@ export default {
autoHideDelay: 3000,
variant: "danger",
solid: true,
toaster: "b-toaster-bottom-right"
toaster: "b-toaster-bottom-right",
});
}
}
},
},
created() {
//load this data once:
@ -301,8 +301,8 @@ export default {
},
watch: {},
components: {
AuthenticatedVerticalNavbar
}
AuthenticatedVerticalNavbar,
},
};
</script>
@ -317,6 +317,10 @@ export default {
top: 0;
}
.content-container {
min-height: calc(100vh - 150px);
}
.input-search-form {
form {
position: relative;

49
src/router/index.js

@ -12,6 +12,9 @@ import Login from "../views/Login.vue";
import Dashboard from "../views/Dashboard.vue";
import Bitcoin from "../views/Bitcoin.vue";
import Lightning from "../views/Lightning.vue";
import Apps from "../views/Apps.vue";
import AppStore from "../views/AppStore.vue";
import AppStoreApp from "../views/AppStoreApp.vue";
import Settings from "../views/Settings.vue";
import Logout from "../views/Logout.vue";
@ -82,6 +85,35 @@ const routes = [
}
]
},
{
path: "/apps",
component: DashboardLayout,
meta: { requiresAuth: true },
children: [
{
path: "",
name: "apps",
component: Apps
}
]
},
{
path: "/app-store",
component: DashboardLayout,
meta: { requiresAuth: true },
children: [
{
path: "",
name: "app-store",
component: AppStore
},
{
path: ":id",
name: "app-store-app",
component: AppStoreApp
}
]
},
{
path: "/settings",
component: DashboardLayout,
@ -114,9 +146,20 @@ const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes,
scrollBehavior: () => {
return { x: 0, y: 0 };
} //scroll to top on page changes
scrollBehavior: (to, from, savedPosition) => {
// Exists when Browser's back/forward pressed
if (savedPosition) {
return savedPosition
// For anchors
} else if (to.hash) {
return { selector: to.hash, behavior: 'smooth' }
// By changing queries we are still in the same component, so "from.path" === "to.path" (new query changes just "to.fullPath", but not "to.path").
} else if (from.path === to.path) {
return {}
}
// Scroll to top
return { x: 0, y: 0 }
}
});
//Fake for now

4
src/store/index.js

@ -6,6 +6,7 @@ import user from "./modules/user";
import system from "./modules/system";
import bitcoin from "./modules/bitcoin";
import lightning from "./modules/lightning";
import apps from "./modules/apps";
Vue.use(Vuex);
@ -51,6 +52,7 @@ export default new Vuex.Store({
user,
system,
bitcoin,
lightning
lightning,
apps
}
});

126
src/store/modules/apps.js

@ -0,0 +1,126 @@
import API from "@/helpers/api";
// import Vue from "vue"
// Initial state
const state = () => ({
installed: [],
store: [],
installing: [],
uninstalling: []
});
// Functions to update the state directly
const mutations = {
setInstalledApps(state, apps) {
const alphabeticallySortedApps = apps.sort((a, b) => a.name.localeCompare(b.name));
state.installed = alphabeticallySortedApps;
},
setAppStore(state, appStore) {
state.store = appStore;
},
addInstallingApp(state, appId) {
if (!state.installing.includes(appId)) {
state.installing.push(appId);
}
},
removeInstallingApp(state, appId) {
const index = state.installing.findIndex((id) => id === appId);
if (index !== -1) {
state.installing.splice(index, 1);
}
},
addUninstallingApp(state, appId) {
if (!state.uninstalling.includes(appId)) {
state.uninstalling.push(appId);
}
},
removeUninstallingApp(state, appId) {
const index = state.uninstalling.findIndex((id) => id === appId);
if (index !== -1) {
state.uninstalling.splice(index, 1);
}
}
};
// Functions to get data from the API
const actions = {
async getInstalledApps({ commit }) {
const installedApps = await API.get(`${process.env.VUE_APP_MANAGER_API_URL}/v1/apps?installed=1`);
if (installedApps) {
commit("setInstalledApps", installedApps);
}
},
async getAppStore({ commit, dispatch }) {
dispatch("getInstalledApps");
const appStore = await API.get(`${process.env.VUE_APP_MANAGER_API_URL}/v1/apps`);
if (appStore) {
commit("setAppStore", appStore);
}
},
async uninstall({ state, commit, dispatch }, appId) {
commit("addUninstallingApp", appId);
try {
await API.post(
`${process.env.VUE_APP_MANAGER_API_URL}/v1/apps/${appId}/uninstall`
);
} catch (error) {
if (error.response && error.response.data) {
commit("removeUninstallingApp", appId);
return this.$bvToast.toast(error.response.data, {
title: "Error",
autoHideDelay: 3000,
variant: "danger",
solid: true,
toaster: "b-toaster-bottom-right",
});
}
}
const poll = window.setInterval(async () => {
await dispatch("getInstalledApps");
const index = state.installed.findIndex((app) => app.id === appId);
if (index === -1) {
commit("removeUninstallingApp", appId);
window.clearInterval(poll);
}
}, 5000);
},
async install({ state, commit, dispatch }, appId) {
commit("addInstallingApp", appId);
try {
await API.post(
`${process.env.VUE_APP_MANAGER_API_URL}/v1/apps/${appId}/install`
);
} catch (error) {
if (error.response && error.response.data) {
commit("removeInstallingApp", appId);
return this.$bvToast.toast(error.response.data, {
title: "Error",
autoHideDelay: 3000,
variant: "danger",
solid: true,
toaster: "b-toaster-bottom-right",
});
}
}
const poll = window.setInterval(async () => {
await dispatch("getInstalledApps");
const index = state.installed.findIndex((app) => app.id === appId);
if (index !== -1) {
commit("removeInstallingApp", appId);
window.clearInterval(poll);
}
}, 5000);
}
};
const getters = {};
export default {
namespaced: true,
state,
actions,
getters,
mutations
};

9
src/store/modules/user.js

@ -6,7 +6,8 @@ const state = () => ({
name: "",
jwt: window.localStorage.getItem("jwt") || "",
registered: true,
seed: []
seed: [],
installedApps: []
});
// Functions to update the state directly
@ -21,6 +22,9 @@ const mutations = {
setName(state, name) {
state.name = name;
},
setInstalledApps(state, installedApps) {
state.installedApps = installedApps;
},
setSeed(state, seed) {
state.seed = seed;
}
@ -65,10 +69,11 @@ const actions = {
},
async getInfo({ commit }) {
const { name } = await API.get(
const { name, installedApps } = await API.get(
`${process.env.VUE_APP_MANAGER_API_URL}/v1/account/info`
);
commit("setName", name);
commit("setInstalledApps", installedApps);
},
async getSeed({ commit, state, dispatch }, plainTextPassword) {

127
src/views/AppStore.vue

@ -0,0 +1,127 @@
<template>
<div class="p-sm-2">
<div class="mt-3 mb-4">
<div class="">
<h1>app store</h1>
<p class="text-muted">
Add super powers to your Umbrel with amazing self-hosted applications
</p>
</div>
</div>
<div class="card-columns">
<card-widget
v-for="categorizedApps in categorizedAppStore"
:key="categorizedApps[0].category"
class="pb-2 card-app-list"
:header="categorizedApps[0].category"
>
<router-link
:to="`/app-store/${app.id}`"
v-for="app in categorizedApps"
:key="app.id"
class="app-list-app d-flex justify-content-between align-items-center px-3 px-lg-4 py-3"
>
<div class="d-flex">
<div class="d-block">
<img
class="app-icon mr-2 mr-lg-3"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/icon.svg`"
/>
</div>
<div class="d-flex justify-content-center flex-column">
<h3 class="app-name font-weight-bolder text-dark mb-1">
{{ app.name }}
</h3>
<p class="text-muted mb-0">
{{ app.tagline }}
</p>
</div>
</div>
<svg
width="14"
height="25"
viewBox="0 0 14 25"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="ml-4 icon-arrow"
>
<path
d="M0.512563 3.0484C-0.170855 2.35104 -0.170855 1.22039 0.512563 0.523023C1.19598 -0.174341 2.30402 -0.174341 2.98744 0.523023L13.4874 11.2373C14.1499 11.9133 14.1731 13.0019 13.54 13.7066L3.91502 24.4209C3.26193 25.1479 2.15494 25.197 1.44248 24.5306C0.730023 23.8642 0.681893 22.7346 1.33498 22.0076L9.82776 12.5537L0.512563 3.0484Z"
fill="#C3C6D1"
/>
</svg>
<!-- Preload gallery images -->
<div class="d-none">
<img
v-for="image in app.gallery"
class="d-none"
:key="app.id + image"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/${image}`"
/>
</div>
</router-link>
</card-widget>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import CardWidget from "@/components/CardWidget";
export default {
data() {
return {};
},
computed: {
...mapState({
store: (state) => state.apps.store,
}),
categorizedAppStore: function () {
let group = this.store.reduce((r, a) => {
r[a.category] = [...(r[a.category] || []), a];
return r;
}, {});
return group;
},
},
created() {
this.$store.dispatch("apps/getAppStore");
},
components: {
CardWidget,
},
};
</script>
<style lang="scss" scoped>
.app-list-app {
border-bottom: solid 1px #edf0f7;
&:first-child {
padding-top: 0 !important;
}
&:last-child {
border-bottom: none;
}
.icon-arrow {
will-change: tranform;
transform: translate3d(0, 0, 0);
transition: 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
}
&:hover {
.icon-arrow {
transform: translate3d(6px, 0, 0);
}
}
}
.card-app-list {
// https://stackoverflow.com/a/34115300
box-shadow: 0 1px 10px rgba(209, 213, 223, 0.5),
0 1px 2px rgba(209, 213, 223, 0) !important;
margin-top: 16px !important;
margin-bottom: 16px !important;
display: inline-block;
width: 100%;
}
</style>

268
src/views/AppStoreApp.vue

@ -0,0 +1,268 @@
<template>
<div class="p-sm-2">
<div class="mt-3 mb-1 mb-sm-3 pb-lg-2">
<router-link
to="/app-store"
class="card-link text-muted d-flex align-items-center mb-4"
><svg
width="7"
height="13"
viewBox="0 0 7 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="mr-1"
>
<path
d="M6.74372 11.4153C7.08543 11.7779 7.08543 12.3659 6.74372 12.7285C6.40201 13.0911 5.84799 13.0911 5.50628 12.7285L0.256283 7.15709C-0.0749738 6.80555 -0.0865638 6.23951 0.229991 5.87303L5.04249 0.301606C5.36903 -0.0764332 5.92253 -0.101971 6.27876 0.244565C6.63499 0.591101 6.65905 1.17848 6.33251 1.55652L2.08612 6.47256L6.74372 11.4153Z"
fill="#C3C6D1"
/>
</svg>
Back</router-link
>
<div
class="d-flex flex-column flex-sm-row justify-content-between align-items-center"
>
<div class="d-flex w-xs-100 justify-content-start pr-2">
<div class="d-block">
<img
class="app-icon app-icon-lg mr-2 mr-sm-3 align-self-top"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/icon.svg`"
/>
</div>
<div>
<h3 class="d-block font-weight-bold mb-1">
{{ app.name }}
</h3>
<p class="text-muted">{{ app.tagline }}</p>
<p>
<small>{{ app.developer }}</small>
</p>
</div>
</div>
<div
class="w-xs-100 d-flex flex-column align-items-sm-center"
v-if="isInstalled && !isUninstalling"
>
<b-button
variant="primary"
size="lg"
class="px-4"
:href="url"
target="_blank"
>Open</b-button
>
<div class="mt-2 text-center" v-if="app.defaultPassword">
<small class="">The default password of this app is</small>
<input-copy
size="sm"
:value="app.defaultPassword"
class="mt-1"
></input-copy>
</div>
</div>
<div class="d-flex flex-column align-items-sm-center w-xs-100" v-else>
<b-button
v-if="isInstalling"
variant="success"
size="lg"
class="px-4 fade-in-out"
disabled
>Installing...</b-button
>
<b-button
v-else-if="isUninstalling"
variant="warning"
size="lg"
class="px-4 fade-in-out"
disabled
>Uninstalling...</b-button
>
<b-button
v-else
variant="success"
size="lg"
class="px-4"
@click="installApp"
>Install</b-button
>
<small
:style="{ opacity: isInstalling || isUninstalling ? 1 : 0 }"
class="mt-1 d-block text-muted text-center"
>This may take a few minutes</small
>
<div
class="mt-2 text-center"
v-if="isInstalling && app.defaultPassword"
>
<small class="">The default password of this app is</small>
<input-copy
size="sm"
:value="app.defaultPassword"
class="mt-1"
></input-copy>
</div>
</div>
</div>
</div>
<div class="app-gallery pt-3 pb-4 mb-2 mb-sm-3">
<img
v-for="image in app.gallery"
class="app-gallery-screen mr-3"
:key="image"
:src="`https://getumbrel.github.io/umbrel-apps-gallery/${app.id}/${image}`"
/>
<div class="d-block" style="padding: 1px"></div>
</div>
<b-row>
<b-col col cols="12" lg="6" xl="8">
<card-widget header="About this app">
<div class="px-3 px-lg-4 pb-4">
<p class="text-newlines">{{ app.description }}</p>
</div>
</card-widget>
</b-col>
<b-col col cols="12" lg="6" xl="4">
<card-widget header="Information">
<div class="px-3 px-lg-4 pb-4">
<div class="d-flex justify-content-between mb-3">
<span>Version</span>
<span>{{ app.version }}</span>
</div>
<div class="d-flex justify-content-between mb-3">
<span>Source Code</span>
<a :href="app.repo" target="_blank">Open Source</a>
</div>
<div class="d-flex justify-content-between mb-3">
<span>Developer</span>
<a :href="app.website" target="_blank">{{ app.developer }}</a>
</div>
<div class="d-flex justify-content-between mb-3">
<span>Compatibility</span>
<span>Compatible</span>
</div>
<div class="mb-4">
<span class="d-block mb-3">Requires</span>
<div
class="d-flex align-items-center justify-content-between mb-3"
v-for="dependency in app.dependencies"
:key="dependency"
>
<div class="d-flex align-items-center">
<img
:src="
require(`@/assets/app-store/dependencies/${dependency}.svg`)
"
style="width: 50px; height: 50px"
class="mr-2"
/>
<span class="text-muted my-0">{{
formatDependency(dependency)
}}</span>
</div>
<div>
<svg
width="30"
height="30"
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.3035 10.7643C19.5718 10.4486 20.0451 10.4103 20.3607 10.6785C20.6763 10.9468 20.7147 11.4201 20.4464 11.7357L14.0714 19.2357C13.799 19.5563 13.3162 19.5901 13.0017 19.3105L9.62671 16.3105C9.31712 16.0354 9.28924 15.5613 9.56443 15.2517C9.83962 14.9421 10.3137 14.9142 10.6233 15.1894L13.4251 17.68L19.3035 10.7643Z"
fill="#00CD98"
/>
</svg>
<small class="text-success">Installed</small>
</div>
</div>
</div>
<b-link
:href="app.support"
target="_blank"
size="sm"
class="mb-2 py-1"
block
>Get support</b-link
>
</div>
</card-widget>
</b-col>
</b-row>
</div>
</template>
<script>
import { mapState } from "vuex";
import CardWidget from "@/components/CardWidget";
import InputCopy from "@/components/Utility/InputCopy";
export default {
data() {
return {};
},
computed: {
...mapState({
installedApps: (state) => state.apps.installed,
appStore: (state) => state.apps.store,
installing: (state) => state.apps.installing,
uninstalling: (state) => state.apps.uninstalling,
}),
app: function () {
return this.appStore.find((app) => app.id === this.$route.params.id);
},
isInstalled: function () {
const installedAppIndex = this.installedApps.findIndex(
(app) => app.id === this.app.id
);
return installedAppIndex !== -1;
},
isInstalling: function () {
const index = this.installing.findIndex((appId) => appId === this.app.id);
return index !== -1;
},
isUninstalling: function () {
const index = this.uninstalling.findIndex(
(appId) => appId === this.app.id
);
return index !== -1;
},
url: function () {
if (window.location.origin.indexOf(".onion") > 0) {
const installedApp = this.installedApps.find(
(app) => app.id === this.app.id
);
return `http://${installedApp.hiddenService}${this.app.path}`;
} else {
return `http://${window.location.hostname}:${this.app.port}${this.app.path}`;
}
},
},
methods: {
formatDependency(dependency) {
let name;
if (dependency === "bitcoind") {
name = "Bitcoin Core";
} else if (dependency === "lnd") {
name = "LND";
} else if (dependency === "electrum") {
name = "Electrum Server";
}
return name;
},
installApp() {
this.$store.dispatch("apps/install", this.app.id);
},
},
async created() {
await this.$store.dispatch("apps/getAppStore");
},
components: {
CardWidget,
InputCopy,
},
};
</script>
<style lang="scss" scoped>
</style>

80
src/views/Apps.vue

@ -0,0 +1,80 @@
<template>
<div class="p-sm-2">
<div v-if="installedApps.length">
<div class="my-3 pb-3">
<div class="d-flex justify-content-between align-items-center">
<h1>apps</h1>
<div>
<b-button variant="outline-primary" size="sm" @click="toggleEdit">{{
isEditing ? "Done" : "Edit"
}}</b-button>
</div>
</div>
</div>
<div class="d-flex flex-wrap justify-content-start apps-container">
<installed-app
v-for="app in installedApps"
:key="app.id"
:id="app.id"
:name="app.name"
:port="app.port"
:path="app.path"
:hiddenService="app.hiddenService"
:showUninstallButton="isEditing"
:isUninstalling="uninstallingApps.includes(app.id)"
>
</installed-app>
</div>
</div>
<div v-else>
<div class="my-3 pb-3">
<h1>apps</h1>
<div
class="d-flex flex-column justify-content-center align-items-center py-5 mb-lg-5"
>
<p class="text-muted mb-2">You don't have any apps installed yet</p>
<b-button variant="success" class="px-4" :to="'app-store'"
>Go to App Store</b-button
>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapState } from "vuex";
import InstalledApp from "@/components/InstalledApp";
export default {
data() {
return {
isEditing: false,
};
},
computed: {
...mapState({
installedApps: (state) => state.apps.installed,
uninstallingApps: (state) => state.apps.uninstalling,
}),
},
created() {
this.$store.dispatch("apps/getInstalledApps");
},
methods: {
toggleEdit() {
this.isEditing = !this.isEditing;
},
},
components: {
InstalledApp,
},
};
</script>
<style lang="scss" scoped>
.apps-container {
column-gap: 2rem;
}
</style>
Loading…
Cancel
Save