lots of little bug fixes
This commit is contained in:
parent
de80d304bf
commit
ee5481f8db
17 changed files with 125 additions and 116 deletions
|
@ -43,12 +43,12 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Kirigami CoreAddons I18n)
|
|||
find_package(FFmpeg)
|
||||
set_package_properties(FFmpeg PROPERTIES TYPE REQUIRED)
|
||||
|
||||
# find_package(Libmpv)
|
||||
# set_package_properties(Libmpv PROPERTIES TYPE REQUIRED)
|
||||
find_package(Libmpv)
|
||||
set_package_properties(Libmpv PROPERTIES TYPE REQUIRED)
|
||||
|
||||
# find_package(MpvQt)
|
||||
# set_package_properties(MpvQt PROPERTIES TYPE REQUIRED
|
||||
# URL "https://invent.kde.org/libraries/mpvqt")
|
||||
find_package(MpvQt)
|
||||
set_package_properties(MpvQt PROPERTIES TYPE REQUIRED
|
||||
URL "https://invent.kde.org/libraries/mpvqt")
|
||||
|
||||
find_package(YouTubeDl)
|
||||
set_package_properties(YouTubeDl PROPERTIES TYPE RUNTIME)
|
||||
|
@ -137,7 +137,7 @@ target_link_libraries(liblumina INTERFACE
|
|||
KF6::I18n
|
||||
KF6::CoreAddons
|
||||
# KF6::FileMetaData
|
||||
# MpvQt::MpvQt
|
||||
MpvQt::MpvQt
|
||||
# mpv
|
||||
ssl
|
||||
crypto
|
||||
|
|
2
justfile
2
justfile
|
@ -1,7 +1,7 @@
|
|||
default:
|
||||
just --list
|
||||
build:
|
||||
cmake -DCMAKE_BUILD_TYPE=Debug -B bld/ .
|
||||
cmake -DQT_QML_GENERATE_QMLLS_INI=ON -DCMAKE_BUILD_TYPE=Debug -B bld/ .
|
||||
make -j8 --dir bld/
|
||||
rm -rf ~/.cache/lumina/lumina/qmlcache/
|
||||
run:
|
||||
|
|
|
@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.28)
|
|||
project(kirigami_rust)
|
||||
|
||||
find_package(ECM 6.0 REQUIRED NO_MODULE)
|
||||
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH})
|
||||
include(KDEInstallDirs)
|
||||
include(ECMUninstallTarget)
|
||||
|
||||
|
|
|
@ -14,8 +14,8 @@ target_sources(lumina
|
|||
# cpp/imagesqlmodel.cpp cpp/imagesqlmodel.h
|
||||
# cpp/filemanager.cpp cpp/filemanager.h
|
||||
# cpp/presentationsqlmodel.cpp cpp/presentationsqlmodel.h
|
||||
# cpp/mpv/mpvitem.h cpp/mpv/mpvitem.cpp
|
||||
# cpp/mpv/mpvproperties.h
|
||||
cpp/mpv/mpvitem.h cpp/mpv/mpvitem.cpp
|
||||
cpp/mpv/mpvproperties.h
|
||||
)
|
||||
|
||||
target_compile_options (lumina PUBLIC -fexceptions)
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
#include <QtQuickControls2/qquickstyle.h>
|
||||
#include <QtCore/qstringliteral.h>
|
||||
// #include <MpvAbstractItem>
|
||||
// #include "cpp/mpv/mpvitem.h"
|
||||
// #include "cpp/mpv/mpvproperties.h"
|
||||
#include "cpp/mpv/mpvitem.h"
|
||||
#include "cpp/mpv/mpvproperties.h"
|
||||
|
||||
// RUST
|
||||
#include <liblumina/src/rust/file_helper.cxxqt.h>
|
||||
|
@ -149,8 +149,8 @@ int main(int argc, char *argv[])
|
|||
// apparently mpv needs this class set
|
||||
// let's register mpv as well
|
||||
std::setlocale(LC_NUMERIC, "C");
|
||||
// qmlRegisterType<MpvItem>("mpv", 1, 0, "MpvItem");
|
||||
// qmlRegisterSingletonInstance("org.presenter", 1, 0, "MpvProperties", MpvProperties::self());
|
||||
qmlRegisterType<MpvItem>("org.presenter", 1, 0, "MpvItem");
|
||||
qmlRegisterSingletonInstance("org.presenter", 1, 0, "MpvProperties", MpvProperties::self());
|
||||
|
||||
qDebug() << "IDKFd";
|
||||
qDebug() << serviceItemModel.get()->rowCount(QModelIndex());
|
||||
|
|
|
@ -200,7 +200,7 @@ Kirigami.ApplicationWindow {
|
|||
Labs.FileDialog {
|
||||
id: saveFileDialog
|
||||
title: "Save"
|
||||
folder: shortcuts.home
|
||||
/* folder: shortcuts.home */
|
||||
/* fileMode: Labs.FileDialog.SaveFile */
|
||||
defaultSuffix: ".pres"
|
||||
/* selectExisting: false */
|
||||
|
@ -220,7 +220,7 @@ Kirigami.ApplicationWindow {
|
|||
Labs.FileDialog {
|
||||
id: loadFileDialog
|
||||
title: "Load"
|
||||
folder: shortcuts.home
|
||||
/* folder: shortcuts.home */
|
||||
/* fileMode: Labs.FileDialog.SaveFile */
|
||||
defaultSuffix: ".pres"
|
||||
/* selectExisting: true */
|
||||
|
@ -235,7 +235,7 @@ Kirigami.ApplicationWindow {
|
|||
Labs.FileDialog {
|
||||
id: soundFileDialog
|
||||
title: "Pick a Sound Effect"
|
||||
folder: shortcuts.home
|
||||
/* folder: shortcuts.home */
|
||||
/* fileMode: Labs.FileDialog.SaveFile */
|
||||
/* selectExisting: true */
|
||||
onAccepted: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Window 2.15
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Window
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
|
||||
Kirigami.ActionToolBar {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import Qt.labs.platform 1.1 as Labs
|
||||
import QtQuick.Pdf 5.15
|
||||
import QtQml.Models 2.15
|
||||
import QtWebEngine 1.10
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt.labs.platform as Labs
|
||||
import QtQuick.Pdf
|
||||
import QtQml.Models
|
||||
import QtWebEngine
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
import org.presenter 1.0
|
||||
import org.presenter
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
@ -34,7 +34,6 @@ Item {
|
|||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height - 280
|
||||
proxyModel: songModel
|
||||
innerModel: songModel
|
||||
libraryType: "song"
|
||||
headerLabel: "Songs"
|
||||
|
@ -64,7 +63,6 @@ Item {
|
|||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height - 280
|
||||
proxyModel: videoModel
|
||||
innerModel: videoModel
|
||||
libraryType: "video"
|
||||
headerLabel: "Videos"
|
||||
|
@ -90,7 +88,6 @@ Item {
|
|||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height - 280
|
||||
proxyModel: imageModel
|
||||
innerModel: imageModel
|
||||
libraryType: "image"
|
||||
headerLabel: "Images"
|
||||
|
@ -111,7 +108,6 @@ Item {
|
|||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height - 280
|
||||
proxyModel: presentationModel
|
||||
innerModel: presentationModel
|
||||
libraryType: "presentation"
|
||||
headerLabel: "Presentations"
|
||||
|
|
|
@ -10,7 +10,6 @@ import org.presenter 1.0
|
|||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property var proxyModel
|
||||
property var innerModel
|
||||
property string libraryType
|
||||
property string headerLabel
|
||||
|
@ -228,9 +227,10 @@ ColumnLayout {
|
|||
property bool selected: selectionModel.hasSelection &&
|
||||
selectionModel.isSelected(innerModel.index(index, 0))
|
||||
property bool fileValidation: {
|
||||
if (filePath)
|
||||
fileHelper.validate(filePath)
|
||||
else
|
||||
if (filePath) {
|
||||
Utils.dbg("FILEPATH IS: " + filePath);
|
||||
fileHelper.validate(filePath);
|
||||
} else
|
||||
false
|
||||
}
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@ Controls.Page {
|
|||
|
||||
Presenter.ServiceList {
|
||||
id: leftDock
|
||||
parentItem: splitMainView
|
||||
Controls.SplitView.preferredWidth: Kirigami.Units.largeSpacing * 25
|
||||
Controls.SplitView.maximumWidth: Kirigami.Units.largeSpacing * 50
|
||||
z: 1
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtWebEngine 1.10
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import QtWebEngine
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
|
||||
Item {
|
||||
|
@ -149,7 +149,7 @@ Item {
|
|||
url: isHtml ? presentation.filePath : ""
|
||||
visible: isHtml
|
||||
settings.playbackRequiresUserGesture: false
|
||||
backgroundColor: Kirigami.Theme.backgroundColor
|
||||
backgroundColor: "transparent"
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true;
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Controls 2.12 as Controls
|
||||
import QtQuick.Controls as Controls
|
||||
/* import QtQuick.Window 2.15 */
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Shapes 1.15
|
||||
import QtQml.Models 2.15
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import QtQml.Models
|
||||
/* import QtQml.Models 2.12 */
|
||||
/* import QtMultimedia 5.15 */
|
||||
/* import QtAudioEngine 1.15 */
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
import org.presenter 1.0
|
||||
|
||||
|
@ -17,22 +17,32 @@ Item {
|
|||
id: root
|
||||
property var selectedItem: serviceItemList.selected
|
||||
property var hlItem
|
||||
property var parentItem
|
||||
|
||||
Rectangle {
|
||||
id: bg
|
||||
color: Kirigami.Theme.backgroundColor
|
||||
anchors.fill: parent
|
||||
opacity: 0.90
|
||||
opacity: 1.0
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
id: backgroundBlur
|
||||
source: bg
|
||||
anchors.fill: bg
|
||||
blur: 1.0
|
||||
blurMultiplier: 20
|
||||
blurEnabled: true
|
||||
}
|
||||
/* ShaderEffectSource { */
|
||||
/* id: shaderArea */
|
||||
/* sourceItem: parentItem */
|
||||
/* sourceRect: Qt.rect(0, 0, parent.width, parent.height) */
|
||||
/* width: parent.width */
|
||||
/* height: parent.height */
|
||||
/* } */
|
||||
|
||||
/* MultiEffect { */
|
||||
/* id: backgroundBlur */
|
||||
/* source: shaderArea */
|
||||
/* anchors.fill: parent */
|
||||
/* autoPaddingEnabled: true */
|
||||
/* blur: 0.1 */
|
||||
/* /\* blurMultiplier: 2 *\/ */
|
||||
/* blurEnabled: true */
|
||||
/* } */
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
@ -78,15 +88,16 @@ Item {
|
|||
}
|
||||
|
||||
Component {
|
||||
id: serviceListItem
|
||||
Item {
|
||||
id: serviceListDelegate
|
||||
Controls.ItemDelegate {
|
||||
id: serviceListItem
|
||||
implicitWidth: serviceItemList.width
|
||||
height: Kirigami.Units.gridUnit * 2
|
||||
Component.onCompleted: Utils.dbg("HELLLLLOOOOOO: " + ServiceItemModel.getItem(0).name)
|
||||
|
||||
property var selectedItems
|
||||
|
||||
DropArea {
|
||||
contentItem: DropArea {
|
||||
id: serviceDrop
|
||||
anchors.fill: parent
|
||||
|
||||
|
@ -272,9 +283,11 @@ Item {
|
|||
/* width: 20 */
|
||||
listItem: serviceListItem
|
||||
listView: serviceItemList
|
||||
onMoveRequested: ServiceItemModel.moveRows(oldIndex,
|
||||
newIndex,
|
||||
1)
|
||||
onMoveRequested: (oldIndex, newIndex) => {
|
||||
ServiceItemModel.moveRows(oldIndex,
|
||||
newIndex,
|
||||
1)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -345,7 +358,7 @@ Item {
|
|||
|
||||
model: ServiceItemModel
|
||||
|
||||
delegate: serviceListItem
|
||||
delegate: serviceListDelegate
|
||||
|
||||
Kirigami.WheelHandler {
|
||||
id: wheelHandler
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import QtQuick 2.13
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
import org.presenter 1.0
|
||||
import Qt.labs.settings 1.0
|
||||
import org.presenter
|
||||
import Qt.labs.settings
|
||||
|
||||
Kirigami.OverlaySheet {
|
||||
id: root
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import QtMultimedia
|
||||
/* import QtAudioEngine 1.15 */
|
||||
import QtWebEngine 1.10
|
||||
import QtWebEngine
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import org.kde.kirigami as Kirigami
|
||||
import "./" as Presenter
|
||||
import org.presenter 1.0
|
||||
import org.presenter
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
@ -35,10 +34,10 @@ Item {
|
|||
property var vTextAlignment: Text.AlignVCenter
|
||||
|
||||
//these properties are for giving video info to parents
|
||||
property int videoPosition: video.position
|
||||
property int videoDuration: video.duration
|
||||
property real videoPosition: video.position
|
||||
property real videoDuration: video.duration
|
||||
property var videoLoop: video.loops
|
||||
property bool videoIsPlaying: video.playbackState
|
||||
property bool videoIsPlaying: !video.pause
|
||||
|
||||
// These propees help to determine the state of the slide
|
||||
property string itemType
|
||||
|
@ -69,12 +68,12 @@ Item {
|
|||
visible: false
|
||||
}
|
||||
|
||||
Video {
|
||||
MpvItem {
|
||||
id: video
|
||||
anchors.fill: parent
|
||||
muted: preview
|
||||
/* Component.onCompleted: mpvLoadingTimer.start() */
|
||||
loops: itemType == "song" ? MediaPlayer.Infinite : vidLoop ? MediaPlayer.Infinite : 1
|
||||
loop: itemType == "song" ? true : vidLoop
|
||||
source: videoSource
|
||||
|
||||
MouseArea {
|
||||
|
@ -89,7 +88,7 @@ Item {
|
|||
Timer {
|
||||
id: pauseTimer
|
||||
interval: 300
|
||||
onTriggered: mpv.pause()
|
||||
onTriggered: video.pause()
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
@ -150,7 +149,7 @@ Item {
|
|||
visible: htmlVisible
|
||||
enabled: htmlVisible
|
||||
zoomFactor: preview ? 0.25 : 1.0
|
||||
backgroundColor: Kirigami.Theme.backgroundColor
|
||||
backgroundColor: "transparent"
|
||||
onLoadingChanged: {
|
||||
if (loadRequest.status == 2)
|
||||
showPassiveNotification("yahoo?");
|
||||
|
@ -185,12 +184,12 @@ Item {
|
|||
}
|
||||
|
||||
function loopVideo() {
|
||||
if (mpv.getProperty("loop") === "inf") {
|
||||
if (video.loop) {
|
||||
showPassiveNotification("already looping");
|
||||
mpv.setProperty("loop", "no");
|
||||
video.loop = false;
|
||||
}
|
||||
else {
|
||||
mpv.setProperty("loop", "inf");
|
||||
video.loop = true;
|
||||
showPassiveNotification("looping video");
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +219,7 @@ Item {
|
|||
}
|
||||
|
||||
function quitMpv() {
|
||||
mpv.quit();
|
||||
video.quit();
|
||||
}
|
||||
|
||||
function pauseVideo() {
|
||||
|
|
|
@ -35,7 +35,7 @@ Item {
|
|||
padding: 10
|
||||
onEditingFinished: updateTitle(text);
|
||||
background: Presenter.TextBackground {
|
||||
control: fontBox
|
||||
control: slideTitleField
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15 as Controls
|
||||
import QtQuick.Layouts 1.15
|
||||
import org.kde.kirigami 2.13 as Kirigami
|
||||
import QtQuick
|
||||
import QtQuick.Controls as Controls
|
||||
import QtQuick.Layouts
|
||||
import org.kde.kirigami as Kirigami
|
||||
import QtMultimedia
|
||||
import "./" as Presenter
|
||||
import org.presenter 1.0
|
||||
|
||||
import org.presenter
|
||||
Item {
|
||||
id: root
|
||||
|
||||
|
@ -97,12 +96,12 @@ Item {
|
|||
// and then save them for having a thumbnail here
|
||||
}
|
||||
|
||||
Video {
|
||||
MpvItem {
|
||||
id: videoPreview
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
source: root.video.filePath.toString()
|
||||
loops: video.loop ? MediaPlayer.Infinite : 1
|
||||
loop: video.loop
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
|
@ -111,13 +110,13 @@ Item {
|
|||
height: videoTitleField.height
|
||||
spacing: 2
|
||||
Kirigami.Icon {
|
||||
source: videoPreview.playbackState == MediaPlayer.PlayingState ? "media-pause" : "media-play"
|
||||
source: videoPreview.pause ? "media-play" : "media-pause"
|
||||
Layout.preferredWidth: 25
|
||||
Layout.preferredHeight: 25
|
||||
color: Kirigami.Theme.textColor
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: videoPreview.playbackState == MediaPlayer.PlayingState ? videoPreview.pause() : videoPreview.play()
|
||||
onPressed: videoPreview.playPause()
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
|
@ -129,21 +128,22 @@ Item {
|
|||
to: videoPreview.duration
|
||||
value: videoPreview.postion
|
||||
live: true
|
||||
onMoved: videoPreview.seek(value);
|
||||
onMoved: videoPreview.position = value;
|
||||
}
|
||||
|
||||
Controls.Label {
|
||||
id: videoTime
|
||||
text: {
|
||||
let mil = Math.floor(videoPreview.position);
|
||||
let sec = Math.floor((mil / 1000) % 60);
|
||||
let min = Math.floor((mil / (1000 * 60)) % 60);
|
||||
let hour = Math.floor((mil / (1000 * 60 * 60)) % 24);
|
||||
sec = (sec < 10) ? "0" + sec : sec;
|
||||
min = (min < 10) ? "0" + min : min;
|
||||
hour = (hour < 10) ? "0" + hour : hour;
|
||||
return hour + ":" + min + ":" + sec
|
||||
}
|
||||
/* text: { */
|
||||
/* let mil = Math.floor(videoPreview.position); */
|
||||
/* let sec = Math.floor((mil / 1000) % 60); */
|
||||
/* let min = Math.floor((mil / (1000 * 60)) % 60); */
|
||||
/* let hour = Math.floor((mil / (1000 * 60 * 60)) % 24); */
|
||||
/* sec = (sec < 10) ? "0" + sec : sec; */
|
||||
/* min = (min < 10) ? "0" + min : min; */
|
||||
/* hour = (hour < 10) ? "0" + hour : hour; */
|
||||
/* return hour + ":" + min + ":" + sec */
|
||||
/* } */
|
||||
text: videoPreview.formattedPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -267,7 +267,7 @@ Item {
|
|||
id: mpvLoadingTimer
|
||||
interval: 500
|
||||
onTriggered: {
|
||||
videoPreview.pause();
|
||||
videoPreview.pause = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -284,7 +284,7 @@ Item {
|
|||
|
||||
function stop() {
|
||||
console.log("stopping video");
|
||||
videoPreview.pause();
|
||||
videoPreview.pause = true;
|
||||
console.log("quit mpv");
|
||||
}
|
||||
|
||||
|
|
|
@ -717,6 +717,6 @@ impl presentation_model::PresentationModel {
|
|||
|
||||
pub fn row_count(&self, _parent: &QModelIndex) -> i32 {
|
||||
// println!("row count is {cnt}");
|
||||
self.presentations.len() as i32
|
||||
self.rust().presentations.len() as i32
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue