mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat(plus): activate license from web interface
This commit is contained in:
parent
c620aaefe5
commit
40af08f2f6
23 changed files with 297 additions and 16 deletions
|
@ -40,7 +40,7 @@ class CheckLicenseStatusCommand extends Command
|
|||
|
||||
case LicenseStatus::STATUS_NO_LICENSE:
|
||||
$this->components->info(
|
||||
'No license found. You can purchase one at ' . config('lemonsqueezy.store_url')
|
||||
'No license found. You can purchase one at https://store.plus.koel.dev/checkout/buy/' . config('lemonsqueezy.plus_product_id') // @phpcs-ignore
|
||||
);
|
||||
break;
|
||||
|
||||
|
|
20
app/Http/Controllers/API/ActivateLicenseController.php
Normal file
20
app/Http/Controllers/API/ActivateLicenseController.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\API;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\API\ActivateLicenseRequest;
|
||||
use App\Models\License;
|
||||
use App\Services\License\LicenseServiceInterface;
|
||||
|
||||
class ActivateLicenseController extends Controller
|
||||
{
|
||||
public function __invoke(ActivateLicenseRequest $request, LicenseServiceInterface $licenseService)
|
||||
{
|
||||
$this->authorize('activate', License::class);
|
||||
|
||||
$licenseService->activate($request->key);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
}
|
|
@ -59,7 +59,7 @@ class FetchInitialDataController extends Controller
|
|||
'short_key' => $licenseStatus->license?->short_key,
|
||||
'customer_name' => $licenseStatus->license?->meta->customerName,
|
||||
'customer_email' => $licenseStatus->license?->meta->customerEmail,
|
||||
'store_url' => config('lemonsqueezy.store_url'),
|
||||
'product_id' => config('lemonsqueezy.product_id'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
|
17
app/Http/Requests/API/ActivateLicenseRequest.php
Normal file
17
app/Http/Requests/API/ActivateLicenseRequest.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests\API;
|
||||
|
||||
/**
|
||||
* @property-read string $key
|
||||
*/
|
||||
class ActivateLicenseRequest extends Request
|
||||
{
|
||||
/** @return array<mixed> */
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'key' => 'required|string',
|
||||
];
|
||||
}
|
||||
}
|
14
app/Policies/LicensePolicy.php
Normal file
14
app/Policies/LicensePolicy.php
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Facades\License;
|
||||
use App\Models\User;
|
||||
|
||||
class LicensePolicy
|
||||
{
|
||||
public function activate(User $user): bool
|
||||
{
|
||||
return $user->is_admin && License::isCommunity();
|
||||
}
|
||||
}
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
return [
|
||||
'store_id' => 62685,
|
||||
'store_url' => 'https://store.plus.koel.dev',
|
||||
'product_id' => env('PLUS_PRODUCT_ID', '58e8adbc-b1aa-43f9-b768-a52d1d8e6a40'),
|
||||
];
|
||||
|
|
22
resources/assets/img/koel-plus.svg
Normal file
22
resources/assets/img/koel-plus.svg
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="gradient-2-0" gradientTransform="matrix(1.630484, 0.681896, -0.385832, 0.922568, 1.187751, 0.103153)" x1="-0.856" y1="0.304" x2="0.144" y2="0.304" xlink:href="#gradient-2"/>
|
||||
<linearGradient id="gradient-2">
|
||||
<stop offset="0" style="stop-color: rgb(198, 43, 232);"/>
|
||||
<stop offset="0.22" style="stop-color: rgb(103, 28, 228);"/>
|
||||
<stop offset="1" style="stop-color: rgb(198, 43, 232);"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect style="fill: url('#gradient-2-0'); fill-rule: nonzero; stroke-width: 0px; stroke-miterlimit: 4.98; stroke: rgb(0, 0, 0); paint-order: fill; transform-origin: 250px 250px;" width="500" height="500"/>
|
||||
<g transform="matrix(0.8879029750823975, 0, 0, 0.8879029750823975, 52.0737762451173, 56.69451904296875)" style="opacity: 1;">
|
||||
<path d="M 328.303 188.961 C 328.303 188.961 318.17 203.266 347.76 217.348 C 347.76 217.348 302.636 212.569 290.67 201.233 C 290.67 201.233 212.854 211.423 243.947 286.124 C 243.947 286.124 218.36 267.71 210.518 243.685 C 210.518 243.685 152.216 304.894 191.314 405.117 C 191.314 405.117 148.62 359.498 142.608 314.694 C 142.608 314.694 138.995 321.72 140.515 332.469 C 140.515 332.469 123.333 282.915 127.745 256.742 C 127.745 256.742 117.266 271.261 116.622 279.47 C 116.622 279.47 118.088 261.069 125.336 249.74 C 125.336 249.74 123.225 233.321 130.436 216.127 C 137.63 198.974 135.336 180.594 133.949 177.325 C 133.949 177.325 131.722 184.773 128.532 187.975 C 128.532 187.975 138.732 147.647 134.398 128.305 C 134.398 128.305 125.485 136.922 126.786 158.527 C 126.786 158.527 121.629 139.445 126.723 119.042 C 126.723 119.042 122.162 119.49 121.789 124.142 C 121.789 124.142 116.949 111.085 123.76 96.051 C 123.76 96.051 84.833 69.942 81.028 66.853 C 81.028 66.853 75.417 64.012 68.493 64.63 C 90.415 55.506 119.45 71.392 137.4 83.323 C 147.164 90.108 151.962 94.401 162.608 97.243 C 173.395 100.122 182.999 109.637 182.999 109.637 C 172.215 93.891 168.799 102.591 139.349 81.622 C 97.464 51.998 74.541 59.985 66.511 64.903 C 65.463 65.098 64.397 65.373 63.323 65.762 C 63.323 65.762 79.902 30.643 149.477 53.127 C 149.477 53.127 160.244 42.164 166.903 41.09 C 166.903 41.09 157.622 47.703 155.936 53.17 C 155.936 53.17 169.148 43.598 179.921 43.705 C 179.921 43.705 167.56 49.992 161.35 55.813 C 161.35 55.813 169.556 51.438 181.381 51.582 C 181.381 51.582 174.035 55.135 171.511 56.355 C 170.985 56.609 170.668 56.762 170.668 56.762 C 170.668 56.762 178.105 55.185 182.139 55.101 C 182.139 55.101 202.253 45.441 220.442 47.332 C 220.442 47.332 214.117 47.379 205.347 54.386 C 205.347 54.386 232.76 52.951 251.126 62.02 C 251.126 62.02 235.577 58.969 225.896 59.036 C 225.896 59.036 273.212 60.924 312.72 112.498 C 354.285 166.757 356.018 171.163 389.963 196.683 C 389.963 196.683 348.906 194.896 328.303 188.961 Z" class="cls-1" style="fill: rgb(255, 255, 255); fill-rule: evenodd;"/>
|
||||
<path d="M 299.325 169.037 C 299.325 169.037 292.029 181.568 313.73 191.067 C 313.73 191.067 280.601 190.376 271.363 181.746 C 271.363 181.746 210.649 197.114 237.47 256.311 C 237.47 256.311 216.699 243.673 209.508 224.113 C 209.508 224.113 164.263 283.396 200.188 358.838 C 200.188 358.838 163.476 328.412 156.127 291.899 C 156.127 291.899 153.416 298.307 155.279 307.151 C 155.279 307.151 137.679 267.002 140.027 243.601 C 140.027 243.601 131.625 257.518 131.554 264.785 C 131.554 264.785 131.733 248.454 137.485 237.67 C 137.485 237.67 134.624 223.264 140.027 207.166 C 145.431 191.069 142.313 174.483 140.875 171.579 C 140.875 171.579 139.316 178.576 136.638 181.746 C 136.638 181.746 143.429 143.784 138.333 125.823 C 138.333 125.823 130.702 134.405 133.249 154.632 C 133.249 154.632 127.312 136.991 130.707 117.35 C 130.707 117.35 126.513 117.968 126.47 122.433 C 126.47 122.433 121.098 110.097 126.47 95.319 C 126.47 95.319 87.26 70.415 83.256 67.357 C 83.256 67.357 77.435 64.55 70.552 65.282 C 91.536 55.723 120.764 71.278 138.333 82.609 C 147.705 88.926 152.314 92.915 162.058 95.319 C 171.802 97.723 180.699 106.334 180.699 106.334 C 170.441 91.87 167.857 100.193 140.027 80.914 C 98.378 52.251 76.414 60.484 68.707 65.56 C 67.634 65.774 66.548 66.082 65.463 66.51 C 65.463 66.51 79.865 30.545 147.653 52.953 C 147.653 52.953 156.901 42.146 162.905 41.09 C 162.905 41.09 154.816 47.589 153.585 52.953 C 153.585 52.953 165.075 43.543 174.768 43.632 C 174.768 43.632 163.981 49.785 158.669 55.495 C 158.669 55.495 165.859 51.182 176.462 51.258 C 176.462 51.258 170.079 54.74 167.877 55.94 C 167.419 56.191 167.142 56.342 167.142 56.342 C 167.142 56.342 173.722 54.758 177.31 54.647 C 177.31 54.647 194.569 45.267 210.356 47.021 C 210.356 47.021 204.931 47.086 197.646 53.8 C 197.646 53.8 221.018 52.269 236.623 60.579 C 236.623 60.579 223.597 57.887 215.44 58.037 C 215.44 58.037 254.712 59.521 286.615 104.64 C 318.519 149.758 319.557 152.847 343.386 171.579 C 343.386 171.579 314.382 172.661 299.325 169.037 Z" class="cls-2" style="fill-rule: evenodd;"/>
|
||||
<path d="M 139.18 66.51 C 139.18 66.51 145.52 60.69 147.653 69.899 C 147.653 69.899 144.627 72.868 142.569 71.594 C 140.512 70.319 139.18 66.51 139.18 66.51 Z" class="cls-1" style="fill: rgb(255, 255, 255); fill-rule: evenodd;"/>
|
||||
<g style="" transform="matrix(0.847328, 0, 0, 0.847328, 39.194847, 35.159096)">
|
||||
<path d="M184.000,67.000 C184.000,67.000 189.596,53.143 207.000,58.000 C221.245,62.388 222.000,74.000 222.000,74.000 C222.000,74.000 215.773,89.560 204.000,88.000 C192.227,86.440 184.492,76.630 184.000,67.000 Z" class="cls-3" style="fill: rgb(250, 0, 0); fill-rule: evenodd;"/>
|
||||
<path d="M200.055,61.063 C207.079,60.796 213.823,64.926 214.031,70.403 C214.243,75.962 207.620,79.457 200.388,78.313 C194.177,77.332 189.733,73.180 189.712,68.967 C189.691,64.801 194.001,61.293 200.055,61.063 Z" class="cls-2" style="fill-rule: evenodd;"/>
|
||||
<ellipse cx="196.5" cy="65" rx="4.5" ry="4" class="cls-4" style="fill: rgb(255, 255, 255);"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 6 KiB |
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<form class="license-form" @submit.prevent="validateLicenseKey">
|
||||
<input
|
||||
type="text"
|
||||
name="license"
|
||||
v-model="licenseKey"
|
||||
placeholder="Enter your license key"
|
||||
required
|
||||
v-koel-focus
|
||||
:disabled="loading"
|
||||
>
|
||||
<Btn blue type="submit" :disabled="loading">Activate</Btn>
|
||||
</form>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { plusService } from '@/services'
|
||||
import { forceReloadWindow, logger } from '@/utils'
|
||||
import { useDialogBox } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
|
||||
const { showSuccessDialog, showErrorDialog } = useDialogBox()
|
||||
const licenseKey = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const validateLicenseKey = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await plusService.activateLicense(licenseKey.value)
|
||||
await showSuccessDialog('Thanks for purchasing Koel Plus! Koel will now refresh to activate the changes.')
|
||||
forceReloadWindow()
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
await showErrorDialog('Failed to activate Koel Plus. Please try again.')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
form {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
&:has(:focus) {
|
||||
outline: 4px solid rgba(255, 255, 255, 0);
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,15 +1,17 @@
|
|||
<template>
|
||||
<a :href="storeUrl" target="_blank" class="upgrade-to-plus-btn">
|
||||
<a href class="upgrade-to-plus-btn" @click.prevent="openModal">
|
||||
<Icon :icon="faPlus" fixed-width/>
|
||||
Upgrade to Plus
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useKoelPlus } from '@/composables'
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||
import { eventBus } from '@/utils'
|
||||
|
||||
const { storeUrl } = useKoelPlus()
|
||||
const openModal = () => {
|
||||
eventBus.emit('MODAL_SHOW_KOEL_PLUS')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
116
resources/assets/js/components/koel-plus/KoelPlusModal.vue
Normal file
116
resources/assets/js/components/koel-plus/KoelPlusModal.vue
Normal file
|
@ -0,0 +1,116 @@
|
|||
<template>
|
||||
<div class="plus text-secondary" data-testid="koel-plus" tabindex="0">
|
||||
<img class="plus-icon" alt="Koel Plus" src="@/../img/koel-plus.svg" width="96">
|
||||
|
||||
<main>
|
||||
<div class="intro">
|
||||
Koel Plus adds premium features on top of the default installation.<br>
|
||||
Pay <em>once</em> and enjoy all additional features forever — including those to be built into the app
|
||||
in the future!
|
||||
</div>
|
||||
|
||||
<div class="buttons" v-show="!showingActivateLicenseForm">
|
||||
<Btn big red @click.prevent="openPurchaseOverlay">Purchase Koel Plus</Btn>
|
||||
<Btn big green @click.prevent="showActivateLicenseForm">I have a license key</Btn>
|
||||
</div>
|
||||
|
||||
<div class="activate-form" v-if="showingActivateLicenseForm">
|
||||
<ActivateLicenseForm v-if="showingActivateLicenseForm" />
|
||||
<Btn transparent class="cancel" @click.prevent="hideActivateLicenseForm">Cancel</Btn>
|
||||
</div>
|
||||
|
||||
<div class="more-info">
|
||||
Visit <a href="https://koel.dev#plus" target="_blank">koel.dev</a> for more information.
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<Btn data-testid="close-modal-btn" red rounded @click.prevent="close">Close</Btn>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useKoelPlus } from '@/composables'
|
||||
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import ActivateLicenseForm from '@/components/koel-plus/ActivateLicenseForm.vue'
|
||||
|
||||
const { checkoutUrl } = useKoelPlus()
|
||||
|
||||
const emit = defineEmits<{ (e: 'close'): void }>()
|
||||
const close = () => emit('close')
|
||||
|
||||
const showingActivateLicenseForm = ref(false)
|
||||
|
||||
const openPurchaseOverlay = () => {
|
||||
close()
|
||||
LemonSqueezy.Url.Open(checkoutUrl.value) // @ts-ignore
|
||||
}
|
||||
|
||||
const showActivateLicenseForm = () => (showingActivateLicenseForm.value = true)
|
||||
const hideActivateLicenseForm = () => (showingActivateLicenseForm.value = false)
|
||||
|
||||
onMounted(() => window.createLemonSqueezy()) // @ts-ignore
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.plus {
|
||||
max-width: 480px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
main {
|
||||
padding: .7rem 1.7rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.plus-icon {
|
||||
margin-top: calc(-48px);
|
||||
border-radius: 99rem;
|
||||
border: 6px solid #fff;
|
||||
}
|
||||
|
||||
.intro {
|
||||
text-align: center;
|
||||
padding: .5rem 1.5rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 1rem
|
||||
}
|
||||
|
||||
.more-info {
|
||||
font-size: .9rem;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.activate-form {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
|
||||
form {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button.cancel {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: .5rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, .2);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -21,6 +21,7 @@ const modalNameToComponentMap = {
|
|||
'create-playlist-folder-form': defineAsyncComponent(() => import('@/components/playlist/CreatePlaylistFolderForm.vue')),
|
||||
'edit-playlist-folder-form': defineAsyncComponent(() => import('@/components/playlist/EditPlaylistFolderForm.vue')),
|
||||
'about-koel': defineAsyncComponent(() => import('@/components/meta/AboutKoelModal.vue')),
|
||||
'koel-plus': defineAsyncComponent(() => import('@/components/koel-plus/KoelPlusModal.vue')),
|
||||
'equalizer': defineAsyncComponent(() => import('@/components/ui/Equalizer.vue'))
|
||||
}
|
||||
|
||||
|
@ -40,6 +41,7 @@ const close = () => {
|
|||
}
|
||||
|
||||
eventBus.on('MODAL_SHOW_ABOUT_KOEL', () => (activeModalName.value = 'about-koel'))
|
||||
.on('MODAL_SHOW_KOEL_PLUS', () => (activeModalName.value = 'koel-plus'))
|
||||
.on('MODAL_SHOW_ADD_USER_FORM', () => (activeModalName.value = 'add-user-form'))
|
||||
.on('MODAL_SHOW_INVITE_USER_FORM', () => (activeModalName.value = 'invite-user-form'))
|
||||
.on('MODAL_SHOW_CREATE_PLAYLIST_FORM', (folder, songs) => {
|
||||
|
@ -85,6 +87,7 @@ dialog {
|
|||
border-radius: 4px;
|
||||
min-width: 460px;
|
||||
max-width: calc(100vw - 24px);
|
||||
overflow: visible;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
min-width: calc(100vw - 24px);
|
||||
|
|
|
@ -60,7 +60,7 @@ import QueueSidebarItem from './QueueSidebarItem.vue'
|
|||
import YouTubeSidebarItem from './YouTubeSidebarItem.vue'
|
||||
import PlaylistList from './PlaylistSidebarList.vue'
|
||||
import SearchForm from '@/components/ui/SearchForm.vue'
|
||||
import BtnUpgradeToPlus from '@/components/meta/BtnUpgradeToPlus.vue'
|
||||
import BtnUpgradeToPlus from '@/components/koel-plus/BtnUpgradeToPlus.vue'
|
||||
|
||||
const { onRouteChanged } = useRouter()
|
||||
const { useYouTube } = useThirdPartyServices()
|
||||
|
|
|
@ -13,12 +13,13 @@
|
|||
<p v-if="isPlus" class="plus-badge">
|
||||
Licensed to {{ license.customerName }} <{{ license.customerEmail }}>
|
||||
<br>
|
||||
License key <span class="key text-green">{{ license.shortKey }}</span>
|
||||
License key: <span class="key">{{ license.shortKey }}</span>
|
||||
</p>
|
||||
|
||||
<template v-else>
|
||||
<p class="upgrade" v-if="isAdmin">
|
||||
<BtnUpgradeToPlus/>
|
||||
<!-- close the modal first to prevent it from overlapping Lemonsqueezy's overlay -->
|
||||
<BtnUpgradeToPlus @click="close"/>
|
||||
</p>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -72,7 +73,7 @@ import { http } from '@/services'
|
|||
|
||||
import SponsorList from '@/components/meta/SponsorList.vue'
|
||||
import Btn from '@/components/ui/Btn.vue'
|
||||
import BtnUpgradeToPlus from '@/components/meta/BtnUpgradeToPlus.vue'
|
||||
import BtnUpgradeToPlus from '@/components/koel-plus/BtnUpgradeToPlus.vue'
|
||||
|
||||
type DemoCredits = {
|
||||
name: string
|
||||
|
@ -168,6 +169,9 @@ onMounted(async () => {
|
|||
.plus-badge {
|
||||
.key {
|
||||
font-family: monospace;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-image: linear-gradient(97.78deg, #c62be8 17.5%, #671ce4 113.39%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@ exports[`renders 1`] = `
|
|||
<div data-v-6b5b01a9="" class="about text-secondary" data-testid="about-koel" tabindex="0">
|
||||
<main data-v-6b5b01a9="">
|
||||
<div data-v-6b5b01a9="" class="logo"><img data-v-6b5b01a9="" alt="Koel's logo" src="undefined/resources/assets/img/logo.svg" width="128"></div>
|
||||
<p data-v-6b5b01a9="" class="current-version">Koel v0.0.0</p>
|
||||
<div data-v-6b5b01a9="" class="current-version"> Koel v0.0.0 <span data-v-6b5b01a9="">Community</span> Edition
|
||||
<!--v-if-->
|
||||
</div>
|
||||
<!--v-if-->
|
||||
<p data-v-6b5b01a9="" class="author"> Made with ❤️ by <a data-v-6b5b01a9="" href="https://github.com/phanan" rel="noopener" target="_blank">Phan An</a> and quite a few <a data-v-6b5b01a9="" href="https://github.com/koel/core/graphs/contributors" rel="noopener" target="_blank">awesome</a> <a data-v-6b5b01a9="" href="https://github.com/koel/koel/graphs/contributors" rel="noopener" target="_blank">contributors</a>. </p>
|
||||
<!--v-if--><br data-v-6b5b01a9="" data-testid="sponsor-list">
|
||||
|
|
|
@ -30,6 +30,10 @@ button {
|
|||
box-shadow: inset 0 0 0 10rem rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
&[big] {
|
||||
padding: .85rem 1.4rem;
|
||||
}
|
||||
|
||||
&[small] {
|
||||
font-size: .9rem;
|
||||
padding: .4rem .7rem;
|
||||
|
|
|
@ -9,6 +9,6 @@ export const useKoelPlus = () => {
|
|||
customerName: commonStore.state.koel_plus.customer_name,
|
||||
customerEmail: commonStore.state.koel_plus.customer_email
|
||||
},
|
||||
storeUrl: computed(() => commonStore.state.koel_plus.store_url)
|
||||
checkoutUrl: computed(() => `https://store.plus.koel.dev/checkout/buy/${commonStore.state.koel_plus.product_id}?embed=1&media=0`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface Events {
|
|||
MODAL_SHOW_CREATE_PLAYLIST_FOLDER_FORM: () => void
|
||||
MODAL_SHOW_EDIT_PLAYLIST_FOLDER_FORM: (playlistFolder: PlaylistFolder) => void
|
||||
MODAL_SHOW_ABOUT_KOEL: () => void
|
||||
MODAL_SHOW_KOEL_PLUS: () => void
|
||||
MODAL_SHOW_EQUALIZER: () => void
|
||||
|
||||
PLAYLIST_DELETE: (playlist: Playlist) => void
|
||||
|
|
|
@ -12,3 +12,4 @@ export * from './cache'
|
|||
export * from './socketListener'
|
||||
export * from './volumeManager'
|
||||
export * from './invitationService'
|
||||
export * from './plusService'
|
||||
|
|
|
@ -164,7 +164,7 @@ class PlaybackService {
|
|||
position: 0
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
this.player.restart()
|
||||
|
@ -397,7 +397,7 @@ class PlaybackService {
|
|||
position: Math.ceil(media.currentTime)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
logger.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
7
resources/assets/js/services/plusService.ts
Normal file
7
resources/assets/js/services/plusService.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { http } from '@/services'
|
||||
|
||||
export const plusService = {
|
||||
activateLicense: async (key: string) => {
|
||||
return await http.post('licenses/activate', { key })
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ interface CommonStoreState {
|
|||
short_key: string | null
|
||||
customer_name: string | null
|
||||
customer_email: string | null
|
||||
store_url: string
|
||||
product_id: string
|
||||
}
|
||||
media_path_set: boolean
|
||||
playlists: Playlist[]
|
||||
|
@ -41,7 +41,7 @@ export const commonStore = {
|
|||
short_key: null,
|
||||
customer_name: null,
|
||||
customer_email: null,
|
||||
store_url: ''
|
||||
product_id: ''
|
||||
},
|
||||
latest_version: '',
|
||||
media_path_set: false,
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
<link rel="icon" href="{{ static_url('img/icon.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ static_url('img/icon.png') }}">
|
||||
|
||||
@unless(License::isPlus())
|
||||
<script src="https://app.lemonsqueezy.com/js/lemon.js" defer></script>
|
||||
@endunless
|
||||
|
||||
<script>
|
||||
// Work around for "global is not defined" error with local-storage.js
|
||||
window.global = window
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Facades\YouTube;
|
||||
use App\Http\Controllers\API\ActivateLicenseController;
|
||||
use App\Http\Controllers\API\AlbumController;
|
||||
use App\Http\Controllers\API\AlbumSongController;
|
||||
use App\Http\Controllers\API\ArtistAlbumController;
|
||||
|
@ -161,6 +162,8 @@ Route::prefix('api')->middleware('api')->group(static function (): void {
|
|||
|
||||
Route::put('songs/make-public', MakeSongsPublicController::class);
|
||||
Route::put('songs/make-private', MakeSongsPrivateController::class);
|
||||
|
||||
Route::post('licenses/activate', ActivateLicenseController::class);
|
||||
});
|
||||
|
||||
// Object-storage (S3) routes
|
||||
|
|
Loading…
Reference in a new issue