Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions front/public/hotkeys-iframe.js
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,13 @@ hotkeys('ctrl+k, command+k', function (event) {
parent.postMessage('showSearchDialog', '*');
});

hotkeys('ctrl+s, command+s', function (event) {
event.stopImmediatePropagation();
event.preventDefault();

parent.postMessage('solveTask', '*');
});

// Hotkeys for HedgeDoc CodeMirror editor
editor.setOption('extraKeys', {
'Ctrl-K': () => {
Expand All @@ -376,4 +383,12 @@ editor.setOption('extraKeys', {
parent.postMessage('showSearchDialog', '*');
return false;
},
'Ctrl-S': () => {
parent.postMessage('solveTask', '*');
return false;
},
'Cmd-S': () => {
parent.postMessage('solveTask', '*');
return false;
},
});
9 changes: 3 additions & 6 deletions front/src/components/Dialogs/SearchDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
<q-card-section class="row items-center no-wrap">
<div class="text-h6 ellipsis">Global search</div>
<q-space />
<q-chip square dense class="keycap q-ml-md">ctrl</q-chip>+<q-chip
square
dense
class="keycap"
>k</q-chip
>
<ShortcutHint :keys="['ctrl', 'k']" />
</q-card-section>

<q-card-section class="q-pt-none">
Expand Down Expand Up @@ -75,10 +70,12 @@ import ctfnote from 'src/ctfnote';
import { safeSlugify } from 'src/ctfnote/ctfs';
import { Ctf, Task } from 'src/ctfnote/models';
import { defineComponent, onMounted, onUnmounted, Ref, ref } from 'vue';
import ShortcutHint from '../Utils/ShortcutHint.vue';
import TaskTagsList from '../Task/TaskTagsList.vue';

export default defineComponent({
components: {
ShortcutHint,
TaskTagsList,
},
emits: useDialogPluginComponent.emits,
Expand Down
92 changes: 92 additions & 0 deletions front/src/components/Dialogs/TaskSolveDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<template>
<q-dialog ref="dialogRef" @hide="onDialogHide">
<q-card class="dialog-solve-task">
<q-form @submit="submit">
<q-card-section class="row items-center no-wrap">
<div class="text-h6 ellipsis">Submit flag for {{ task.title }}</div>
<q-space />
<ShortcutHint v-if="$route.name === 'task'" :keys="['ctrl', 's']" />
</q-card-section>

<q-card-section class="q-pt-none q-pb-sm q-gutter-sm">
<q-input
v-model="form.flag"
filled
dense
label="Flag"
@vue:mounted="focusInput"
>
<template #prepend>
<q-icon name="flag" />
</template>
</q-input>
</q-card-section>

<q-card-actions align="right" class="q-px-md q-pb-md">
<q-btn v-close-popup flat color="primary" label="Cancel" />
<q-btn
color="positive"
type="submit"
label="Save"
style="width: 64px"
/>
</q-card-actions>
</q-form>
</q-card>
</q-dialog>
</template>

<script lang="ts">
import { useDialogPluginComponent } from 'quasar';
import { Task } from 'src/ctfnote/models';
import ctfnote from 'src/ctfnote';
import { defineComponent, reactive } from 'vue';
import { TaskPatch } from 'src/generated/graphql';
import ShortcutHint from '../Utils/ShortcutHint.vue';

export default defineComponent({
components: {
ShortcutHint,
},
props: {
task: { type: Object as () => Task, required: true },
},
emits: useDialogPluginComponent.emits,
setup(props) {
const form = reactive({
flag: props.task?.flag ?? '',
});

const { dialogRef, onDialogHide, onDialogOK } = useDialogPluginComponent();

return {
dialogRef,
form,
onDialogHide,
onDialogOK,
updateTask: ctfnote.tasks.useUpdateTask(),
notifySuccess: ctfnote.ui.useNotify().notifySuccess,
};
},
methods: {
async submit() {
this.onDialogOK();

const patch: TaskPatch = {};
if (this.form.flag !== this.task.flag) {
patch.flag = this.form.flag;
}
await this.updateTask(this.task, patch);
},
focusInput(target: { el: HTMLElement }) {
target.el.focus();
},
},
});
</script>

<style scoped>
.dialog-solve-task {
width: 400px;
}
</style>
40 changes: 40 additions & 0 deletions front/src/components/Utils/ShortcutHint.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<template v-for="(key, index) in platformKeys" :key="key">
<q-chip square dense class="keycap">
{{ key }}
</q-chip>
<template v-if="index < keys.length - 1"> + </template>
</template>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
props: {
keys: { type: Array, required: true },
},
setup(props) {
let platformKeys = props.keys;

// Replace keys with Apple keyboard symbols on macOS
if (navigator.userAgent.includes('Mac OS X')) {
platformKeys = props.keys.map((key) => {
if (key === 'ctrl') return '⌘';
if (key === 'alt') return '⌥';
if (key === 'shift') return '⇧';
else return key;
});
}

return {
platformKeys,
};
},
});
</script>

<style scoped>
.keycap {
background-color: rgba(200, 200, 200, 0.4);
}
</style>
48 changes: 21 additions & 27 deletions front/src/ctfnote/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
import { Ctf, Id, Task, WorkingOn, makeId } from './models';
import { Dialog } from 'quasar';
import TaskEditDialogVue from '../components/Dialogs/TaskEditDialog.vue';
import TaskSolveDialogVue from '../components/Dialogs/TaskSolveDialog.vue';
import { ref, computed } from 'vue';

export function buildWorkingOn(w: WorkingOnFragment): WorkingOn {
return {
Expand Down Expand Up @@ -56,36 +58,28 @@ export function useCancelWorkingOn() {
}

export function useSolveTaskPopup() {
const updateTask = useUpdateTask();
// Used to force opening at most one dialog at a time
const openedSolveTaskPopup = ref(false);

const lock = () => (openedSolveTaskPopup.value = true);
const unlock = () => (openedSolveTaskPopup.value = false);
const locked = computed(() => openedSolveTaskPopup.value);

return (task: Task) => {
// If the dialog is already opened, don't do anything
if (locked.value) return;

lock();

Dialog.create({
title: 'Submit flag for ' + task.title,
color: 'primary',
class: 'compact-dialog',
prompt: {
model: task.flag ?? '',
type: 'text',
label: 'Flag',
filled: true,
class: 'solve-task-popup-focus',
},
cancel: {
label: 'Cancel',
flat: true,
},
ok: {
color: 'positive',
label: 'Save',
component: TaskSolveDialogVue,
componentProps: {
task,
},
}).onOk((flag: string) => {
void updateTask(task, { flag });
});

window.setTimeout(() => {
(
document.querySelector('.solve-task-popup-focus') as HTMLElement
).focus();
}, 0);
})
.onOk(unlock)
.onCancel(unlock)
.onDismiss(unlock);
};
}

Expand Down
62 changes: 50 additions & 12 deletions front/src/pages/Task.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<q-page class="page">
<iframe v-if="task" :src="task.padUrl + '#'" />
<iframe v-if="task" :src="task.padUrl + '#'" @load="listenForHotkeys()" />

<div v-else class="flex justify-center items-start q-mt-md">
<q-card>
Expand All @@ -23,8 +23,10 @@
</template>

<script lang="ts">
import hotkeys from 'hotkeys-js';
import ctfnote from 'src/ctfnote';
import { Ctf, Id, Task } from 'src/ctfnote/models';
import { computed, defineComponent, onMounted, watch } from 'vue';
import { computed, defineComponent, onMounted, onUnmounted, watch } from 'vue';

export default defineComponent({
props: {
Expand All @@ -36,6 +38,10 @@ export default defineComponent({
() => props.ctf.tasks.find((t) => t.id == props.taskId) ?? null
);

const solveTask = ctfnote.tasks.useSolveTaskPopup();

const solveTaskShortcut = 'ctrl+s, command+s';

watch(
task,
(task) => {
Expand All @@ -46,20 +52,52 @@ export default defineComponent({
{ immediate: true }
);

let solveTaskShortcutListener: (
this: Window,
ev: MessageEvent<string>
) => void;

onMounted(() => {
// Listen for shortcut
hotkeys(solveTaskShortcut, function (event) {
event.stopImmediatePropagation();
event.preventDefault();

if (task.value !== null) {
solveTask(task.value);
}
});

// Listen for solve task shortcut from HedgeDoc iframe
solveTaskShortcutListener = (event) => {
if (event.origin !== window.location.origin) return;
if (event.data === 'solveTask') {
if (task.value !== null) {
solveTask(task.value);
}
}
};
window.addEventListener('message', solveTaskShortcutListener);
});

onUnmounted(() => {
hotkeys.unbind(solveTaskShortcut);
window.removeEventListener('message', solveTaskShortcutListener);
});

return { task, solveTask };
},
methods: {
listenForHotkeys() {
const taskFrame = window.frames[0];
if (taskFrame !== undefined) {
taskFrame.addEventListener('DOMContentLoaded', () => {
// inject hotkey script with some CTFNote code to catch hotkey for search dialog
// and communicate that with the parent window
const hotkeyScript = taskFrame.document.createElement('script');
hotkeyScript.src = '/pad/js/hotkeys-iframe.js'; // this won't exist in development but will in production
taskFrame.document.body.appendChild(hotkeyScript);
});
// inject hotkey script with some CTFNote code to catch hotkey for search dialog
// and communicate that with the parent window
const hotkeyScript = taskFrame.document.createElement('script');
hotkeyScript.src = '/pad/js/hotkeys-iframe.js'; // this won't exist in development but will in production
taskFrame.document.body.appendChild(hotkeyScript);
}
});

return { task };
},
},
});
</script>
Expand Down