commit 8d57bfb1aef3b71557bc408154ee028751fd688e Author: Timotej Lazar Date: Mon Jun 14 19:09:53 2021 +0200 First commit There was history before but now there is no more. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b539482 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.qmlc diff --git a/Event.qml b/Event.qml new file mode 100644 index 0000000..38d3eeb --- /dev/null +++ b/Event.qml @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 + +import 'util.js' as Util + +// This is the delegate for event list items. +Pane { + id: control + + property int time + property alias tag: tag.text + property alias fields: inputs.model + property bool editing: false + + signal remove + + clip: true + height: visible ? stuff.height + 2*padding : 0 // TODO fix filtering and remove this + padding: 2 + + // set to current value or default + function reset() { + for (var i = 0; i < fields.count; i++) { + const child = inputs.itemAt(i) + if (child && child.item) + child.item.set(fields.get(i).value) + } + } + + function store() { + for (var i = 0; i < fields.count; i++) + fields.setProperty(i, 'value', inputs.itemAt(i).item.value) + } + + // Pass keys to each field input in order. + Keys.forwardTo: Array.from({ length: inputs.count }, (_, i) => inputs.itemAt(i).item) + + Behavior on height { NumberAnimation { duration: 50 } } + + ColumnLayout { + id: stuff + anchors { left: parent.left; right: parent.right; margins: 5 } + + RowLayout { + Label { + text: new Date(time).toISOString().substr(12, 9) + font.pixelSize: 10 + Layout.alignment: Qt.AlignBaseline + } + Label { + id: tag + font.weight: Font.DemiBold + Layout.alignment: Qt.AlignBaseline + } + Label { + text: { + var str = '' + for (var i = 0; i < fields.count; i++) { + const field = fields.get(i) + if (field.value && field.type !== 'TextArea') + str += (field.type === 'Bool' ? field.name : field.value) + ' ' + } + return str + } + elide: Text.ElideRight + textFormat: Text.PlainText + Layout.fillWidth: true + Layout.alignment: Qt.AlignBaseline + } + } + + // Event‐specific inputs. + GridLayout { + id: fieldset + + flow: GridLayout.TopToBottom + rows: inputs.count + + columnSpacing: 10 + visible: editing + + // Labels. + Repeater { + model: inputs.model + delegate: Label { + text: Util.addShortcut(model.name, model.key) + Layout.alignment: Qt.AlignRight + } + } + + // Inputs. + Repeater { + id: inputs + delegate: Loader { + source: 'qrc:/Fields/' + model.type + '.qml' + Layout.fillHeight: true + Layout.fillWidth: true + Binding { + target: item; property: 'definition' + value: model + } + } + } + } + } +} diff --git a/Events.qml b/Events.qml new file mode 100644 index 0000000..8853f90 --- /dev/null +++ b/Events.qml @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 +import QtQml.Models 2.1 + +import 'util.js' as Util + +ListView { + id: control + + property bool editing: false + property var tags: [] + + signal changed + + clip: true + focus: true + keyNavigationEnabled: true + highlightMoveDuration: 0 + highlightResizeDuration: 0 + ScrollBar.vertical: ScrollBar { anchors.right: parent.right } + + // Create a new blank event, insert it and start editing. + function create(time, tag, fields) { + const index = Util.find(list, 'time', time) + list.insert(index, { + 'time': time, + 'tag': tag, + 'fields': fields, + }) + currentIndex = index + if (fields.length > 0) + editing = true + changed() + } + + function clear() { + list.clear() + } + + function load(json) { + // Return list of fields for the given tag. + function getFields(name) { + for (var i = 0; i < tags.length; i++) + if (tags[i].tag === name) + return tags[i].fields + return [] + } + + for (var i = 0; i < json.length; i++) { + const event = json[i] + var fields = getFields(event.tag) + for (var j = 0; j < fields.length; j++) + fields[j].value = event.fields[fields[j].name] + list.append({ 'time': event.time, 'tag': event.tag, 'fields': fields }) + } + forceActiveFocus() + } + + function save() { + var data = [] + for (var i = 0; i < list.count; i++) { + const event = list.get(i) + var fields = {} + for (var j = 0; j < event.fields.count; j++) { + const field = event.fields.get(j) + fields[field.name] = field.value + } + data.push({ 'time': event.time, 'tag': event.tag, 'fields': fields }) + } + return data + } + + onCurrentIndexChanged: editing = false + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + if (editing) { + currentItem.store() + changed() + editing = false + } else { + if (currentItem.fields.count > 0) + editing = true + } + break + case Qt.Key_Escape: + if (editing) { + currentItem.reset() + editing = false + } + break + case Qt.Key_Delete: + editing = false + if (currentIndex >= 0 && currentIndex < list.count) { + list.remove(currentIndex) + changed() + } + break + case Qt.Key_Tab: + case Qt.Key_Backtab: + // swallow tabs so we don’t lose focus when editing + break + default: + return + } + event.accepted = true + } + + model: ListModel { + id: list + dynamicRoles: true + } + + delegate: Event { + id: item + + time: model.time + tag: model.tag + fields: model.fields + + width: control.width + editing: control.editing && ListView.isCurrentItem + + background: Rectangle { + anchors.fill: parent + color: border.width > 0 ? Util.alphize(border.color, 0.1) : + (index % 2 === 0 ? palette.base : palette.alternateBase) + border { + color: editing ? palette.highlight : palette.dark + width: item.ListView.isCurrentItem ? 1 : 0 + } + radius: border.width + } + + Connections { + enabled: ListView.currentIndex === index + function onHeightChanged() { + control.positionViewAtIndex(index, ListView.Contain) + } + } + onEditingChanged: { + reset() + if (editing) + forceActiveFocus() + } + onRemove: { + list.remove(ObjectModel.index) + } + } +} diff --git a/Fields/Bool.qml b/Fields/Bool.qml new file mode 100644 index 0000000..ccb0758 --- /dev/null +++ b/Fields/Bool.qml @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 + +Row { + id: control + width: parent.width + + property var definition + property alias value: input.checked + + Keys.onPressed: { + if (event.text === definition.key) { + value = !value + event.accepted = true + } + } + function set(val) { value = val || false } + + CheckBox { + id: input + focusPolicy: Qt.NoFocus + padding: 0 + font.capitalization: Font.SmallCaps + } +} diff --git a/Fields/Enum.qml b/Fields/Enum.qml new file mode 100644 index 0000000..30712b6 --- /dev/null +++ b/Fields/Enum.qml @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 + +import '../util.js' as Util + +Column { + id: control + + property var definition + property int index: -1 + readonly property string value: index >= 0 ? definition.values.get(index).name : '' + + function set(val) { + for (var i = 0; i < definition.values.count; i++) { + if (definition.values.get(i).name === val) { + index = i + return true + } + } + index = -1 + } + + Keys.onPressed: { + for (var i = 0; i < definition.values.count; i++) { + if (definition.values.get(i).key === event.text) { + index = (index === i ? -1 : i) + event.accepted = true + break + } + } + } + + Flow { + spacing: 5 + width: parent.width + + ButtonGroup { id: buttons } + + Repeater { + model: definition.values + delegate: Button { + ButtonGroup.group: buttons + checkable: true + checked: control.index === index + focusPolicy: Qt.NoFocus + + implicitWidth: implicitContentWidth + leftPadding + rightPadding + padding: 0 + leftPadding: 5 + rightPadding: leftPadding + + onClicked: control.index = (control.index === index ? -1 : index) + text: Util.addShortcut(name, key) + } + } + } +} diff --git a/Fields/Text.qml b/Fields/Text.qml new file mode 100644 index 0000000..49d7ad2 --- /dev/null +++ b/Fields/Text.qml @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 + +Label { + id: control + + property var definition + property alias value: control.text + + Keys.onPressed: { + if (event.text === definition.key) { + popup.open() + event.accepted = true + } + } + + function set(val) { value = val || '' } + + elide: Text.ElideRight + + Popup { + id: popup + + width: control.width + height: control.height + padding: 0 + + onOpened: { + input.text = value + input.forceActiveFocus() + } + + TextInput { + id: input + + clip: true + padding: 2 + topPadding: 0 + bottomPadding: 0 + width: parent.width + + onAccepted: { + value = input.text.trim() + popup.close() + } + } + } +} diff --git a/Fields/TextArea.qml b/Fields/TextArea.qml new file mode 100644 index 0000000..20cfeff --- /dev/null +++ b/Fields/TextArea.qml @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 + +Label { + id: control + + property var definition + property alias value: control.text + + Keys.onPressed: { + if (event.text === definition.key) { + popup.open() + event.accepted = true + } + } + + function set(val) { value = (val || '').trim() } + + wrapMode: Text.Wrap + + Popup { + id: popup + + width: control.width + height: input.height + padding: 0 + + onOpened: { + input.text = value + input.forceActiveFocus() + } + + TextArea { + id: input + + padding: 2 + topPadding: 0 + bottomPadding: 0 + width: parent.width + wrapMode: TextEdit.Wrap + + Keys.onPressed: { + if (event.modifiers === Qt.NoModifier) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + value = input.text.trim() + popup.close() + } + } + } + } + } +} diff --git a/Filter.qml b/Filter.qml new file mode 100644 index 0000000..e1b5f93 --- /dev/null +++ b/Filter.qml @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 + +GridLayout { + property var tags: [] + + function check(item) { + if (item.tag === tags.currentText) + return true + return false + } + + signal changed + + columns: 2 + + Label { text: qsTr('Tag') } + ComboBox { + model: tags + textRole: 'tag' + Layout.fillWidth: true + onCurrentTextChanged: changed() + } + + Label { + text: qsTr('Filters are not implemented yet! 😊') + wrapMode: Text.Wrap + Layout.fillWidth: true + Layout.columnSpan: 2 + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..1738462 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Fuzbal + +Friendly Usable Zero‐Bullshit Analyzer & Labeler: a keyboard‐driven utility for tagging events in video clips, created for analyzing football matches but likely useful for other kinds of videos. + +While functional, this project is not yet production‐ready. While unlikely, it might eat your files or your cake. + +## Usage + +Open a video. Press `space` to start or stop video playback. Seek with `←` and `→`. Use `,` and `.` to decrease and increase the playback rate, and `=` to reset it. + +To add a new event, press the key for the corresponding tag and fill out event details. Custom tags can be defined as a JSON array and loaded at runtime. See `tags.json` for the built‐in example showcasing all supported field types. + +Events for `video.mp4` are saved in JSON format in the file `video.mp4.events`. Saved file includes tag definitions, which are loaded automatically when the file is opened. Event timestamps are stored with millisecond precision. + +## Compiling + +Qt≥5.14 is required. Once Debian catches up, this might be enough: + + # apt install git qtmultimedia-dev qtquickcontrols2-dev qml-module-qtmultimedia qml-module-qtquick-dialogs + +One or more of the `gst-plugins` packages are needed at runtime to play videos. Build with: + + $ mkdir build && cd build + $ qmake .. + $ make + +This should create the `fuzbal` binary. + +## License + +This project is released into the public domain. Breeze icons are distributed under LGPL3+. See `UNLICENSE` and `icons/breeze/LICENSE` for details. diff --git a/Sidebar.qml b/Sidebar.qml new file mode 100644 index 0000000..2e61713 --- /dev/null +++ b/Sidebar.qml @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 +import Qt.labs.platform 1.1 + +Page { + id: control + + property bool modified: false + property Video video + + function clear() { + description.clear() + events.clear() + } + + function save() { + modified = false + return { + meta: { + version: Qt.application.version, + video: video.source.toString(), + description: description.text + }, + tags: tags.model, + events: events.save() + } + } + + function load(data) { + if (data.meta.description !== undefined) + description.text = data.meta.description + if (data.tags !== undefined) + tags.model = data.tags + events.load(data.events) + modified = false + } + + FileDialog { + id: videoDialog + title: qsTr('Open video') + onAccepted: { + clear() + video.source = currentFile + const events = io.read(video.source+'.events') + if (events) + load(JSON.parse(events)) + } + } + + FileDialog { + id: tagsDialog + title: qsTr('Load tags') + nameFilters: [qsTr('JSON files (*.json)'), qsTr('All files (*)')] + onAccepted: tags.model = JSON.parse(io.read(currentFile)) + } + + Keys.forwardTo: [tags, video] + + header: ToolBar { + horizontalPadding: 0 + RowLayout { + anchors.fill: parent + ToolButton { + action: Action { + icon.name: 'document-open' + shortcut: StandardKey.Open + onTriggered: videoDialog.open() + } + focusPolicy: Qt.NoFocus + } + Label { + text: video.loaded ? video.source : '' + elide: Text.ElideLeft + Layout.fillWidth: true + } + ToolButton { + action: Action { + onTriggered: io.write(video.source+'.events', JSON.stringify(save())) + shortcut: StandardKey.Save + icon.name: 'document-save' + enabled: video.loaded && control.modified + } + visible: video.loaded + opacity: enabled ? 1 : 0.25 + focusPolicy: Qt.NoFocus + } + } + } + + ColumnLayout { + anchors.fill: parent + + // Description box. + ColumnLayout { + spacing: 0 + RowLayout { + Label { + text: qsTr('Description') + Layout.fillWidth: true + } + Label { text: description.enabled ? qsTr('−') : qsTr('+') } + TapHandler { onTapped: description.enabled = !description.enabled } + } + + ScrollView { + Layout.fillWidth: true + Layout.preferredHeight: 100 + contentWidth: parent.availableWidth + padding: 1 + + visible: description.enabled + background: Frame { } + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + TextArea { + id: description + + background: Rectangle { color: palette.base } + leftPadding: padding + selectByMouse: true + wrapMode: Text.Wrap + + onTextChanged: modified = true + KeyNavigation.priority: KeyNavigation.BeforeItem + KeyNavigation.tab: events + } + } + } + + // Events list. + ColumnLayout { + spacing: 0 + Label { text: qsTr('Events') } + Frame { + padding: 1 + Layout.fillWidth: true + Layout.fillHeight: true + + Events { + id: events + + anchors.fill: parent + focus: true + tags: tags.model + + onEditingChanged: video.pause(editing) + onChanged: modified = true + + MouseArea { + anchors.fill: parent + enabled: !parent.editing + onPressed: { + const index = events.indexAt(mouse.x, mouse.y) + if (index !== -1) { + events.currentIndex = index + video.seek(events.itemAtIndex(index).time) + } + forceActiveFocus() + } + } + } + } + } + + Page { + Layout.fillWidth: true + Layout.fillHeight: false + + StackLayout { + currentIndex: bar.currentIndex + implicitHeight: children[currentIndex].implicitHeight + width: parent.width + + Frame { + padding: 5 + enabled: visible + Layout.fillWidth: true + + ColumnLayout { + width: parent.width + spacing: 0 + + RowLayout { + Label { + text: qsTr('Tags') + Layout.fillWidth: true + } + ToolButton { + icon.name: 'document-open' + Layout.alignment: Qt.AlignTop + onClicked: tagsDialog.open() + focusPolicy:Qt.NoFocus + } + } + Tags { + id: tags + model: JSON.parse(io.read('qrc:/tags.json')) + enabled: video.loaded && !events.editing + onClicked: events.create(video.time, tag, fields) + Layout.fillWidth: true + } + } + } + + Frame { + padding: 5 + enabled: visible + Layout.fillWidth: true + + Filter { + id: filter + tags: tags.model + width: parent.width + onChanged: print('filter changed') + } + } + } + + footer: TabBar { + id: bar + Layout.fillWidth: true + ActionGroup { id: tabActions } + Repeater { + model: [ + { text: qsTr('&Annotate'), shortcut: qsTr('Alt+A') }, + { text: qsTr('&Filter'), shortcut: qsTr('Alt+F') } + ] + delegate: TabButton { + action: Action { + ActionGroup.group: tabActions + shortcut: modelData.shortcut + } + text: modelData.text + focusPolicy: Qt.NoFocus + padding: 5 + onClicked: TabBar.tabBar.setCurrentIndex(index) + } + } + } + } + } +} diff --git a/Tags.qml b/Tags.qml new file mode 100644 index 0000000..7d3f0ce --- /dev/null +++ b/Tags.qml @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 + +import 'util.js' as Util + +// Tag list. +Page { + id: control + + property alias model: tags.model + + signal clicked(string tag, var fields) + + Keys.enabled: enabled + Keys.onPressed: { + for (var i = 0; i < model.length; i++) { + const tag = model[i] + if (tag.key === event.text) { + clicked(tag.tag, tag.fields) + return + } + } + event.accepted = false + } + + RowLayout { + width: parent.width + + Flow { + spacing: 5 + Layout.fillWidth: true + + Repeater { + id: tags + delegate: Button { + text: Util.addShortcut(modelData.tag, modelData.key) + onClicked: control.clicked(modelData.tag, modelData.fields) + focusPolicy: Qt.NoFocus + implicitWidth: implicitContentWidth + 2*padding + } + } + } + } +} diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/Video.qml b/Video.qml new file mode 100644 index 0000000..f63a7ce --- /dev/null +++ b/Video.qml @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.14 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 +import QtMultimedia 5.11 + +Page { + property bool loaded: + media.status !== MediaPlayer.NoMedia && + media.status !== MediaPlayer.InvalidMedia && + media.status !== MediaPlayer.UnknownStatus + property alias source: media.source + property alias time: media.position + + function pause(yes) { + if (yes === undefined) + yes = media.playbackState === MediaPlayer.PlayingState + if (yes) + media.pause() + else + media.play() + } + + function seek(offset, relative) { + if (relative) + offset += media.position + media.seek(offset) + } + + Keys.onPressed: { + switch (event.key) { + // (Un)pause video. + case Qt.Key_Space: + pause() + break + // Seek video. + case Qt.Key_Left: + seek(-500, true) + break + case Qt.Key_Right: + seek(500, true) + break + // Change playback rate. + case Qt.Key_Equal: + rate.reset() + break + case Qt.Key_Comma: + rate.decrease() + break + case Qt.Key_Period: + rate.increase() + break + default: + return // don’t accept the event + } + event.accepted = true + } + + // Video. + ColumnLayout { + spacing: 0 + anchors.fill: parent + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: 'black' + clip: true + + VideoOutput { + anchors.fill: parent + fillMode: VideoOutput.PreserveAspectFit + source: media + + transform: Scale { + id: zoom + property real scale: 1.0 + xScale: scale + yScale: scale + origin.x: wheel.point.position.x + origin.y: wheel.point.position.y + } + + MediaPlayer { + id: media + notifyInterval: 100 + playbackRate: Number.fromLocaleString(rate.displayText) + volume: QtMultimedia.convertVolume( + volume.value, + QtMultimedia.LogarithmicVolumeScale, + QtMultimedia.LinearVolumeScale) + } + + TapHandler { + acceptedButtons: Qt.RightButton + onTapped: pause() + } + + WheelHandler { + id: wheel + onWheel: zoom.scale = Math.max(1.0, (event.angleDelta.y > 0 ? 1.1 : 0.9) * zoom.scale) + } + } + } + + // Video controls. + RowLayout { + Layout.margins: 5 + + Button { + icon.name: 'media-playback-pause' + implicitWidth: implicitHeight + checkable: true + checked: media.playbackState !== MediaPlayer.PlayingState + onClicked: checked ? media.pause() : media.play() + } + Label { text: new Date(media.position).toISOString().substr(12, 9) } + Slider { + Layout.fillWidth: true + from: 0; to: media.duration + value: media.position + onMoved: media.seek(value) + } + Label { text: new Date(media.duration).toISOString().substr(12, 7) } + + Volume { + id: volume + muted: media.muted + focusPolicy: Qt.NoFocus + } + + // Playback speed control. + SpinBox { + id: rate + implicitWidth: 80 + focusPolicy: Qt.NoFocus + + from: 25; to: 250; stepSize: 25 + value: 100 + + function reset() { value = 100 } + + textFromValue: function (value, locale) { + return (value / 100).toLocaleString(locale, 'f', 2) + } + } + } + } +} diff --git a/Volume.qml b/Volume.qml new file mode 100644 index 0000000..cbf41e9 --- /dev/null +++ b/Volume.qml @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick.Controls 2.13 + +Button { + property bool muted + property alias value: volume.value + + implicitWidth: implicitHeight + icon.name: 'audio-volume-' + + (muted ? 'muted' : + (value < 0.33 ? 'low' : + (value < 0.66 ? 'medium' : 'high'))) + + checkable: true + checked: popup.opened + + onClicked: popup.opened ? popup.close() : popup.open() + Popup { + id: popup + y: -height + height: 100 + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + Slider { + id: volume + anchors.fill: parent + orientation: Qt.Vertical + } + } +} diff --git a/fuzbal.pro b/fuzbal.pro new file mode 100644 index 0000000..dc20822 --- /dev/null +++ b/fuzbal.pro @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Unlicense + +QT += multimedia qml quick quickcontrols2 svg widgets + +CONFIG += embed_translations lrelease + +DEFINES += GIT_VERSION=\\\"$$system(git -C "$$_PRO_FILE_PWD_" describe --always --tags)\\\" + +SOURCES += \ + main.cpp + +HEADERS += \ + io.h + +RESOURCES += main.qrc icons.qrc + +TRANSLATIONS += translations/fuzbal_sl.ts diff --git a/icons.qrc b/icons.qrc new file mode 100644 index 0000000..81773a5 --- /dev/null +++ b/icons.qrc @@ -0,0 +1,15 @@ + + + + icons/breeze/index.theme + icons/breeze/actions/symbolic/document-open.svg + icons/breeze/actions/symbolic/document-save.svg + icons/breeze/actions/symbolic/edit-clear.svg + icons/breeze/actions/symbolic/edit-delete.svg + icons/breeze/actions/symbolic/media-playback-pause.svg + icons/breeze/status/symbolic/audio-volume-high.svg + icons/breeze/status/symbolic/audio-volume-low.svg + icons/breeze/status/symbolic/audio-volume-medium.svg + icons/breeze/status/symbolic/audio-volume-muted.svg + + diff --git a/icons/breeze/LICENSE b/icons/breeze/LICENSE new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/icons/breeze/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/icons/breeze/actions/symbolic/document-open.svg b/icons/breeze/actions/symbolic/document-open.svg new file mode 100644 index 0000000..4d2b838 --- /dev/null +++ b/icons/breeze/actions/symbolic/document-open.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/actions/symbolic/document-save.svg b/icons/breeze/actions/symbolic/document-save.svg new file mode 100644 index 0000000..cd2db5a --- /dev/null +++ b/icons/breeze/actions/symbolic/document-save.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/actions/symbolic/edit-clear.svg b/icons/breeze/actions/symbolic/edit-clear.svg new file mode 100644 index 0000000..f49be9b --- /dev/null +++ b/icons/breeze/actions/symbolic/edit-clear.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/actions/symbolic/edit-delete.svg b/icons/breeze/actions/symbolic/edit-delete.svg new file mode 100644 index 0000000..9dfb2e0 --- /dev/null +++ b/icons/breeze/actions/symbolic/edit-delete.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/icons/breeze/actions/symbolic/media-playback-pause.svg b/icons/breeze/actions/symbolic/media-playback-pause.svg new file mode 100644 index 0000000..37ef343 --- /dev/null +++ b/icons/breeze/actions/symbolic/media-playback-pause.svg @@ -0,0 +1,8 @@ + + + + diff --git a/icons/breeze/index.theme b/icons/breeze/index.theme new file mode 100644 index 0000000..cd4128b --- /dev/null +++ b/icons/breeze/index.theme @@ -0,0 +1,580 @@ +[Icon Theme] +Name=Breeze +Name[ar]=نسيم +Name[ast]=Breeze +Name[ca]=Brisa +Name[ca@valencia]=Brisa +Name[cs]=Breeze +Name[da]=Breeze +Name[de]=Breeze +Name[el]=Breeze +Name[en_GB]=Breeze +Name[es]=Brisa +Name[et]=Breeze +Name[eu]=Breeze +Name[fi]=Breeze +Name[fr]=Breeze +Name[gd]=Oiteag +Name[gl]=Breeze +Name[hu]=Breeze +Name[ia]=Breeze +Name[id]=Breeze +Name[it]=Brezza +Name[ko]=Breeze +Name[lt]=Breeze +Name[nl]=Breeze +Name[nn]=Breeze +Name[pl]=Bryza +Name[pt]=Brisa +Name[pt_BR]=Breeze +Name[ru]=Breeze +Name[sk]=Vánok +Name[sl]=Sapica (Breeze) +Name[sr]=Поветарац +Name[sr@ijekavian]=Поветарац +Name[sr@ijekavianlatin]=Povetarac +Name[sr@latin]=Povetarac +Name[sv]=Breeze +Name[tg]=Насим +Name[uk]=Breeze +Name[x-test]=xxBreezexx +Name[zh_CN]=微风 +Name[zh_TW]=Breeze + +Comment=Breeze by the KDE VDG +Comment[ast]=Breeze pol VDG de KDE +Comment[ca]=Brisa, creat pel VDG del KDE +Comment[ca@valencia]=Brisa pel VDG del KDE +Comment[cs]=Breeze od KDE VDG +Comment[da]=Breeze af KDE VDG +Comment[de]=Breeze von der KDE VDG +Comment[en_GB]=Breeze by the KDE VDG +Comment[es]=Brisa por KDE VDG +Comment[et]=Breeze KDE VDG-lt +Comment[eu]=Breeze, KDE VDGk egina +Comment[fi]=Breeze KDE VDG:ltä +Comment[fr]=Breeze, par KDE VDG +Comment[gl]=Breeze de KDE VDG +Comment[hu]=Breeze a KDE VDG-től +Comment[ia]=Breeze (Brisa) per le KDE VDG +Comment[id]=Breeze oleh KDE VDG +Comment[it]=Brezza del KDE VDG +Comment[ko]=KDE 시각 디자인 그룹에서 제작한 Breeze +Comment[lt]=Breeze pagal KDE VDG +Comment[nl]=Breeze door de KDE VDG +Comment[nn]=Breeze frå KDE VDG +Comment[pl]=Bryza autorstwa KDE VDG +Comment[pt]=Brisa da VDG do KDE +Comment[pt_BR]=Breeze pelo KDE VDG +Comment[ru]=Breeze от KDE VDG +Comment[sk]=Vánok od KDE VDG +Comment[sl]=Breeze od KDE VDG +Comment[sv]=Breeze av KDE:s visuella designgrupp +Comment[tg]=Насим аз KDE VDG +Comment[uk]=Breeze, автори — KDE VDG +Comment[x-test]=xxBreeze by the KDE VDGxx +Comment[zh_CN]=微风,由 KDE VDG 创作 +Comment[zh_TW]=由 KDE VDG 團隊製作的 Breeze + +DisplayDepth=32 + +Inherits=hicolor + +Example=folder + +FollowsColorScheme=true + +DesktopDefault=48 +DesktopSizes=16,22,32,48,64,128,256 +ToolbarDefault=22 +ToolbarSizes=16,22,32,48 +MainToolbarDefault=22 +MainToolbarSizes=16,22,32,48 +SmallDefault=16 +SmallSizes=16,22,32,48 +PanelDefault=48 +PanelSizes=16,22,32,48,64,128,256 +DialogDefault=32 +DialogSizes=16,22,32,48,64,128,256 + +KDE-Extensions=.svg + +########## Directories +########## ordered by category and alphabetically + +Directories=actions/12,actions/16,actions/22,actions/24,actions/32,actions/64,animations/16,animations/22,apps/16,apps/22,apps/32,apps/48,preferences/32,applets/22,applets/48,applets/64,applets/128,applets/256,categories/32,devices/16,devices/22,devices/64,emblems/8,emblems/16,emblems/22,emotes/22,mimetypes/16,mimetypes/22,mimetypes/32,mimetypes/64,places/16,places/22,places/32,places/64,status/16,status/22,status/24,status/32,status/64,actions/symbolic,devices/symbolic,emblems/symbolic,places/symbolic,status/symbolic +ScaledDirectories=actions/16@2x,actions/22@2x,actions/24@2x,actions/32@2x,animations/16@2x,apps/16@2x,apps/22@2x,devices/16@2x,devices/22@2x,emblems/16@2x,emblems/22@2x,emotes/22@2x,mimetypes/16@2x,mimetypes/22@2x,places/16@2x,places/22@2x,status/16@2x,status/22@2x + +########## Actions +########## ordered by size + +#12x12 - Fixed size - For Inkscape +[actions/12] +Size=12 +Context=Actions +Type=Fixed + +#16x16 - Fixed size - For use in sidebar(s) smaller toolbar(s) >!!!ONLY!!!<: e.g. Kate movable sidebar/toolbar (search and replace, current project, etc.) or Juk tree view - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/16] +Size=16 +Context=Actions +Type=Fixed + +#16x16@2x - Fixed size - For use in sidebar(s) smaller toolbar(s) >!!!ONLY!!!<: e.g. Kate movable sidebar/toolbar (search and replace, current project, etc.) or Juk tree view - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/16@2x] +Size=16 +Scale=2 +Context=Actions +Type=Fixed + +#22x22 - Fixed size - For toolbar icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/22] +Size=22 +Context=Actions +Type=Fixed + +#22x22@2x - Fixed size - For toolbar icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/22@2x] +Size=22 +Scale=2 +Context=Actions +Type=Fixed + +#24x24 - Fixed size - GTK icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/24] +Size=24 +Context=Actions +Type=Fixed + +#24x24@2x - Fixed size - GTK icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/24@2x] +Size=24 +Scale=2 +Context=Actions +Type=Fixed + +#32x32 - Fixed size - For toolbar icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/32] +Size=32 +Context=Actions +Type=Scalable +MinSize=32 +MaxSize=256 + +#32x32@2x - Fixed size - For toolbar icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/32@2x] +Size=32 +Scale=2 +Context=Actions +Type=Scalable +MinSize=32 +MaxSize=256 + +#64x64 - Fixed size - For toolbar icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[actions/64] +Size=64 +Context=Actions +Type=Scalable +MinSize=32 +MaxSize=256 + +########## Animations +########## ordered by size + +#16x16 - Fixed size - Application icon(s) for Dolphin sidebar - OPTIONAL + DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[animations/16] +Size=16 +Context=Animations +Type=Fixed + +#16x16@2x - Fixed size - Application icon(s) for Dolphin sidebar - OPTIONAL + DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[animations/16@2x] +Size=16 +Scale=2 +Context=Animations +Type=Fixed + +#22x22 - Scalable +[animations/22] +Size=22 +Context=Animations +Type=Scalable +MinSize=22 +MaxSize=256 + +########## Apps +########## ordered by size + +#16x16 - Fixed size - Application icon(s) for Dolphin sidebar - OPTIONAL + DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[apps/16] +Size=16 +Context=Applications +Type=Fixed + +#16x16@2x - Fixed size - Application icon(s) for Dolphin sidebar - OPTIONAL + DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[apps/16@2x] +Size=16 +Scale=2 +Context=Applications +Type=Fixed + +#22x22 - Fixed size - Workaround icon(s) for toolbar(s) button(s) e.g. Dolphin Open Terminal/About Dolphin/About KDE buttons - WRONG_ICON_USAGE_BY_APP - Monochrome +[apps/22] +Size=22 +Context=Applications +Type=Fixed + +#22x22@2x - Fixed size - Workaround icon(s) for toolbar(s) button(s) e.g. Dolphin Open Terminal/About Dolphin/About KDE buttons - WRONG_ICON_USAGE_BY_APP - Monochrome +[apps/22@2x] +Size=22 +Scale=2 +Context=Applications +Type=Fixed + +#32x32 - Fixed size - For System Settings icons >!!!ONLY!!!< - Scalable to the following sizes: 32x32 (default), 64x64, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[apps/32] +Size=32 +Context=Applications +Type=Fixed + +#48x48 - Scalable - For application icons >!!!ONLY!!!< - Scalable to the following sizes: 48x48 (default), 96x96 and 24x24 (not recommended) - DO_NOT_USE_ANYWHERE_ELSE - Color +[apps/48] +Size=48 +Context=Applications +Type=Scalable +MinSize=48 +MaxSize=256 + +#32x32 - Fixed size - For System Settings icons >!!!ONLY!!!< - Scalable to the following sizes: 32x32 (default), 64x64, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[preferences/32] +Size=32 +Context=Applications +Type=Scalable +MinSize=32 +MaxSize=256 + +#256x256 - Color for applets +[applets/22] +Size=22 +Context=Status +Type=Scalable +MinSize=22 +MaxSize=256 + +#256x256 - Color for applets +[applets/48] +Size=48 +Context=Status +Type=Scalable +MinSize=32 +MaxSize=256 + +#256x256 - Animation icons for kwin desktop effects +[applets/64] +Size=64 +Context=Status +Type=Scalable +MinSize=32 +MaxSize=256 + +#256x256 - Color +[applets/128] +Size=128 +Context=Applications +Type=Scalable +MinSize=32 +MaxSize=256 + +#256x256 - Scalable - For applets / widgets icons >!!!ONLY!!! - DO_NOT_USE_ANYWHERE_ELSE - Color +[applets/256] +Size=256 +Context=Applications +Type=Scalable +MinSize=48 +MaxSize=256 + +########## Categories +########## ordered by size + +#32x32 - Fixed size - For categories icons >!!!ONLY!!!< - Used in Kickoff (KDE 4.x.x) and Lancelot. Also used in MATE and Cinnamon (just FYI) - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[categories/32] +Size=32 +Context=Categories +Type=Scalable +MinSize=32 +MaxSize=256 + +########## Devices +########## ordered by size + +#16x16 - Fixed size - For small device icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[devices/16] +Size=16 +Context=Devices +Type=Fixed + + +#16x16@2x - Fixed size - For small device icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[devices/16@2x] +Size=16 +Scale=2 +Context=Devices +Type=Fixed + +#22x22 - Fixed size - For small device icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[devices/22] +Size=22 +Context=Devices +Type=Fixed + +#22x22@2x - Fixed size - For small device icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[devices/22@2x] +Size=22 +Scale=2 +Context=Devices +Type=Fixed + +#64x64 - Scalable - For device icons >!!!ONLY!!!< - Scalable to the following sizes: 64x64 (default), 32x32, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[devices/64] +Size=64 +Context=Devices +Type=Scalable +MinSize=24 +MaxSize=256 + +########## Emblems +########## ordered by size + +#8x8 - Fixed size - File system emblems - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[emblems/8] +Size=8 +Context=Emblems +Type=Fixed + +#16x16 - Fixed size - File system emblems - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[emblems/16] +Size=16 +Context=Emblems +Type=Fixed + +#16x16@2x - Fixed size - File system emblems - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[emblems/16@2x] +Size=16 +Scale=2 +Context=Emblems +Type=Fixed + +#22x22 - Fixed size - File system emblems - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[emblems/22] +Size=22 +Context=Emblems +Type=Fixed + +#22x22@2x - Fixed size - File system emblems - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[emblems/22@2x] +Size=22 +Scale=2 +Context=Emblems +Type=Fixed + +########## Emoticons +########## ordered by size + +#22x22 - Fixed size - Emoticons - DO_NOT_USE_ANYWHERE_ELSE - Color/flat +[emotes/22] +Size=22 +Context=Emotes +Type=Fixed + +#22x22@2x - Fixed size - Emoticons - DO_NOT_USE_ANYWHERE_ELSE - Color/flat +[emotes/22@2x] +Size=22 +Scale=2 +Context=Emotes +Type=Fixed + +########## Mimetypes +########## ordered by size + +#16x16 - Fixed size - For small file type icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[mimetypes/16] +Size=16 +Context=MimeTypes +Type=Fixed +MinSize=16 + +#16x16@2x - Fixed size - For small file type icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[mimetypes/16@2x] +Size=16 +Scale=2 +Context=MimeTypes +Type=Fixed +MinSize=16 + +#22x22 - Fixed size - For small file type icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[mimetypes/22] +Size=22 +Context=MimeTypes +Type=Scalable +MinSize=22 +MaxSize=24 + +#22x22@2x - Fixed size - For small file type icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[mimetypes/22@2x] +Size=22 +Scale=2 +Context=MimeTypes +Type=Scalable +MinSize=22 +MaxSize=24 + +#32x32 - Scalable - For file type icons >!!!ONLY!!!< - Scalable to the following sizes: 64x64 (default), 32x32, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[mimetypes/32] +Size=32 +Context=MimeTypes +Type=Scalable +MinSize=32 +MaxSize=48 + +#64x64 - Scalable - For file type icons >!!!ONLY!!!< - Scalable to the following sizes: 64x64 (default), 32x32, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[mimetypes/64] +Size=64 +Context=MimeTypes +Type=Scalable +MinSize=64 +MaxSize=256 + +########## Places +########## ordered by size + +#16x16 - Fixed size - For small folder icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[places/16] +Size=16 +Context=Places +Type=Fixed +MinSize=16 + +#16x16@2x - Fixed size - For small folder icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[places/16@2x] +Size=16 +Scale=2 +Context=Places +Type=Fixed +MinSize=16 + +#22x22 - Fixed size - Workaround icon(s) for toolbar(s) button(s) e.g. KMail trash icon - WRONG_ICON_USAGE_BY_APP - Monochrome +[places/22] +Size=22 +Context=Places +Type=Fixed + +#22x22@2x - Fixed size - Workaround icon(s) for toolbar(s) button(s) e.g. KMail trash icon - WRONG_ICON_USAGE_BY_APP - Monochrome +[places/22@2x] +Size=22 +Scale=2 +Context=Places +Type=Fixed + +#32x32 - Scalable - For folder icons >!!!ONLY!!!< - Scalable to the following sizes: 64x64 (default), 32x32, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[places/32] +Size=32 +Context=Places +Type=Scalable +MinSize=24 +MaxSize=48 + +#64x64 - Scalable - For folder icons >!!!ONLY!!!< - Scalable to the following sizes: 64x64 (default), 32x32, 128x128, 256x256 - DO_NOT_USE_ANYWHERE_ELSE - Color +[places/64] +Size=64 +Context=Places +Type=Scalable +MinSize=32 +MaxSize=256 + +########## Status +########## ordered by size + +#16x16 - Fixed size - For IM status icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[status/16] +Size=16 +Context=Status +Type=Fixed + +#16x16@2x - Fixed size - For IM status icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[status/16@2x] +Size=16 +Scale=2 +Context=Status +Type=Fixed + +#22x22 - Fixed size - Icon(s) for Plasma theme/System Tray. Not particularly used on Plasma. - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[status/22] +Size=22 +Context=Status +Type=Fixed + +#22x22@2x - Fixed size - Icon(s) for Plasma theme/System Tray. Not particularly used on Plasma. - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[status/22@2x] +Size=22 +Scale=2 +Context=Status +Type=Fixed + +#24x24 - Fixed size - for GTK apps. - WRONG_ICON_USAGE_BY_APP - Monochrome +[status/24] +Size=24 +Context=Status +Type=Fixed + +#32x32 - Fixed size - Icon(s) for Plasma theme/System Tray. Not particularly used on Plasma. - DO_NOT_USE_ANYWHERE_ELSE - Monochrome +[status/32] +Size=32 +Context=Status +Type=Fixed + +#64x64 - Fixed size - For dialog icons >!!!ONLY!!!< - DO_NOT_USE_ANYWHERE_ELSE - Color +[status/64] +Size=64 +Context=Status +Type=Scalable +MinSize=22 +MaxSize=256 + +# Gnome symbolic icons + +[actions/symbolic] +Context=Actions +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[devices/symbolic] +Context=Devices +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[emblems/symbolic] +Context=Emblems +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[places/symbolic] +Context=Places +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[status/symbolic] +Context=Status +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +########## EOF diff --git a/icons/breeze/status/symbolic/audio-volume-high.svg b/icons/breeze/status/symbolic/audio-volume-high.svg new file mode 100644 index 0000000..3ec9ff2 --- /dev/null +++ b/icons/breeze/status/symbolic/audio-volume-high.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/icons/breeze/status/symbolic/audio-volume-low.svg b/icons/breeze/status/symbolic/audio-volume-low.svg new file mode 100644 index 0000000..57cf679 --- /dev/null +++ b/icons/breeze/status/symbolic/audio-volume-low.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/breeze/status/symbolic/audio-volume-medium.svg b/icons/breeze/status/symbolic/audio-volume-medium.svg new file mode 100644 index 0000000..17e0295 --- /dev/null +++ b/icons/breeze/status/symbolic/audio-volume-medium.svg @@ -0,0 +1,17 @@ + + + + + + + diff --git a/icons/breeze/status/symbolic/audio-volume-muted.svg b/icons/breeze/status/symbolic/audio-volume-muted.svg new file mode 100644 index 0000000..f74a4a9 --- /dev/null +++ b/icons/breeze/status/symbolic/audio-volume-muted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/io.h b/io.h new file mode 100644 index 0000000..0d9b238 --- /dev/null +++ b/io.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Unlicense + +#ifndef IO_H +#define IO_H + +#include +#include +#include +#include +#include + +class IO : public QObject { + Q_OBJECT +public slots: + void write(const QUrl &url, const QString &data) { + QFile file{urlToPath(url)}; + if (file.open(QIODevice::WriteOnly | QIODevice::Text)) + file.write(data.toUtf8()); + else + qWarning() << "error opening file" << url; + } + + QString read(const QUrl &url) { + QFile file{urlToPath(url)}; + if (file.open(QIODevice::ReadOnly)) + return file.readAll(); + qWarning() << "error opening file" << url; + return {}; + } + +private: + static const QString urlToPath(const QUrl &path) { + return path.scheme() == "qrc" ? (":" + path.path()) : path.toLocalFile(); + } +}; + +#endif diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..a388f27 --- /dev/null +++ b/main.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Unlicense + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +try { + if (QIcon::themeName().isEmpty()) { + QIcon::setThemeSearchPaths(QIcon::themeSearchPaths() << ":/icons");; + QIcon::setThemeName("breeze"); + } + + QApplication app{argc, argv}; + app.setOrganizationName("fuzbal"); + app.setApplicationName("fuzbal"); + app.setApplicationVersion(GIT_VERSION); + + QTranslator translator; + translator.load(QLocale(), "fuzbal", "_", ":/i18n"); + app.installTranslator(&translator); + + QCommandLineParser parser; + parser.setApplicationDescription("Friendly Usable Zero-Bullshit Annotator & Labeler"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.process(app); + + IO io; + + QQmlApplicationEngine engine; + engine.rootContext()->setContextProperty("io", &io); + engine.load(QUrl{"qrc:/main.qml"}); + + return app.exec(); +} catch (std::exception &e) { + qCritical() << "critical error:" << e.what(); + return 1; +} catch (...) { + qCritical() << "critical error"; + return 1; +} diff --git a/main.qml b/main.qml new file mode 100644 index 0000000..7e4d720 --- /dev/null +++ b/main.qml @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Unlicense + +import QtQuick 2.12 +import QtQuick.Controls 2.13 +import QtQuick.Layouts 1.6 + +ApplicationWindow { + visible: true + width: 1024 + height: 768 + + SplitView { + anchors.fill: parent + hoverEnabled: true + + handle: Rectangle { + implicitWidth: sidebar.rightPadding + implicitHeight: implicitWidth + color: SplitHandle.pressed ? palette.dark : + (SplitHandle.hovered ? palette.mid : 'transparent') + } + + Video { + id: video + SplitView.fillWidth: true + SplitView.minimumWidth: parent.width/2 + } + + Sidebar { + id: sidebar + video: video + padding: 5 + leftPadding: 0 + focus: true + SplitView.fillHeight: true + SplitView.preferredWidth: 300 + SplitView.minimumWidth: 200 + } + } +} diff --git a/main.qrc b/main.qrc new file mode 100644 index 0000000..8a4a58d --- /dev/null +++ b/main.qrc @@ -0,0 +1,20 @@ + + + + main.qml + qtquickcontrols2.conf + tags.json + util.js + Event.qml + Events.qml + Fields/Bool.qml + Fields/Enum.qml + Fields/Text.qml + Fields/TextArea.qml + Filter.qml + Tags.qml + Sidebar.qml + Video.qml + Volume.qml + + diff --git a/qtquickcontrols2.conf b/qtquickcontrols2.conf new file mode 100644 index 0000000..9cd1411 --- /dev/null +++ b/qtquickcontrols2.conf @@ -0,0 +1,2 @@ +[Controls] +Style=Fusion diff --git a/tags.json b/tags.json new file mode 100644 index 0000000..978e44f --- /dev/null +++ b/tags.json @@ -0,0 +1,56 @@ +[ + { + "tag": "pass", "key": "p", "fields": [ + { "name": "player", "type": "Text", "key": "p" }, + { + "name": "type", "type": "Enum", + "values": [ + { "name": "long", "key": "l" }, + { "name": "short", "key": "s" } + ] + }, + { "name": "success", "type": "Bool", "key": "u", "value": true }, + { "name": "comment", "type": "TextArea", "key": "c" } + ] + }, + { + "tag": "shot", "key": "s", "fields": [ + { "name": "player", "type": "Text", "key": "p" }, + { + "name": "outcome", "type": "Enum", + "values": [ + { "name": "goal", "key": "g" }, + { "name": "block", "key": "b" }, + { "name": "deflect", "key": "d" }, + { "name": "out", "key": "o" } + ] + }, + { "name": "comment", "type": "TextArea", "key": "c" } + ] + }, + { + "tag": "foul", "key": "f", "fields": [ + { "name": "player", "type": "Text", "key": "p" }, + { "name": "opponent", "type": "Text", "key": "o" }, + { "name": "comment", "type": "TextArea", "key": "c" } + ] + }, + { + "tag": "offside", "key": "o", "fields": [ + { "name": "player", "type": "Text", "key": "p" }, + { "name": "comment", "type": "TextArea", "key": "c" } + ] + }, + { + "tag": "assist", "key": "a", "fields": [ + { "name": "player", "type": "Text", "key": "p" }, + { "name": "comment", "type": "TextArea", "key": "c" } + ] + }, + { + "tag": "offense", "key": "O", "fields": [] + }, + { + "tag": "defense", "key": "D", "fields": [] + } +] diff --git a/translations/fuzbal_sl.ts b/translations/fuzbal_sl.ts new file mode 100644 index 0000000..ba5ea39 --- /dev/null +++ b/translations/fuzbal_sl.ts @@ -0,0 +1,85 @@ + + + + + Filter + + + Tag + Oznaka + + + + Filters are not implemented yet! 😊 + Filtriranje še ne deluje! 😊 + + + + Sidebar + + + Open video + Odpri video + + + + Load tags + Naloži oznake + + + + JSON files (*.json) + Datoteke JSON (*.json) + + + + All files (*) + Vse datoteke (*) + + + + Description + Opis + + + + − + + + + + + + + + + + Events + Dogodki + + + + Tags + Oznake + + + + &Annotate + &Označi + + + + Alt+A + Alt+O + + + + &Filter + &Filtriraj + + + + Alt+F + + + + diff --git a/util.js b/util.js new file mode 100644 index 0000000..61f70e4 --- /dev/null +++ b/util.js @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Unlicense + +// If text contains key, make it stand out; otherwise, append [key] to text. +function addShortcut(text, key) { + if (!key) + return text + else if (text.indexOf(key) < 0) + return `${text} [${key}]` + else + return text.replace(new RegExp('\(' + key + '\)'), '$1') +} + +// Set alpha value for color. +function alphize(color, alpha) { + return Qt.hsla(color.hslHue, color.hslSaturation, color.hslLightness, alpha) +} + +// Return the last event in list with property not greater than value. +function find(list, property, value) { + var low = 0 + var high = list.count - 1 + while (low <= high) { + var mid = Math.floor((low + high) / 2) + if (list.get(mid)[property] <= value) + low = mid + 1 + else + high = mid - 1 + } + return low +}