Inertial Chicken
This commit is contained in:
parent
f14d65d301
commit
586b39e1ef
20 changed files with 1417 additions and 38 deletions
193
LICENSE
193
LICENSE
|
|
@ -1,46 +1,165 @@
|
|||
GPL Cooperation Commitment
|
||||
Version 1.0
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Before filing or continuing to prosecute any legal proceeding or claim
|
||||
(other than a Defensive Action) arising from termination of a Covered
|
||||
License, we commit to extend to the person or entity ('you') accused
|
||||
of violating the Covered License the following provisions regarding
|
||||
cure and reinstatement, taken from GPL version 3. As used here, the
|
||||
term 'this License' refers to the specific Covered License being
|
||||
enforced.
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly
|
||||
and finally terminates your license, and (b) permanently, if the
|
||||
copyright holder fails to notify you of the violation by some
|
||||
reasonable means prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you
|
||||
have received notice of violation of this License (for any work)
|
||||
from that copyright holder, and you cure the violation prior to 30
|
||||
days after your receipt of the notice.
|
||||
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.
|
||||
|
||||
We intend this Commitment to be irrevocable, and binding and
|
||||
enforceable against us and assignees of or successors to our
|
||||
copyrights.
|
||||
0. Additional Definitions.
|
||||
|
||||
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.
|
||||
|
||||
'Covered License' means the GNU General Public License, version 2
|
||||
(GPLv2), the GNU Lesser General Public License, version 2.1
|
||||
(LGPLv2.1), or the GNU Library General Public License, version 2
|
||||
(LGPLv2), all as published by the Free Software Foundation.
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
'Defensive Action' means a legal proceeding or claim that We bring
|
||||
against you in response to a prior proceeding or claim initiated by
|
||||
you or your affiliate.
|
||||
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.
|
||||
|
||||
'We' means each contributor to this repository as of the date of
|
||||
inclusion of this file, including subsidiaries of a corporate
|
||||
contributor.
|
||||
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".
|
||||
|
||||
This work is available under a Creative Commons Attribution-ShareAlike
|
||||
4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/).
|
||||
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.
|
||||
|
|
|
|||
66
README.md
66
README.md
|
|
@ -1,3 +1,67 @@
|
|||
# miva
|
||||
|
||||
Mobile image and video browser/viewer
|
||||
Mobile image and video browser/viewer
|
||||
|
||||
## Building
|
||||
|
||||
You will need Qt 6.8+ in a dev flavour. In particular (for PostmarketOS):
|
||||
|
||||
- qt6-qtbase-dev
|
||||
- qt6-qtdeclarative-dev
|
||||
|
||||
You'll also need some KF6 stuff:
|
||||
|
||||
- kio-dev
|
||||
|
||||
Then build/install it:
|
||||
|
||||
```
|
||||
doneill@lyria ~/c/miva> mkdir build
|
||||
doneill@lyria ~/c/miva> cd build
|
||||
doneill@lyria ~/c/m/build> qmake6 ..
|
||||
Info: creating stash file /home/doneill/code/miva/build/.qmake.stash
|
||||
doneill@lyria ~/c/m/build> make
|
||||
g++ -c -pipe -O2 -std=gnu++1z -Wall -Wextra -D_REENTRANT -DQT_NO_DEBUG -DQT_QUICK_LIB -DQT_OPENGL_LIB -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_QML_LIB -DQT_QMLINTEGRATION_LIB -DQT_NETWORK_LIB -DQT_CORE_LIB -I../../miva -I. -I/usr/include/KF6/KIOGui -I/usr/include/KF6/KIO -I/usr/include/KF6/KIOCore -I/usr/include/KF6/KCoreAddons -I/usr/include/qt6 -I/usr/include/qt6/QtQuick -I/usr/include/qt6/QtOpenGL -I/usr/include/qt6/QtWidgets -I/usr/include/qt6/QtGui -I/usr/include/qt6/QtQml -I/usr/include/qt6/QtQmlIntegration -I/usr/include/qt6/QtNetwork -I/usr/include/qt6/QtCore -I. -I/usr/lib/qt6/mkspecs/linux-g++ -o main.o ../src/main.cpp
|
||||
...
|
||||
g++ -Wl,-O1 -Wl,-rpath,/usr/lib -Wl,-rpath-link,/usr/lib -o miva main.o mutils.o thumbnailprovider.o qrc_qml.o moc_mutils.o -lKF6KIOCore -lKF6KIOGui -lKF6CoreAddons /usr/lib/libQt6Quick.so /usr/lib/libQt6OpenGL.so /usr/lib/libQt6Widgets.so /usr/lib/libQt6Gui.so /usr/lib/libGLESv2.so /usr/lib/libEGL.so /usr/lib/libQt6Qml.so /usr/lib/libQt6Network.so /usr/lib/libQt6Core.so -lpthread -lGLESv2 -lEGL
|
||||
doneill@lyria ~/c/m/build> sudo make install
|
||||
[sudo: authenticate] Password:
|
||||
/usr/bin/qmake6 -install qinstall -exe miva /usr/local/bin/miva
|
||||
strip /usr/local/bin/miva
|
||||
/usr/bin/qmake6 -install qinstall /home/doneill/code/miva/dist/miva.desktop /usr/local/share/applications/miva.desktop
|
||||
/usr/bin/qmake6 -install qinstall /home/doneill/code/miva/dist/miva.png /usr/local/share/icons/hicolor/512x512/apps/miva.png
|
||||
doneill@lyria ~/c/m/build> miva
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, it should show up in the Plasma Mobile launcher with the opossum icon. (I like opossums.)
|
||||
|
||||
You can navigate the filesystem from the gallery interface, which is fairly straightforward. KIO is used to generate (or use existing) thumbnails for supported image and video formats.
|
||||
|
||||
Tapping on a directory (folder icons) will move into them. Tapping on an image or video will display that image or video.
|
||||
|
||||
While viewing an image, tapping it will swap between scaled to fit and unscaled.
|
||||
|
||||
While viewing a video, tapping it once will pause or unpause playback. Double-tapping will toggle fullscreen.
|
||||
|
||||
A button in the top-left will get you back to the browser.
|
||||
|
||||
Really, this is a super dead-simple image viewer.
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
GIF files are treated as "videos" to facilitate smooth pausing, looping, and seeking.
|
||||
|
||||
## Credits
|
||||
|
||||
The following graphics are from KDE Breeze:
|
||||
|
||||
- document-open-folder.svg
|
||||
- go-parent-folder.svg
|
||||
- insert-image.svg
|
||||
- question.svg
|
||||
- video-symbolic.svg
|
||||
|
||||
They have been unmodified.
|
||||
|
|
|
|||
18
dist/miva.desktop
vendored
Normal file
18
dist/miva.desktop
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
|
||||
Name=Miva
|
||||
Comment=View images and videos
|
||||
GenericName=Image and Video Browser and Viewer
|
||||
|
||||
Exec=miva
|
||||
TryExec=miva
|
||||
|
||||
Icon=miva
|
||||
Terminal=false
|
||||
Categories=Video;Graphics;Viewer;Player;Qt;KDE;
|
||||
MimeType=image/jpeg;image/gif;image/webp;video/webm;video/mp4;video/x-matroska;
|
||||
|
||||
StartupNotify=true
|
||||
|
||||
BIN
dist/miva.kra
vendored
Normal file
BIN
dist/miva.kra
vendored
Normal file
Binary file not shown.
BIN
dist/miva.png
vendored
Normal file
BIN
dist/miva.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
12
images/document-open-folder.svg
Normal file
12
images/document-open-folder.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="m3 3v1 15h1 15v-1-13h-6.992188l-2-2-.007812.007813v-.007813h-6-1m6.00781 5h8.992188v10h-14v-8h3v-.007813l.007813.007813 2-2" class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 508 B |
12
images/go-parent-folder.svg
Normal file
12
images/go-parent-folder.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 L 3 4 L 3 19 L 4 19 L 12 19 L 12 18 L 4 18 L 4 10 L 7 10 L 7 9.9921875 L 7.0078125 10 L 9.0078125 8 L 18 8 L 18 12 L 19 12 L 19 5 L 12.007812 5 L 10.007812 3 L 10 3.0078125 L 10 3 L 4 3 L 3 3 z M 15.5 11.792969 L 14.792969 12.5 L 12 15.292969 L 12.707031 16 L 15 13.707031 L 15 19 L 16 19 L 16 18 L 16 17 L 16 13.707031 L 18.292969 16 L 19 15.292969 L 16.207031 12.5 L 15.5 11.792969 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 777 B |
12
images/insert-image.svg
Normal file
12
images/insert-image.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 L 3 4 L 3 19 L 4 19 L 15 19 L 12 16 L 15 13 L 17 13 L 16 12 L 14 10 L 10 14 L 9 13 L 4.1484375 17.851562 L 4 18 L 4 4 L 18 4 L 18 12 L 19 12 L 19 4 L 19 3 L 4 3 L 3 3 z M 7 5 C 5.892 5 5 5.892 5 7 C 5 8.108 5.892 9 7 9 C 8.108 9 9 8.108 9 7 C 9 5.892 8.108 5 7 5 z M 16 14 L 16 16 L 14 16 L 14 17 L 16 17 L 16 19 L 17 19 L 17 17 L 19 17 L 19 16 L 17 16 L 17 14 L 16 14 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 762 B |
12
images/question.svg
Normal file
12
images/question.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 10.96875 3 C 9.21782 3 7.56187 3.4216019 6 4.2636719 L 6.7773438 6.0507812 C 7.5582637 5.6672613 8.2600456 5.3933025 8.8847656 5.2265625 C 9.5177356 5.0598325 10.162692 4.9765625 10.820312 4.9765625 C 11.806753 4.9765625 12.563734 5.1968119 13.089844 5.6386719 C 13.615944 6.0805419 13.878906 6.7136725 13.878906 7.5390625 C 13.878906 7.9809325 13.822111 8.369625 13.707031 8.703125 C 13.591951 9.036605 13.394724 9.369635 13.115234 9.703125 C 12.835754 10.036615 12.247563 10.586476 11.351562 11.353516 C 10.251483 12.356846 9.10046 14.139 9 16 L 11 16 L 10.994141 15.96875 C 10.994141 15.21007 11.125612 14.601138 11.388672 14.142578 C 11.659952 13.675688 12.201805 13.087576 13.015625 12.378906 C 14.010295 11.536836 14.673153 10.903726 15.001953 10.478516 C 15.338993 10.053316 15.589506 9.6041363 15.753906 9.1289062 C 15.918406 8.6536762 16 8.1071944 16 7.4902344 C 16 6.0728944 15.55227 4.97119 14.65625 4.1875 C 13.76024 3.39546 12.53061 3 10.96875 3 z M 9 17 L 9 19 L 11 19 L 11 17 L 9 17 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
12
images/video-symbolic.svg
Normal file
12
images/video-symbolic.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<defs id="defs3051">
|
||||
<style type="text/css" id="current-color-scheme">
|
||||
.ColorScheme-Text {
|
||||
color:#232629;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g transform="translate(1,1)">
|
||||
<path style="fill:currentColor;fill-opacity:1;stroke:none" d="M 3 3 L 3 19 L 19 19 L 19 3 L 3 3 z M 6 4 L 16 4 L 16 9.9003906 L 6 9.9003906 L 6 4 z M 4 5 L 5 5 L 5 6 L 4 6 L 4 5 z M 17 5 L 18 5 L 18 6 L 17 6 L 17 5 z M 4 7 L 5 7 L 5 8 L 4 8 L 4 7 z M 17 7 L 18 7 L 18 8 L 17 8 L 17 7 z M 4 9 L 5 9 L 5 10 L 4 10 L 4 9 z M 17 9.0996094 L 18 9.0996094 L 18 10.099609 L 17 10.099609 L 17 9.0996094 z M 4 12 L 5 12 L 5 13 L 4 13 L 4 12 z M 6 12 L 16 12 L 16 18 L 6 18 L 6 12 z M 17 12 L 18 12 L 18 13 L 17 13 L 17 12 z M 4 14 L 5 14 L 5 15 L 4 15 L 4 14 z M 17 14 L 18 14 L 18 15 L 17 15 L 17 14 z M 4 16 L 5 16 L 5 17 L 4 17 L 4 16 z M 17 16 L 18 16 L 18 17 L 17 17 L 17 16 z " class="ColorScheme-Text"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 996 B |
53
miva.pro
Normal file
53
miva.pro
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
TEMPLATE = app
|
||||
|
||||
TARGET = miva
|
||||
|
||||
QT += quick widgets
|
||||
|
||||
CONFIG += c++17
|
||||
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/mutils.cpp \
|
||||
src/thumbnailprovider.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/mutils.h \
|
||||
src/thumbnailprovider.h
|
||||
|
||||
RESOURCES += \
|
||||
qml.qrc
|
||||
|
||||
LIBS += -lKF6KIOCore -lKF6KIOGui -lKF6CoreAddons
|
||||
INCLUDEPATH += /usr/include/KF6/KIOGui /usr/include/KF6/KIO /usr/include/KF6/KIOCore /usr/include/KF6/KCoreAddons
|
||||
|
||||
|
||||
isEmpty(PREFIX) {
|
||||
PREFIX = /usr/local
|
||||
}
|
||||
isEmpty(BINDIR) {
|
||||
BINDIR = $$PREFIX/bin
|
||||
}
|
||||
isEmpty(DATADIR) {
|
||||
DATADIR = $$PREFIX/share
|
||||
}
|
||||
isEmpty(APPDIR) {
|
||||
APPDIR = $$DATADIR/applications
|
||||
}
|
||||
isEmpty(ICONDIR) {
|
||||
ICONDIR = $$DATADIR/icons/hicolor/512x512/apps
|
||||
}
|
||||
|
||||
# ---- binary ----
|
||||
target.path = $$BINDIR
|
||||
INSTALLS += target
|
||||
|
||||
# ---- desktop file ----
|
||||
desktop.files = dist/miva.desktop
|
||||
desktop.path = $$APPDIR
|
||||
INSTALLS += desktop
|
||||
|
||||
# ---- icon ----
|
||||
icon.files = dist/miva.png
|
||||
icon.path = $$ICONDIR
|
||||
INSTALLS += icon
|
||||
13
qml.qrc
Normal file
13
qml.qrc
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>qml/Main.qml</file>
|
||||
<file>qml/Viewer.qml</file>
|
||||
<file>qml/Navigation.qml</file>
|
||||
|
||||
<file>images/document-open-folder.svg</file>
|
||||
<file>images/insert-image.svg</file>
|
||||
<file>images/video-symbolic.svg</file>
|
||||
<file>images/question.svg</file>
|
||||
<file>images/go-parent-folder.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
169
qml/Main.qml
Normal file
169
qml/Main.qml
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
|
||||
import miva
|
||||
|
||||
Window {
|
||||
id: main
|
||||
|
||||
title: "Miva"
|
||||
width: 640
|
||||
height: 480
|
||||
visible: true
|
||||
|
||||
Material.theme: Material.Dark
|
||||
readonly property bool isFullscreen: main.visibility === Window.FullScreen
|
||||
|
||||
readonly property var supportedTypes: [
|
||||
{ "suffix":"png", "type":"image" },
|
||||
{ "suffix":"jpeg", "type":"image" },
|
||||
{ "suffix":"jpg", "type":"image" },
|
||||
{ "suffix":"webp", "type":"image" },
|
||||
{ "suffix":"gif", "type":"video" },
|
||||
{ "suffix":"webm", "type":"video" },
|
||||
{ "suffix":"mp4", "type":"video" },
|
||||
{ "suffix":"mkv", "type":"video" },
|
||||
]
|
||||
|
||||
MUtils {
|
||||
id: mutils
|
||||
}
|
||||
|
||||
SystemPalette {
|
||||
id: systemPalette
|
||||
}
|
||||
|
||||
function getType(suffix) {
|
||||
for( let a of supportedTypes )
|
||||
{
|
||||
if( a["suffix"] === suffix )
|
||||
return a["type"];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Material.background
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fsmodel
|
||||
property var model
|
||||
property string currentDirectory: (""+StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]).substring(7)
|
||||
property var pathArray: []
|
||||
|
||||
function rebuildPath() {
|
||||
let result = [];
|
||||
const pieces = currentDirectory.split("/");
|
||||
for( let a=0; a < pieces.length; a++ ) {
|
||||
if( 0 === a )
|
||||
result.push("/");
|
||||
else
|
||||
result.push(pieces[a]);
|
||||
}
|
||||
pathArray = result;
|
||||
}
|
||||
|
||||
function populateModel() {
|
||||
let m = [];
|
||||
let filters = MUtils.NoDot | MUtils.Dirs | MUtils.Files;
|
||||
if( currentDirectory === "/" )
|
||||
filters = MUtils.NoDotAndDotDot | MUtils.Dirs | MUtils.Files;
|
||||
const e = mutils.dirListing(currentDirectory, filters, MUtils.Name | MUtils.DirsFirst);
|
||||
for( let ent of e ) {
|
||||
let name = ent["fileName"];
|
||||
let fent = ent["absoluteFilePath"];
|
||||
if( ent["isDir"] )
|
||||
m.push( { 'name':name, 'type':'dir', 'full':fent } );
|
||||
else {
|
||||
const type = main.getType(ent["suffix"]);
|
||||
if( type )
|
||||
m.push( { 'name':name, 'type':type, 'full':fent } );
|
||||
}
|
||||
}
|
||||
//console.log(JSON.stringify(m, null, 4));
|
||||
model = m;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
rebuildPath();
|
||||
populateModel();
|
||||
}
|
||||
|
||||
Component.onCompleted: refresh();
|
||||
onCurrentDirectoryChanged: refresh();
|
||||
}
|
||||
|
||||
Item {
|
||||
id: imagesmodel
|
||||
|
||||
property var model
|
||||
|
||||
readonly property string currentDirectory: fsmodel.currentDirectory
|
||||
|
||||
function populateModel() {
|
||||
let m = [];
|
||||
const e = mutils.dirListing(currentDirectory, MUtils.NoDotAndDotDot | MUtils.Files, MUtils.Name);
|
||||
for( let ent of e ) {
|
||||
let name = ent["fileName"];
|
||||
let fent = ent["absoluteFilePath"];
|
||||
|
||||
const type = main.getType(ent["suffix"]);
|
||||
if( type )
|
||||
m.push( { 'name':name, 'type':type, 'full':fent } );
|
||||
}
|
||||
//console.log( JSON.stringify(m, null, 4) );
|
||||
model = m;
|
||||
}
|
||||
|
||||
//Component.onCompleted: populateModel();
|
||||
onCurrentDirectoryChanged: populateModel();
|
||||
}
|
||||
|
||||
Navigation {
|
||||
anchors.fill: parent
|
||||
model: fsmodel.model
|
||||
navModel: fsmodel.pathArray
|
||||
onNavigate: function(path) {
|
||||
fsmodel.currentDirectory = path;
|
||||
}
|
||||
onViewEntry: function(filename) {
|
||||
for( let a=0; a < imagesmodel.model.length; a++ ) {
|
||||
if( filename !== imagesmodel.model[a]["name"] )
|
||||
continue;
|
||||
|
||||
viewer.warpTo(a);
|
||||
viewer.visible = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Viewer {
|
||||
id: viewer
|
||||
focus: true
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
model: imagesmodel.model
|
||||
|
||||
Keys.onLeftPressed: viewer.previousImage();
|
||||
Keys.onRightPressed: viewer.nextImage();
|
||||
|
||||
onToggleFullscreen: {
|
||||
main.visibility = main.isFullscreen ? Window.Windowed : Window.FullScreen;
|
||||
}
|
||||
onHideViewer: {
|
||||
viewer.visible = false;
|
||||
if( main.isFullscreen )
|
||||
main.visibility = Window.Windowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
155
qml/Navigation.qml
Normal file
155
qml/Navigation.qml
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
|
||||
import miva
|
||||
|
||||
Item {
|
||||
id: navigation
|
||||
|
||||
property alias model: flowItems.model
|
||||
property alias navModel: listviewNav.model
|
||||
|
||||
signal navigate(string path)
|
||||
signal viewEntry(string filename)
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
ListView {
|
||||
id: listviewNav
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 32
|
||||
orientation: ListView.Horizontal
|
||||
delegate: TabButton {
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
text: modelData
|
||||
|
||||
onReleased: {
|
||||
if( 0 === index ) {
|
||||
console.log("/");
|
||||
navigation.navigate("/");
|
||||
return;
|
||||
}
|
||||
|
||||
let newPath = "";
|
||||
for( let a=1; a < listviewNav.model.length && a <= index; a++ )
|
||||
newPath += "/" + listviewNav.model[a];
|
||||
console.log(newPath);
|
||||
navigation.navigate(newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: flowItems
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
|
||||
//columns: Math.floor(width / 150)
|
||||
cellWidth: 120
|
||||
cellHeight: 160
|
||||
|
||||
delegate: Item {
|
||||
id: del
|
||||
width: 120
|
||||
height: 160
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 140
|
||||
clip: true
|
||||
color: systemPalette.button
|
||||
anchors.centerIn: parent
|
||||
radius: 5
|
||||
|
||||
Image {
|
||||
id: thumbImage
|
||||
width: parent.width - 10
|
||||
height: width
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: 5
|
||||
}
|
||||
source: del.modelData["type"] === "dir" ? "" : "image://thumbnails/file://" + del.modelData["full"]
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
||||
|
||||
Image {
|
||||
visible: Image.Ready != thumbImage.status
|
||||
width: parent.width - 10
|
||||
height: width
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: 5
|
||||
}
|
||||
source: {
|
||||
if( "dir" === del.modelData["type"] && ".." === del.modelData["name"] )
|
||||
return "qrc:/images/go-parent-folder.svg";
|
||||
else if( "dir" === del.modelData["type"] )
|
||||
return "qrc:/images/document-open-folder.svg";
|
||||
else if( "video" === del.modelData["type"] )
|
||||
return "qrc:/images/video-symbolic.svg";
|
||||
else if( "image" === del.modelData["type"] )
|
||||
return "qrc:/images/insert-image.svg";
|
||||
else return "qrc:/images/question.svg";
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
margins: 5
|
||||
}
|
||||
elide: Text.ElideRight
|
||||
text: ("dir" === del.modelData["type"] && ".." === del.modelData["name"]) ? qsTr("Up") : del.modelData["name"]
|
||||
horizontalAlignment: contentWidth >= width ? Text.AlignLeft : Text.AlignHCenter
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
if( ".." === del.modelData["name"] ) {
|
||||
let newPath = "";
|
||||
for( let a=1; a < listviewNav.model.length-1; a++ )
|
||||
newPath += "/" + listviewNav.model[a];
|
||||
|
||||
if( "" === newPath )
|
||||
newPath = "/";
|
||||
|
||||
//console.log("dir: "+newPath);
|
||||
navigation.navigate(newPath);
|
||||
} if( "dir" === del.modelData["type"] ) {
|
||||
let newPath = "";
|
||||
for( let a=1; a < listviewNav.model.length; a++ )
|
||||
newPath += "/" + listviewNav.model[a];
|
||||
newPath += "/" + del.modelData["name"];
|
||||
|
||||
//console.log("dir: "+newPath);
|
||||
navigation.navigate(newPath);
|
||||
} else {
|
||||
//console.log("file: "+del.modelData["name"]);
|
||||
navigation.viewEntry(del.modelData["name"]);
|
||||
}
|
||||
} // onClicked
|
||||
} // MouseArea
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
284
qml/Viewer.qml
Normal file
284
qml/Viewer.qml
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Layouts
|
||||
|
||||
import QtMultimedia
|
||||
|
||||
import miva
|
||||
|
||||
Item {
|
||||
id: viewer
|
||||
|
||||
property alias currentIndex: listview.currentIndex
|
||||
property alias model: listview.model
|
||||
|
||||
signal toggleFullscreen()
|
||||
signal hideViewer()
|
||||
|
||||
function warpTo(idx) {
|
||||
listview.highlightMoveDuration = 0;
|
||||
listview.currentIndex = idx;
|
||||
}
|
||||
function previousImage() {
|
||||
let nidx = listview.currentIndex-1;
|
||||
if( nidx < 0 ) {
|
||||
nidx = model.length - 1;
|
||||
listview.highlightMoveDuration = 0;
|
||||
}
|
||||
listview.currentIndex = nidx;
|
||||
Qt.callLater( function() { listview.highlightMoveDuration = 200; } );
|
||||
}
|
||||
function nextImage() {
|
||||
let nidx = listview.currentIndex+1;
|
||||
if( nidx >= model.length ) {
|
||||
nidx = 0;
|
||||
listview.highlightMoveDuration = 0;
|
||||
}
|
||||
listview.currentIndex = nidx;
|
||||
Qt.callLater( function() { listview.highlightMoveDuration = 200; } );
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Material.background
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listview
|
||||
anchors.fill: parent
|
||||
orientation: Qt.Horizontal
|
||||
snapMode: ListView.SnapOneItem
|
||||
highlightRangeMode: ListView.StrictlyEnforceRange
|
||||
highlightMoveDuration: 200
|
||||
pixelAligned: true
|
||||
reuseItems: true
|
||||
|
||||
onMovementEnded: highlightMoveDuration = 200;
|
||||
|
||||
Component.onCompleted: {
|
||||
if( !currentItem )
|
||||
return;
|
||||
|
||||
currentItem.listviewComplete();
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: viewerdelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property bool isCurrentItem: index === listview.currentIndex
|
||||
|
||||
width: listview.width
|
||||
height: listview.height
|
||||
|
||||
onIsCurrentItemChanged: mediaplayer.wantPlayback = true;
|
||||
function listviewComplete() {
|
||||
mediaplayer.updatePlayback();
|
||||
}
|
||||
|
||||
MediaPlayer {
|
||||
id: mediaplayer
|
||||
videoOutput: video
|
||||
audioOutput: audio
|
||||
readonly property bool canPlay: viewerdelegate.isCurrentItem && visible
|
||||
property bool wantPlayback: true
|
||||
onCanPlayChanged: updatePlayback();
|
||||
onWantPlaybackChanged: updatePlayback();
|
||||
|
||||
function updatePlayback() {
|
||||
if( canPlay && wantPlayback )
|
||||
play();
|
||||
else
|
||||
pause();
|
||||
}
|
||||
|
||||
source: video.visible ? "file://" + viewerdelegate.modelData["full"] : ""
|
||||
/*
|
||||
onPlaybackStateChanged: {
|
||||
console.log(`Video.playbackState: ${playbackState}`);
|
||||
if( !canPlay || !wantPlayback )
|
||||
return;
|
||||
|
||||
if( playbackState !== MediaPlayer.PlayingState )
|
||||
//play();
|
||||
seek(0);
|
||||
}
|
||||
*/
|
||||
loops: MediaPlayer.Infinite
|
||||
onPositionChanged: videopositionslider.value = mediaplayer.position
|
||||
}
|
||||
|
||||
AudioOutput {
|
||||
id: audio
|
||||
muted: muteButton.checked
|
||||
}
|
||||
|
||||
VideoOutput {
|
||||
id: video
|
||||
endOfStreamPolicy: VideoOutput.KeepLastFrame
|
||||
visible: viewerdelegate.modelData["type"] === "video"
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
MultiPointTouchArea {
|
||||
anchors.fill: parent
|
||||
mouseEnabled: false
|
||||
minimumTouchPoints: 1
|
||||
maximumTouchPoints: 2
|
||||
|
||||
touchPoints: [
|
||||
TouchPoint { id: point1 },
|
||||
TouchPoint { id: point2 }
|
||||
]
|
||||
|
||||
Timer {
|
||||
id: multiPointDoubleTapTimer
|
||||
property variant previousPoints
|
||||
interval: Qt.styleHints.mouseDoubleClickInterval
|
||||
repeat: false
|
||||
onTriggered: parent.tapped(previousPoints)
|
||||
}
|
||||
|
||||
onReleased: function(p) {
|
||||
if (multiPointDoubleTapTimer.running) {
|
||||
multiPointDoubleTapTimer.stop()
|
||||
doubleTapped(p)
|
||||
} else if(p.length === 1) {
|
||||
multiPointDoubleTapTimer.previousPoints = p
|
||||
multiPointDoubleTapTimer.restart()
|
||||
} else {
|
||||
multiPointDoubleTapTimer.stop()
|
||||
tapped(p)
|
||||
}
|
||||
}
|
||||
|
||||
function tapped(p) {
|
||||
let deltasX = [], deltasY = [];
|
||||
for (let a=0; a < p.length; a++) {
|
||||
deltasX.push( p[a].x - p[a].startX )
|
||||
deltasY.push( p[a].y - p[a].startY )
|
||||
}
|
||||
|
||||
let absX=0, absY=0
|
||||
let avgX=0, avgY=0
|
||||
for (let b=0; b < p.length; b++) {
|
||||
const absDX = Math.abs(deltasX[b])
|
||||
const absDY = Math.abs(deltasY[b])
|
||||
if (absDX > absX)
|
||||
absX = absDX
|
||||
if (absDY > absY)
|
||||
absY = absDY
|
||||
avgX += deltasX[b] / p.length
|
||||
avgY += deltasY[b] / p.length
|
||||
}
|
||||
const dtDist = Qt.styleHints.touchDoubleTapDistance * 2
|
||||
|
||||
if (p.length === 1) {
|
||||
return mediaplayer.wantPlayback = !mediaplayer.wantPlayback;
|
||||
if (absX < dtDist && absY < dtDist) {
|
||||
if (p[0].x < width * 0.25)
|
||||
return // smallSeekBack
|
||||
else if (p[0].x > width * 0.75)
|
||||
return // smallSeekForward
|
||||
|
||||
return mediaplayer.wantPlayback = !mediaplayer.wantPlayback;
|
||||
}
|
||||
|
||||
if (absX > absY && absX > Qt.styleHints.startDragDistance) {
|
||||
if (deltasX[0] > 0)
|
||||
return // mediumSeekForward
|
||||
else if (deltasX[0] < 0)
|
||||
return // mediumSeekBack
|
||||
}
|
||||
} else if (p.length === 2) {
|
||||
if (absX < dtDist && absY < dtDist)
|
||||
return viewer.toggleFullscreen();
|
||||
|
||||
if (absX > absY && absX > Qt.styleHints.startDragDistance) {
|
||||
if (avgX > 0)
|
||||
return viewer.nextImage();
|
||||
|
||||
else if (avgX < 0)
|
||||
return viewer.previousImage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function doubleTapped(p) {
|
||||
return viewer.toggleFullscreen();
|
||||
/*
|
||||
if (p.length > 1)
|
||||
return
|
||||
|
||||
if (p[0].x < width * 0.25)
|
||||
return HarunaApp.actions["seekBackwardBigAction"].trigger()
|
||||
else if (p[0].x > width * 0.75)
|
||||
return HarunaApp.actions["seekForwardBigAction"].trigger()
|
||||
|
||||
HarunaApp.actions["toggleFullscreenAction"].trigger()
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
margins: 10
|
||||
}
|
||||
visible: video.visible && !main.isFullscreen
|
||||
opacity: 0.25
|
||||
|
||||
Button {
|
||||
id: muteButton
|
||||
icon.name: checked ? "audio-volume-muted" : "audio-volume-high"
|
||||
checkable: true
|
||||
checked: true
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: videopositionslider
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
from: 0
|
||||
to: mediaplayer.duration
|
||||
enabled: mediaplayer.seekable
|
||||
onMoved: mediaplayer.position = value;
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
visible: viewerdelegate.modelData["type"] === "image"
|
||||
anchors.fill: parent
|
||||
//playing: parent.index === listview.currentIndex && visible
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: visible ? "file://" + viewerdelegate.modelData["full"] : ""
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: image.fillMode = (image.fillMode === Image.PreserveAspectFit ? Image.Pad : Image.PreserveAspectFit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RoundButton {
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
margins: 15
|
||||
}
|
||||
opacity: 0.2
|
||||
icon.name: "window-close"
|
||||
onClicked: viewer.hideViewer();
|
||||
}
|
||||
}
|
||||
|
||||
136
src/main.cpp
Normal file
136
src/main.cpp
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QtEnvironmentVariables>
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
|
||||
#include "mutils.h"
|
||||
#include "thumbnailprovider.h"
|
||||
|
||||
#ifdef _EMSCRIPTEN_
|
||||
#include <QJsonDocument>
|
||||
#include <emscripten.h>
|
||||
|
||||
EM_JS(void, __js_log, (const char *formatstr, size_t formatlen, const char *argsstr, size_t argslen), {
|
||||
const jsformat = UTF16ToString(formatstr, formatlen);
|
||||
const jsargstr = UTF16ToString(argsstr, argslen);
|
||||
try {
|
||||
const jsargs = JSON.parse(jsargstr);
|
||||
|
||||
console.log( jsformat, ...jsargs );
|
||||
} catch(e) {
|
||||
console.log("Failed to log message: "+e);
|
||||
}
|
||||
});
|
||||
|
||||
void __log(const QString &format, const QVariantList &args)
|
||||
{
|
||||
QJsonDocument jsonArgs = QJsonDocument::fromVariant(args);
|
||||
QString asJson = QString::fromUtf8(jsonArgs.toJson(QJsonDocument::Compact));
|
||||
__js_log( (const char *)format.utf16(), format.length()*2, (const char*)asJson.utf16(), asJson.length()*2 );
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef ANDROID
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
QByteArray localMsg = msg.toLocal8Bit();
|
||||
const char *file = context.file ? context.file : "";
|
||||
const char *function = context.function ? context.function : "";
|
||||
# ifndef _EMSCRIPTEN_
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
fprintf(stderr, "\u001b[36;1m[debug]\u001b[36m");
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
fprintf(stderr, "\u001b[37;1m[info]\u001b[37m");
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
fprintf(stderr, "\u001b[35;1m[warning]\u001b[35m");
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
fprintf(stderr, "\u001b[38;1m[critical]\u001b[38m");
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
fprintf(stderr, "\u001b[34;1m[fatal]\u001b[34m");
|
||||
break;
|
||||
}
|
||||
fprintf(stderr, " %s \u001b[35m(%s:%u, %s)\u001b[0m\n", localMsg.constData(), file, context.line, function);
|
||||
# else
|
||||
QString header;
|
||||
QVariantList args;
|
||||
switch (type) {
|
||||
case QtDebugMsg:
|
||||
header = "%c[debug]";
|
||||
args << "background: green; color: white;";
|
||||
args << "background: white; color: green;";
|
||||
break;
|
||||
case QtInfoMsg:
|
||||
header = "%c[info]";
|
||||
args << "background: black; color: white;";
|
||||
args << "background: white; color: black;";
|
||||
break;
|
||||
case QtWarningMsg:
|
||||
header = "%c[warning]";
|
||||
args << "background: yellow; color: black;";
|
||||
args << "background: black; color: yellow;";
|
||||
break;
|
||||
case QtCriticalMsg:
|
||||
header = "%c[critical]";
|
||||
args << "background: black; color: orange;";
|
||||
args << "background: orange; color: black;";
|
||||
break;
|
||||
case QtFatalMsg:
|
||||
header = "%c[fatal]";
|
||||
args << "background: red; color: white;";
|
||||
args << "background: white; color: red;";
|
||||
break;
|
||||
}
|
||||
|
||||
__log( QString("%1%c %2 (%3:%4, '%5')").arg(header).arg(localMsg).arg(file).arg(context.line).arg(function), args );
|
||||
//fprintf(stderr, " %s \u001b[35m(%s:%u, %s)\u001b[0m\n", localMsg.constData(), file, context.line, function);
|
||||
# endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
#ifndef ANDROID
|
||||
qInstallMessageHandler(messageHandler);
|
||||
#endif
|
||||
|
||||
QApplication app(argc, argv);
|
||||
|
||||
app.setApplicationDisplayName("Miva");
|
||||
app.setApplicationName("miva");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("ONeill Codesmithing");
|
||||
app.setOrganizationDomain("oneill.app");
|
||||
app.setDesktopFileName("miva");
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
|
||||
qmlRegisterType<MUtils>("miva", 1, 0, "MUtils");
|
||||
|
||||
engine.addImageProvider("thumbnails", new ThumbnailProvider);
|
||||
//engine.rootContext()->setContextProperty("MUtils", new MUtils());
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/qml/Main.qml"));
|
||||
|
||||
QObject::connect(
|
||||
&engine,
|
||||
&QQmlApplicationEngine::objectCreationFailed,
|
||||
&app,
|
||||
[]() { QCoreApplication::exit(-1); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
engine.load(url);
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
148
src/mutils.cpp
Normal file
148
src/mutils.cpp
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "mutils.h"
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QFileDialog>
|
||||
#include <QGuiApplication>
|
||||
#include <QMimeData>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QSysInfo>
|
||||
#include <QUuid>
|
||||
|
||||
MUtils::MUtils(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVariant MUtils::dirListing(const QString &path, DirFilter filter, DirSort sort)
|
||||
{
|
||||
QDir d(path);
|
||||
QDir::Filters qfilter = QDir::Filters( (int)filter );
|
||||
QDir::SortFlags qsort = QDir::SortFlags( (int)sort );
|
||||
QVariantList results;
|
||||
QFileInfoList ents = d.entryInfoList( qfilter, qsort );
|
||||
for( QFileInfo ent : ents ) {
|
||||
QVariantMap ment;
|
||||
ment["absoluteFilePath"] = ent.absoluteFilePath();
|
||||
ment["absolutePath"] = ent.absolutePath();
|
||||
ment["baseName"] = ent.baseName();
|
||||
ment["fileName"] = ent.fileName();
|
||||
ment["filePath"] = ent.filePath();
|
||||
ment["isDir"] = ent.isDir();
|
||||
ment["isFile"] = ent.isFile();
|
||||
ment["path"] = ent.path();
|
||||
ment["size"] = ent.size();
|
||||
ment["suffix"] = ent.suffix();
|
||||
results.append(ment);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
QByteArray MUtils::readFile(const QString &path)
|
||||
{
|
||||
QFile f(path);
|
||||
if( !f.open(QIODeviceBase::ReadOnly) )
|
||||
return {};
|
||||
|
||||
QByteArray result = f.readAll();
|
||||
f.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
QString MUtils::getenv(const QString &name) const
|
||||
{
|
||||
if (name.trimmed().isEmpty())
|
||||
return {};
|
||||
|
||||
const QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||
return env.value(name);
|
||||
}
|
||||
|
||||
QString MUtils::uuid()
|
||||
{
|
||||
return QUuid::createUuid().toString();
|
||||
}
|
||||
|
||||
bool MUtils::launchProcess(const QString &app, const QStringList &args, const QString &workingDir)
|
||||
{
|
||||
return QProcess::startDetached(app, args, workingDir);
|
||||
}
|
||||
|
||||
QString MUtils::chooseSaveFilePath(
|
||||
const QString &title,
|
||||
const QString &directory,
|
||||
const QString &fileName)
|
||||
{
|
||||
const QString suggestedPath = QDir(directory).filePath(fileName);
|
||||
return QFileDialog::getSaveFileName(
|
||||
nullptr,
|
||||
title,
|
||||
suggestedPath);
|
||||
}
|
||||
|
||||
QString MUtils::chooseSavePath(
|
||||
const QString &title,
|
||||
const QString &directory)
|
||||
{
|
||||
const QString suggestedPath = QDir(directory).absolutePath();
|
||||
return QFileDialog::getExistingDirectory(nullptr, title, suggestedPath, QFileDialog::ShowDirsOnly);
|
||||
}
|
||||
|
||||
QString MUtils::clipboardText() const
|
||||
{
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
if (!clipboard)
|
||||
return {};
|
||||
|
||||
QString text = clipboard->text(QClipboard::Clipboard);
|
||||
if (text.isEmpty() && clipboard->supportsSelection())
|
||||
text = clipboard->text(QClipboard::Selection);
|
||||
return text;
|
||||
}
|
||||
|
||||
bool MUtils::hasClipboardText() const
|
||||
{
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
if (!clipboard)
|
||||
return false;
|
||||
|
||||
const QMimeData *clipboardMime = clipboard->mimeData(QClipboard::Clipboard);
|
||||
if (clipboardMime && clipboardMime->hasText() && !clipboardMime->text().isEmpty())
|
||||
return true;
|
||||
|
||||
if (!clipboard->supportsSelection())
|
||||
return false;
|
||||
|
||||
const QMimeData *selectionMime = clipboard->mimeData(QClipboard::Selection);
|
||||
return selectionMime && selectionMime->hasText() && !selectionMime->text().isEmpty();
|
||||
}
|
||||
|
||||
void MUtils::setClipboardText(const QString &text) const
|
||||
{
|
||||
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||
if (!clipboard)
|
||||
return;
|
||||
|
||||
clipboard->setText(text, QClipboard::Clipboard);
|
||||
if (clipboard->supportsSelection())
|
||||
clipboard->setText(text, QClipboard::Selection);
|
||||
}
|
||||
|
||||
QVariantMap MUtils::sysinfo() const
|
||||
{
|
||||
QSysInfo si;
|
||||
|
||||
QVariantMap result;
|
||||
result["arch"] = si.currentCpuArchitecture();
|
||||
result["kernelType"] = si.kernelType();
|
||||
result["kernelVersion"] = si.kernelVersion();
|
||||
result["prettyProductName"] = si.prettyProductName();
|
||||
result["productType"] = si.productType();
|
||||
result["productVersion"] = si.productVersion();
|
||||
|
||||
return result;
|
||||
}
|
||||
77
src/mutils.h
Normal file
77
src/mutils.h
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QVariantMap>
|
||||
|
||||
class MUtils : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum DirFilterFlag {
|
||||
Dirs = QDir::Dirs,
|
||||
AllDirs = QDir::AllDirs,
|
||||
Files = QDir::Files,
|
||||
Drives = QDir::Drives,
|
||||
NoSymLinks = QDir::NoSymLinks,
|
||||
NoDotAndDotDot = QDir::NoDotAndDotDot,
|
||||
NoDot = QDir::NoDot,
|
||||
NoDotDot = QDir::NoDotDot,
|
||||
AllEntries = QDir::AllEntries,
|
||||
Readable = QDir::Readable,
|
||||
Writable = QDir::Writable,
|
||||
Executable = QDir::Executable,
|
||||
Hidden = QDir::Hidden,
|
||||
System = QDir::System,
|
||||
CaseSensitive = QDir::CaseSensitive
|
||||
};
|
||||
Q_ENUM(DirFilterFlag)
|
||||
Q_DECLARE_FLAGS(DirFilter, DirFilterFlag)
|
||||
Q_FLAG(DirFilter)
|
||||
|
||||
enum DirSortFlag {
|
||||
Name = QDir::Name,
|
||||
Time = QDir::Time,
|
||||
Size = QDir::Size,
|
||||
Type = QDir::Type,
|
||||
Unsorted = QDir::Unsorted,
|
||||
NoSort = QDir::NoSort,
|
||||
DirsFirst = QDir::DirsFirst,
|
||||
DirsLast = QDir::DirsLast,
|
||||
Reversed = QDir::Reversed,
|
||||
IgnoreCase = QDir::IgnoreCase,
|
||||
LocaleAware = QDir::LocaleAware
|
||||
};
|
||||
Q_ENUM(DirSortFlag)
|
||||
Q_DECLARE_FLAGS(DirSort, DirSortFlag)
|
||||
Q_FLAG(DirSort)
|
||||
|
||||
explicit MUtils(QObject *parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QVariant dirListing(const QString &path, DirFilter filter=AllEntries, DirSort sort=NoSort);
|
||||
Q_INVOKABLE QByteArray readFile(const QString &path);
|
||||
Q_INVOKABLE QString getenv(const QString &name) const;
|
||||
Q_INVOKABLE QString uuid();
|
||||
Q_INVOKABLE bool launchProcess(const QString &app, const QStringList &args={}, const QString &workingDir={});
|
||||
Q_INVOKABLE QString chooseSaveFilePath(
|
||||
const QString &title,
|
||||
const QString &directory,
|
||||
const QString &fileName);
|
||||
Q_INVOKABLE QString chooseSavePath(
|
||||
const QString &title,
|
||||
const QString &directory);
|
||||
Q_INVOKABLE QString clipboardText() const;
|
||||
Q_INVOKABLE bool hasClipboardText() const;
|
||||
Q_INVOKABLE void setClipboardText(const QString &text) const;
|
||||
Q_INVOKABLE QVariantMap sysinfo() const;
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(MUtils::DirFilter)
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(MUtils::DirSort)
|
||||
51
src/thumbnailprovider.cpp
Normal file
51
src/thumbnailprovider.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "thumbnailprovider.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QQuickAsyncImageProvider>
|
||||
|
||||
#include <KIO/PreviewJob>
|
||||
#include <KFileItem>
|
||||
|
||||
AsyncThumbnailResponse::AsyncThumbnailResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
m_id = id;
|
||||
m_size = requestedSize;
|
||||
fetch();
|
||||
}
|
||||
|
||||
QQuickTextureFactory *AsyncThumbnailResponse::textureFactory() const
|
||||
{
|
||||
return QQuickTextureFactory::textureFactoryForImage(m_image);
|
||||
}
|
||||
|
||||
void AsyncThumbnailResponse::fetch() {
|
||||
const QUrl url = QUrl::fromPercentEncoding(m_id.toUtf8());
|
||||
const QSize thumbSize = m_size.isValid() ? m_size : QSize(256, 256);
|
||||
|
||||
KFileItem item(url, KFileItem::NormalMimeTypeDetermination);
|
||||
QStringList plugins = KIO::PreviewJob::availablePlugins();
|
||||
plugins.removeAll("gst-audio-thumbnailer"); // This doesn't work for our purposes, but claims to support M4B
|
||||
auto job = KIO::filePreview({ item }, thumbSize, &plugins);
|
||||
|
||||
QObject::connect(job, &KIO::PreviewJob::gotPreview, this,
|
||||
[this](const KFileItem &, const QPixmap &pixmap) {
|
||||
m_image = pixmap.toImage();
|
||||
emit finished();
|
||||
});
|
||||
|
||||
QObject::connect(job, &KIO::PreviewJob::failed, this,
|
||||
[this](const KFileItem &item) {
|
||||
qWarning() << QStringLiteral("ThumbnailProvider: Failed to load thumbnail for %1").arg(item.url().toString());
|
||||
emit finished();
|
||||
});
|
||||
}
|
||||
|
||||
QQuickImageResponse *ThumbnailProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
|
||||
{
|
||||
AsyncThumbnailResponse *res = new AsyncThumbnailResponse(id, requestedSize);
|
||||
return res;
|
||||
}
|
||||
32
src/thumbnailprovider.h
Normal file
32
src/thumbnailprovider.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
// SPDX-FileCopyrightText: 2026 Daniel O'Neill <daniel@oneill.app>
|
||||
//
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QImage>
|
||||
#include <QMimeDatabase>
|
||||
#include <QQuickAsyncImageProvider>
|
||||
#include <QQuickTextureFactory>
|
||||
#include <QString>
|
||||
|
||||
class AsyncThumbnailResponse : public QQuickImageResponse {
|
||||
public:
|
||||
AsyncThumbnailResponse(const QString &id, const QSize &requestedSize);
|
||||
|
||||
QQuickTextureFactory *textureFactory() const override;
|
||||
|
||||
void fetch();
|
||||
|
||||
QString m_id;
|
||||
QImage m_image;
|
||||
QSize m_size;
|
||||
|
||||
QMimeDatabase m_mimedb;
|
||||
};
|
||||
|
||||
|
||||
class ThumbnailProvider : public QQuickAsyncImageProvider {
|
||||
public:
|
||||
QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
|
||||
};
|
||||
Loading…
Reference in a new issue