migration: settings screen

This commit is contained in:
Phan An 2022-04-22 00:20:21 +02:00
parent 43be702dfb
commit f0f1d975c9
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
10 changed files with 32 additions and 40 deletions

View file

@ -832,7 +832,7 @@ paths:
security:
- Bearer Token: []
/api/settings:
post:
put:
summary: Save the application settings
tags:
- settings

View file

@ -15,8 +15,7 @@ class SettingController extends Controller
$this->mediaSyncService = $mediaSyncService;
}
// @TODO: This should be a PUT request
public function store(SettingRequest $request)
public function update(SettingRequest $request)
{
Setting::set('media_path', rtrim(trim($request->media_path), '/'));

View file

@ -1,4 +1,4 @@
import Component from '@/components/screens/settings.vue'
import Component from '@/components/screens/SettingsScreen.vue'
import { sharedStore, settingStore } from '@/stores'
import { alerts } from '@/utils'
import { mock } from '@/__tests__/__helpers__'

View file

@ -46,7 +46,7 @@ const UserListScreen = defineAsyncComponent(() => import('@/components/screens/u
const AlbumArtOverlay = defineAsyncComponent(() => import('@/components/ui/album-art-overlay.vue'))
const AlbumScreen = defineAsyncComponent(() => import('@/components/screens/AlbumScreen.vue'))
const ArtistScreen = defineAsyncComponent(() => import('@/components/screens/ArtistScreen.vue'))
const SettingsScreen = defineAsyncComponent(() => import('@/components/screens/settings.vue'))
const SettingsScreen = defineAsyncComponent(() => import('@/components/screens/SettingsScreen.vue'))
const ProfileScreen = defineAsyncComponent(() => import('@/components/screens/profile.vue'))
const YoutubeScreen = defineAsyncComponent(() => import('@/components/screens/youtube.vue'))
const UploadScreen = defineAsyncComponent(() => import('@/components/screens/UploadScreen.vue'))

View file

@ -2,22 +2,22 @@
<section id="settingsWrapper">
<ScreenHeader>Settings</ScreenHeader>
<form @submit.prevent="confirmThenSave" class="main-scroll-wrap">
<form class="main-scroll-wrap" @submit.prevent="confirmThenSave">
<div class="form-row">
<label for="inputSettingsPath">Media Path</label>
<p class="help" id="mediaPathHelp">
<p id="mediaPathHelp" class="help">
The <em>absolute</em> path to the server directory containing your media.
Koel will scan this directory for songs and extract any available information.<br>
Scanning may take a while, especially if you have a lot of songs, so be patient.
</p>
<input
aria-describedby="mediaPathHelp"
id="inputSettingsPath"
type="text"
v-model="state.media_path"
v-model="mediaPath"
aria-describedby="mediaPathHelp"
name="media_path"
type="text"
>
</div>
@ -29,39 +29,40 @@
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, reactive } from 'vue'
import { settingStore, sharedStore } from '@/stores'
import { computed, defineAsyncComponent, ref } from 'vue'
import { settingStore } from '@/stores'
import { alerts, forceReloadWindow, hideOverlay, parseValidationError, showOverlay } from '@/utils'
import router from '@/router'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const Btn = defineAsyncComponent(() => import('@/components/ui/Btn.vue'))
const state = settingStore.state
const sharedState = reactive(sharedStore.state)
const mediaPath = ref(settingStore.state.media_path)
const originalMediaPath = mediaPath.value
const shouldWarn = computed(() => {
// Warn the user if the media path is not empty and about to change.
if (!sharedState.originalMediaPath || !state.media_path) {
if (!originalMediaPath || !mediaPath.value) {
return false
}
return sharedState.originalMediaPath !== state.media_path.trim()
return originalMediaPath !== mediaPath.value.trim()
})
const save = async () => {
showOverlay()
try {
await settingStore.update()
await settingStore.update({ media_path: mediaPath.value })
alerts.success('Settings saved.')
// Make sure we're back to home first.
router.go('home')
forceReloadWindow()
} catch (err: any) {
hideOverlay()
const msg = err.response.status === 422 ? parseValidationError(err.response.data)[0] : 'Unknown error.'
alerts.error(msg)
} finally {
hideOverlay()
}
}

View file

@ -1,11 +1,7 @@
import { reactive } from 'vue'
import { http } from '@/services'
import { alerts } from '@/utils'
import stub from '@/stubs/settings'
export const settingStore = {
stub,
state: reactive<Settings>({
media_path: ''
}),
@ -18,8 +14,8 @@ export const settingStore = {
return this.state
},
async update (): Promise<void> {
await http.post('settings', this.all)
alerts.success('Settings saved.')
async update (settings: Settings) {
await http.put('settings', settings)
Object.assign(this.state, settings)
}
}

View file

@ -282,7 +282,7 @@ interface User {
avatar: string
}
interface Settings extends Object {
interface Settings {
media_path?: string
}
@ -306,11 +306,6 @@ interface SongListMeta {
declare module 'koel/types/ui' {
import { ComponentInternalInstance } from 'vue'
export type BaseContextMenu = ComponentInternalInstance & {
open (y: number, x: number): void
close (): void
}
export type BasePlaylistMenu = ComponentInternalInstance & {
open (top: number, left: number): void
close (): void

View file

@ -7,23 +7,23 @@ const encodeEntities = (str: string) => str.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
export const alerts = {
alert: (msg: string): void => alertify.alert(encodeEntities(msg)),
alert: (msg: string) => alertify.alert(encodeEntities(msg)),
confirm: (msg: string, okFunc: TAnyFunction, cancelFunc?: TAnyFunction): void => {
confirm: (msg: string, okFunc: TAnyFunction, cancelFunc?: TAnyFunction) => {
alertify.confirm(msg, okFunc, cancelFunc)
},
log: (msg: string, type: logType = 'log', cb?: TAnyFunction): void => {
log: (msg: string, type: logType = 'log', cb?: TAnyFunction) => {
alertify.logPosition('top right')
alertify.closeLogOnClick(true)
alertify[type](encodeEntities(msg), cb)
},
success (msg: string, cb?: TAnyFunction): void {
success (msg: string, cb?: TAnyFunction) {
this.log(msg, 'success', cb)
},
error (msg: string, cb?: TAnyFunction): void {
error (msg: string, cb?: TAnyFunction) {
this.log(msg, 'error', cb)
}
}

View file

@ -52,7 +52,7 @@ Route::middleware('auth')->group(static function (): void {
Route::get('data', [DataController::class, 'index']);
Route::post('settings', [SettingController::class, 'store']);
Route::put('settings', [SettingController::class, 'update']);
Route::post('{song}/scrobble', [ScrobbleController::class, 'store']);
Route::put('songs', [SongController::class, 'update']);

View file

@ -5,10 +5,11 @@ namespace Tests\Feature;
use App\Models\Setting;
use App\Models\User;
use App\Services\MediaSyncService;
use Mockery\MockInterface;
class SettingTest extends TestCase
{
private $mediaSyncService;
private MockInterface $mediaSyncService;
public function setUp(): void
{
@ -22,7 +23,7 @@ class SettingTest extends TestCase
$this->mediaSyncService->shouldReceive('sync')->once();
$user = User::factory()->admin()->create();
$this->postAsUser('/api/settings', ['media_path' => __DIR__], $user);
$this->putAsUser('/api/settings', ['media_path' => __DIR__], $user);
self::assertEquals(__DIR__, Setting::get('media_path'));
}