Initial public commit
This commit is contained in:
commit
f46992805f
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
main
|
||||||
|
target/
|
595
COPYING.md
Normal file
595
COPYING.md
Normal file
|
@ -0,0 +1,595 @@
|
||||||
|
GNU General Public License
|
||||||
|
==========================
|
||||||
|
|
||||||
|
_Version 3, 29 June 2007_
|
||||||
|
_Copyright © 2007 Free Software Foundation, Inc. <<http://fsf.org/>>_
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license
|
||||||
|
document, but changing it is not allowed.
|
||||||
|
|
||||||
|
## Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for software and other
|
||||||
|
kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed to take away
|
||||||
|
your freedom to share and change the works. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change all versions of a
|
||||||
|
program--to make sure it remains free software for all its users. We, the Free
|
||||||
|
Software Foundation, use the GNU General Public License for most of our software; it
|
||||||
|
applies also to any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General
|
||||||
|
Public Licenses are designed to make sure that you have the freedom to distribute
|
||||||
|
copies of free software (and charge for them if you wish), that you receive source
|
||||||
|
code or can get it if you want it, that you can change the software or use pieces of
|
||||||
|
it in new free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you these rights or
|
||||||
|
asking you to surrender the rights. Therefore, you have certain responsibilities if
|
||||||
|
you distribute copies of the software, or if you modify it: responsibilities to
|
||||||
|
respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee,
|
||||||
|
you must pass on to the recipients the same freedoms that you received. You must make
|
||||||
|
sure that they, too, receive or can get the source code. And you must show them these
|
||||||
|
terms so they know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps: **(1)** assert
|
||||||
|
copyright on the software, and **(2)** offer you this License giving you legal permission
|
||||||
|
to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains that there is
|
||||||
|
no warranty for this free software. For both users' and authors' sake, the GPL
|
||||||
|
requires that modified versions be marked as changed, so that their problems will not
|
||||||
|
be attributed erroneously to authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run modified versions of
|
||||||
|
the software inside them, although the manufacturer can do so. This is fundamentally
|
||||||
|
incompatible with the aim of protecting users' freedom to change the software. The
|
||||||
|
systematic pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we have designed
|
||||||
|
this version of the GPL to prohibit the practice for those products. If such problems
|
||||||
|
arise substantially in other domains, we stand ready to extend this provision to
|
||||||
|
those domains in future versions of the GPL, as needed to protect the freedom of
|
||||||
|
users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents. States should
|
||||||
|
not allow patents to restrict development and use of software on general-purpose
|
||||||
|
computers, but in those that do, we wish to avoid the special danger that patents
|
||||||
|
applied to a free program could make it effectively proprietary. To prevent this, the
|
||||||
|
GPL assures that patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
## TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
### 0. Definitions
|
||||||
|
|
||||||
|
“This License” refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
“Copyright” also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
“The Program” refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as “you”. “Licensees” and
|
||||||
|
“recipients” may be individuals or organizations.
|
||||||
|
|
||||||
|
To “modify” a work means to copy from or adapt all or part of the work in
|
||||||
|
a fashion requiring copyright permission, other than the making of an exact copy. The
|
||||||
|
resulting work is called a “modified version” of the earlier work or a
|
||||||
|
work “based on” the earlier work.
|
||||||
|
|
||||||
|
A “covered work” means either the unmodified Program or a work based on
|
||||||
|
the Program.
|
||||||
|
|
||||||
|
To “propagate” a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for infringement under
|
||||||
|
applicable copyright law, except executing it on a computer or modifying a private
|
||||||
|
copy. Propagation includes copying, distribution (with or without modification),
|
||||||
|
making available to the public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To “convey” a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through a computer
|
||||||
|
network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays “Appropriate Legal Notices” to the
|
||||||
|
extent that it includes a convenient and prominently visible feature that **(1)**
|
||||||
|
displays an appropriate copyright notice, and **(2)** tells the user that there is no
|
||||||
|
warranty for the work (except to the extent that warranties are provided), that
|
||||||
|
licensees may convey the work under this License, and how to view a copy of this
|
||||||
|
License. If the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
### 1. Source Code
|
||||||
|
|
||||||
|
The “source code” for a work means the preferred form of the work for
|
||||||
|
making modifications to it. “Object code” means any non-source form of a
|
||||||
|
work.
|
||||||
|
|
||||||
|
A “Standard Interface” means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of interfaces
|
||||||
|
specified for a particular programming language, one that is widely used among
|
||||||
|
developers working in that language.
|
||||||
|
|
||||||
|
The “System Libraries” of an executable work include anything, other than
|
||||||
|
the work as a whole, that **(a)** is included in the normal form of packaging a Major
|
||||||
|
Component, but which is not part of that Major Component, and **(b)** serves only to
|
||||||
|
enable use of the work with that Major Component, or to implement a Standard
|
||||||
|
Interface for which an implementation is available to the public in source code form.
|
||||||
|
A “Major Component”, in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system (if any) on which
|
||||||
|
the executable work runs, or a compiler used to produce the work, or an object code
|
||||||
|
interpreter used to run it.
|
||||||
|
|
||||||
|
The “Corresponding Source” for a work in object code form means all the
|
||||||
|
source code needed to generate, install, and (for an executable work) run the object
|
||||||
|
code and to modify the work, including scripts to control those activities. However,
|
||||||
|
it does not include the work's System Libraries, or general-purpose tools or
|
||||||
|
generally available free programs which are used unmodified in performing those
|
||||||
|
activities but which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for the work, and
|
||||||
|
the source code for shared libraries and dynamically linked subprograms that the work
|
||||||
|
is specifically designed to require, such as by intimate data communication or
|
||||||
|
control flow between those subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users can regenerate
|
||||||
|
automatically from other parts of the Corresponding Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that same work.
|
||||||
|
|
||||||
|
### 2. Basic Permissions
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of copyright on the
|
||||||
|
Program, and are irrevocable provided the stated conditions are met. This License
|
||||||
|
explicitly affirms your unlimited permission to run the unmodified Program. The
|
||||||
|
output from running a covered work is covered by this License only if the output,
|
||||||
|
given its content, constitutes a covered work. This License acknowledges your rights
|
||||||
|
of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not convey, without
|
||||||
|
conditions so long as your license otherwise remains in force. You may convey covered
|
||||||
|
works to others for the sole purpose of having them make modifications exclusively
|
||||||
|
for you, or provide you with facilities for running those works, provided that you
|
||||||
|
comply with the terms of this License in conveying all material for which you do not
|
||||||
|
control copyright. Those thus making or running the covered works for you must do so
|
||||||
|
exclusively on your behalf, under your direction and control, on terms that prohibit
|
||||||
|
them from making any copies of your copyrighted material outside their relationship
|
||||||
|
with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under the conditions
|
||||||
|
stated below. Sublicensing is not allowed; section 10 makes it unnecessary.
|
||||||
|
|
||||||
|
### 3. Protecting Users' Legal Rights From Anti-Circumvention Law
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological measure under any
|
||||||
|
applicable law fulfilling obligations under article 11 of the WIPO copyright treaty
|
||||||
|
adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention
|
||||||
|
of such measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid circumvention of
|
||||||
|
technological measures to the extent such circumvention is effected by exercising
|
||||||
|
rights under this License with respect to the covered work, and you disclaim any
|
||||||
|
intention to limit operation or modification of the work as a means of enforcing,
|
||||||
|
against the work's users, your or third parties' legal rights to forbid circumvention
|
||||||
|
of technological measures.
|
||||||
|
|
||||||
|
### 4. Conveying Verbatim Copies
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you receive it, in any
|
||||||
|
medium, provided that you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice; keep intact all notices stating that this License and
|
||||||
|
any non-permissive terms added in accord with section 7 apply to the code; keep
|
||||||
|
intact all notices of the absence of any warranty; and give all recipients a copy of
|
||||||
|
this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey, and you may offer
|
||||||
|
support or warranty protection for a fee.
|
||||||
|
|
||||||
|
### 5. Conveying Modified Source Versions
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to produce it from
|
||||||
|
the Program, in the form of source code under the terms of section 4, provided that
|
||||||
|
you also meet all of these conditions:
|
||||||
|
|
||||||
|
* **a)** The work must carry prominent notices stating that you modified it, and giving a
|
||||||
|
relevant date.
|
||||||
|
* **b)** The work must carry prominent notices stating that it is released under this
|
||||||
|
License and any conditions added under section 7. This requirement modifies the
|
||||||
|
requirement in section 4 to “keep intact all notices”.
|
||||||
|
* **c)** You must license the entire work, as a whole, under this License to anyone who
|
||||||
|
comes into possession of a copy. This License will therefore apply, along with any
|
||||||
|
applicable section 7 additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no permission to license the
|
||||||
|
work in any other way, but it does not invalidate such permission if you have
|
||||||
|
separately received it.
|
||||||
|
* **d)** If the work has interactive user interfaces, each must display Appropriate Legal
|
||||||
|
Notices; however, if the Program has interactive interfaces that do not display
|
||||||
|
Appropriate Legal Notices, your work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent works, which are
|
||||||
|
not by their nature extensions of the covered work, and which are not combined with
|
||||||
|
it such as to form a larger program, in or on a volume of a storage or distribution
|
||||||
|
medium, is called an “aggregate” if the compilation and its resulting
|
||||||
|
copyright are not used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work in an aggregate
|
||||||
|
does not cause this License to apply to the other parts of the aggregate.
|
||||||
|
|
||||||
|
### 6. Conveying Non-Source Forms
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms of sections 4 and
|
||||||
|
5, provided that you also convey the machine-readable Corresponding Source under the
|
||||||
|
terms of this License, in one of these ways:
|
||||||
|
|
||||||
|
* **a)** Convey the object code in, or embodied in, a physical product (including a
|
||||||
|
physical distribution medium), accompanied by the Corresponding Source fixed on a
|
||||||
|
durable physical medium customarily used for software interchange.
|
||||||
|
* **b)** Convey the object code in, or embodied in, a physical product (including a
|
||||||
|
physical distribution medium), accompanied by a written offer, valid for at least
|
||||||
|
three years and valid for as long as you offer spare parts or customer support for
|
||||||
|
that product model, to give anyone who possesses the object code either **(1)** a copy of
|
||||||
|
the Corresponding Source for all the software in the product that is covered by this
|
||||||
|
License, on a durable physical medium customarily used for software interchange, for
|
||||||
|
a price no more than your reasonable cost of physically performing this conveying of
|
||||||
|
source, or **(2)** access to copy the Corresponding Source from a network server at no
|
||||||
|
charge.
|
||||||
|
* **c)** Convey individual copies of the object code with a copy of the written offer to
|
||||||
|
provide the Corresponding Source. This alternative is allowed only occasionally and
|
||||||
|
noncommercially, and only if you received the object code with such an offer, in
|
||||||
|
accord with subsection 6b.
|
||||||
|
* **d)** Convey the object code by offering access from a designated place (gratis or for
|
||||||
|
a charge), and offer equivalent access to the Corresponding Source in the same way
|
||||||
|
through the same place at no further charge. You need not require recipients to copy
|
||||||
|
the Corresponding Source along with the object code. If the place to copy the object
|
||||||
|
code is a network server, the Corresponding Source may be on a different server
|
||||||
|
(operated by you or a third party) that supports equivalent copying facilities,
|
||||||
|
provided you maintain clear directions next to the object code saying where to find
|
||||||
|
the Corresponding Source. Regardless of what server hosts the Corresponding Source,
|
||||||
|
you remain obligated to ensure that it is available for as long as needed to satisfy
|
||||||
|
these requirements.
|
||||||
|
* **e)** Convey the object code using peer-to-peer transmission, provided you inform
|
||||||
|
other peers where the object code and Corresponding Source of the work are being
|
||||||
|
offered to the general public at no charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded from the
|
||||||
|
Corresponding Source as a System Library, need not be included in conveying the
|
||||||
|
object code work.
|
||||||
|
|
||||||
|
A “User Product” is either **(1)** a “consumer product”, which
|
||||||
|
means any tangible personal property which is normally used for personal, family, or
|
||||||
|
household purposes, or **(2)** anything designed or sold for incorporation into a
|
||||||
|
dwelling. In determining whether a product is a consumer product, doubtful cases
|
||||||
|
shall be resolved in favor of coverage. For a particular product received by a
|
||||||
|
particular user, “normally used” refers to a typical or common use of
|
||||||
|
that class of product, regardless of the status of the particular user or of the way
|
||||||
|
in which the particular user actually uses, or expects or is expected to use, the
|
||||||
|
product. A product is a consumer product regardless of whether the product has
|
||||||
|
substantial commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
“Installation Information” for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install and execute
|
||||||
|
modified versions of a covered work in that User Product from a modified version of
|
||||||
|
its Corresponding Source. The information must suffice to ensure that the continued
|
||||||
|
functioning of the modified object code is in no case prevented or interfered with
|
||||||
|
solely because modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or specifically for
|
||||||
|
use in, a User Product, and the conveying occurs as part of a transaction in which
|
||||||
|
the right of possession and use of the User Product is transferred to the recipient
|
||||||
|
in perpetuity or for a fixed term (regardless of how the transaction is
|
||||||
|
characterized), the Corresponding Source conveyed under this section must be
|
||||||
|
accompanied by the Installation Information. But this requirement does not apply if
|
||||||
|
neither you nor any third party retains the ability to install modified object code
|
||||||
|
on the User Product (for example, the work has been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a requirement to
|
||||||
|
continue to provide support service, warranty, or updates for a work that has been
|
||||||
|
modified or installed by the recipient, or for the User Product in which it has been
|
||||||
|
modified or installed. Access to a network may be denied when the modification itself
|
||||||
|
materially and adversely affects the operation of the network or violates the rules
|
||||||
|
and protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided, in accord with
|
||||||
|
this section must be in a format that is publicly documented (and with an
|
||||||
|
implementation available to the public in source code form), and must require no
|
||||||
|
special password or key for unpacking, reading or copying.
|
||||||
|
|
||||||
|
### 7. Additional Terms
|
||||||
|
|
||||||
|
“Additional permissions” are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions. Additional
|
||||||
|
permissions that are applicable to the entire Program shall be treated as though they
|
||||||
|
were included in this License, to the extent that they are valid under applicable
|
||||||
|
law. If additional permissions apply only to part of the Program, that part may be
|
||||||
|
used separately under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option remove any
|
||||||
|
additional permissions from that copy, or from any part of it. (Additional
|
||||||
|
permissions may be written to require their own removal in certain cases when you
|
||||||
|
modify the work.) You may place additional permissions on material, added by you to a
|
||||||
|
covered work, for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you add to a
|
||||||
|
covered work, you may (if authorized by the copyright holders of that material)
|
||||||
|
supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
* **a)** Disclaiming warranty or limiting liability differently from the terms of
|
||||||
|
sections 15 and 16 of this License; or
|
||||||
|
* **b)** Requiring preservation of specified reasonable legal notices or author
|
||||||
|
attributions in that material or in the Appropriate Legal Notices displayed by works
|
||||||
|
containing it; or
|
||||||
|
* **c)** Prohibiting misrepresentation of the origin of that material, or requiring that
|
||||||
|
modified versions of such material be marked in reasonable ways as different from the
|
||||||
|
original version; or
|
||||||
|
* **d)** Limiting the use for publicity purposes of names of licensors or authors of the
|
||||||
|
material; or
|
||||||
|
* **e)** Declining to grant rights under trademark law for use of some trade names,
|
||||||
|
trademarks, or service marks; or
|
||||||
|
* **f)** Requiring indemnification of licensors and authors of that material by anyone
|
||||||
|
who conveys the material (or modified versions of it) with contractual assumptions of
|
||||||
|
liability to the recipient, for any liability that these contractual assumptions
|
||||||
|
directly impose on those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered “further
|
||||||
|
restrictions” within the meaning of section 10. If the Program as you received
|
||||||
|
it, or any part of it, contains a notice stating that it is governed by this License
|
||||||
|
along with a term that is a further restriction, you may remove that term. If a
|
||||||
|
license document contains a further restriction but permits relicensing or conveying
|
||||||
|
under this License, you may add to a covered work material governed by the terms of
|
||||||
|
that license document, provided that the further restriction does not survive such
|
||||||
|
relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you must place, in
|
||||||
|
the relevant source files, a statement of the additional terms that apply to those
|
||||||
|
files, or a notice indicating where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the form of a
|
||||||
|
separately written license, or stated as exceptions; the above requirements apply
|
||||||
|
either way.
|
||||||
|
|
||||||
|
### 8. Termination
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly provided under
|
||||||
|
this License. Any attempt otherwise to propagate or modify it is void, and will
|
||||||
|
automatically terminate your rights under this License (including any patent licenses
|
||||||
|
granted under the third paragraph of section 11).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the licenses of
|
||||||
|
parties who have received copies or rights from you under this License. If your
|
||||||
|
rights have been terminated and not permanently reinstated, you do not qualify to
|
||||||
|
receive new licenses for the same material under section 10.
|
||||||
|
|
||||||
|
### 9. Acceptance Not Required for Having Copies
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or run a copy of the
|
||||||
|
Program. Ancillary propagation of a covered work occurring solely as a consequence of
|
||||||
|
using peer-to-peer transmission to receive a copy likewise does not require
|
||||||
|
acceptance. However, nothing other than this License grants you permission to
|
||||||
|
propagate or modify any covered work. These actions infringe copyright if you do not
|
||||||
|
accept this License. Therefore, by modifying or propagating a covered work, you
|
||||||
|
indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
### 10. Automatic Licensing of Downstream Recipients
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically receives a license
|
||||||
|
from the original licensors, to run, modify and propagate that work, subject to this
|
||||||
|
License. You are not responsible for enforcing compliance by third parties with this
|
||||||
|
License.
|
||||||
|
|
||||||
|
An “entity transaction” is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an organization, or
|
||||||
|
merging organizations. If propagation of a covered work results from an entity
|
||||||
|
transaction, each party to that transaction who receives a copy of the work also
|
||||||
|
receives whatever licenses to the work the party's predecessor in interest had or
|
||||||
|
could give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if the predecessor
|
||||||
|
has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the rights granted or
|
||||||
|
affirmed under this License. For example, you may not impose a license fee, royalty,
|
||||||
|
or other charge for exercise of rights granted under this License, and you may not
|
||||||
|
initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||||
|
that any patent claim is infringed by making, using, selling, offering for sale, or
|
||||||
|
importing the Program or any portion of it.
|
||||||
|
|
||||||
|
### 11. Patents
|
||||||
|
|
||||||
|
A “contributor” is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The work thus
|
||||||
|
licensed is called the contributor's “contributor version”.
|
||||||
|
|
||||||
|
A contributor's “essential patent claims” are all patent claims owned or
|
||||||
|
controlled by the contributor, whether already acquired or hereafter acquired, that
|
||||||
|
would be infringed by some manner, permitted by this License, of making, using, or
|
||||||
|
selling its contributor version, but do not include claims that would be infringed
|
||||||
|
only as a consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, “control” includes the right to grant patent
|
||||||
|
sublicenses in a manner consistent with the requirements of this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free patent license
|
||||||
|
under the contributor's essential patent claims, to make, use, sell, offer for sale,
|
||||||
|
import and otherwise run, modify and propagate the contents of its contributor
|
||||||
|
version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a “patent license” is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent (such as an
|
||||||
|
express permission to practice a patent or covenant not to sue for patent
|
||||||
|
infringement). To “grant” such a patent license to a party means to make
|
||||||
|
such an agreement or commitment not to enforce a patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license, and the
|
||||||
|
Corresponding Source of the work is not available for anyone to copy, free of charge
|
||||||
|
and under the terms of this License, through a publicly available network server or
|
||||||
|
other readily accessible means, then you must either **(1)** cause the Corresponding
|
||||||
|
Source to be so available, or **(2)** arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or **(3)** arrange, in a manner consistent with
|
||||||
|
the requirements of this License, to extend the patent license to downstream
|
||||||
|
recipients. “Knowingly relying” means you have actual knowledge that, but
|
||||||
|
for the patent license, your conveying the covered work in a country, or your
|
||||||
|
recipient's use of the covered work in a country, would infringe one or more
|
||||||
|
identifiable patents in that country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or arrangement, you
|
||||||
|
convey, or propagate by procuring conveyance of, a covered work, and grant a patent
|
||||||
|
license to some of the parties receiving the covered work authorizing them to use,
|
||||||
|
propagate, modify or convey a specific copy of the covered work, then the patent
|
||||||
|
license you grant is automatically extended to all recipients of the covered work and
|
||||||
|
works based on it.
|
||||||
|
|
||||||
|
A patent license is “discriminatory” if it does not include within the
|
||||||
|
scope of its coverage, prohibits the exercise of, or is conditioned on the
|
||||||
|
non-exercise of one or more of the rights that are specifically granted under this
|
||||||
|
License. You may not convey a covered work if you are a party to an arrangement with
|
||||||
|
a third party that is in the business of distributing software, under which you make
|
||||||
|
payment to the third party based on the extent of your activity of conveying the
|
||||||
|
work, and under which the third party grants, to any of the parties who would receive
|
||||||
|
the covered work from you, a discriminatory patent license **(a)** in connection with
|
||||||
|
copies of the covered work conveyed by you (or copies made from those copies), or **(b)**
|
||||||
|
primarily for and in connection with specific products or compilations that contain
|
||||||
|
the covered work, unless you entered into that arrangement, or that patent license
|
||||||
|
was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting any implied
|
||||||
|
license or other defenses to infringement that may otherwise be available to you
|
||||||
|
under applicable patent law.
|
||||||
|
|
||||||
|
### 12. No Surrender of Others' Freedom
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or otherwise)
|
||||||
|
that contradict the conditions of this License, they do not excuse you from the
|
||||||
|
conditions of this License. If you cannot convey a covered work so as to satisfy
|
||||||
|
simultaneously your obligations under this License and any other pertinent
|
||||||
|
obligations, then as a consequence you may not convey it at all. For example, if you
|
||||||
|
agree to terms that obligate you to collect a royalty for further conveying from
|
||||||
|
those to whom you convey the Program, the only way you could satisfy both those terms
|
||||||
|
and this License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
### 13. Use with the GNU Affero General Public License
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have permission to link or
|
||||||
|
combine any covered work with a work licensed under version 3 of the GNU Affero
|
||||||
|
General Public License into a single combined work, and to convey the resulting work.
|
||||||
|
The terms of this License will continue to apply to the part which is the covered
|
||||||
|
work, but the special requirements of the GNU Affero General Public License, section
|
||||||
|
13, concerning interaction through a network will apply to the combination as such.
|
||||||
|
|
||||||
|
### 14. Revised Versions of this License
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of the GNU
|
||||||
|
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 Program specifies that
|
||||||
|
a certain numbered version of the GNU General Public License “or any later
|
||||||
|
version” applies to it, you have the option of following the terms and
|
||||||
|
conditions either of that numbered version or of any later version published by the
|
||||||
|
Free Software Foundation. If the Program does not specify a version number of the GNU
|
||||||
|
General Public License, you may choose any version ever published by the Free
|
||||||
|
Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future versions of the GNU
|
||||||
|
General Public License can be used, that proxy's public statement of acceptance of a
|
||||||
|
version permanently authorizes you to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different permissions. However, no
|
||||||
|
additional obligations are imposed on any author or copyright holder as a result of
|
||||||
|
your choosing to follow a later version.
|
||||||
|
|
||||||
|
### 15. Disclaimer of Warranty
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||||
|
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
|
||||||
|
QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
|
||||||
|
DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
### 16. Limitation of Liability
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
|
||||||
|
COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
|
||||||
|
PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
|
||||||
|
INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE
|
||||||
|
OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE
|
||||||
|
WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
### 17. Interpretation of Sections 15 and 16
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided above cannot be
|
||||||
|
given local legal effect according to their terms, reviewing courts shall apply local
|
||||||
|
law that most closely approximates an absolute waiver of all civil liability in
|
||||||
|
connection with the Program, unless a warranty or assumption of liability accompanies
|
||||||
|
a copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
_END OF TERMS AND CONDITIONS_
|
||||||
|
|
||||||
|
## How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to
|
||||||
|
the public, the best way to achieve this is to make it free software which everyone
|
||||||
|
can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them
|
||||||
|
to the start of each source file to most effectively state the exclusion of warranty;
|
||||||
|
and each file should have at least the “copyright” line and a pointer to
|
||||||
|
where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type 'show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type 'show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w` and `show c` should show the appropriate parts of
|
||||||
|
the General Public License. Of course, your program's commands might be different;
|
||||||
|
for a GUI interface, you would use an “about box”.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school, if any, to
|
||||||
|
sign a “copyright disclaimer” for the program, if necessary. For more
|
||||||
|
information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<<http://www.gnu.org/licenses/>>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may consider it
|
||||||
|
more useful to permit linking proprietary applications with the library. If this is
|
||||||
|
what you want to do, use the GNU Lesser General Public License instead of this
|
||||||
|
License. But first, please read
|
||||||
|
<<http://www.gnu.org/philosophy/why-not-lgpl.html>>.
|
23
Makefile
Executable file
23
Makefile
Executable file
|
@ -0,0 +1,23 @@
|
||||||
|
.PHONY: build clean dist
|
||||||
|
|
||||||
|
BUILD_DATE := $(shell date +'%Y%m%d')
|
||||||
|
VERSION := $(shell support/get-version.sh)
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o target/gomicsv \
|
||||||
|
-ldflags="-X main.buildDate=$(BUILD_DATE) -X main.versionString=$(VERSION)" \
|
||||||
|
cmd/gomicsv/main.go
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf target/; \
|
||||||
|
|
||||||
|
dist: build
|
||||||
|
rm -rf target/dist; \
|
||||||
|
rm -rf target/*.zip; \
|
||||||
|
mkdir -p target/dist/gomicsv; \
|
||||||
|
cp target/gomicsv target/dist/gomicsv/gomicsv; \
|
||||||
|
cp README.md target/dist/gomicsv; \
|
||||||
|
cp COPYING.md target/dist/gomicsv; \
|
||||||
|
pushd target/dist; \
|
||||||
|
zip -r ../Gomicsv_$(VERSION).zip gomicsv; \
|
||||||
|
rm -rf gomicsv; \
|
115
README.md
Normal file
115
README.md
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<!-- vim: set textwidth=80 colorcolumn=80: -->
|
||||||
|
<!-- markdownlint-configure-file
|
||||||
|
{
|
||||||
|
"no-inline-html": false
|
||||||
|
}
|
||||||
|
-->
|
||||||
|
# Gomics-v
|
||||||
|
|
||||||
|
> A GTK comic viewer
|
||||||
|
|
||||||
|
Gomics-v is a GTK comic and image archive viewer written in Go, available under
|
||||||
|
the GNU GPL v3 or later.
|
||||||
|
|
||||||
|
**This is a personal fork of *Gomics* (without the *-v*). Please also consult the
|
||||||
|
main project’s README at <https://github.com/salviati/gomics>**.
|
||||||
|
|
||||||
|
This fork has been made solely with personal use for reading manga with
|
||||||
|
mouse-centered navigation in mind. Consequently, some aspects of other workflows
|
||||||
|
might have been broken.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="screenshot.png" title="A screenshot of Gomics-v">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Table of contents
|
||||||
|
|
||||||
|
1. [Changes from Gomics](#changes-from-gomics)
|
||||||
|
2. [Installation](#installation)
|
||||||
|
3. [Building](#installation)
|
||||||
|
4. [License](#license)
|
||||||
|
|
||||||
|
## Changes from Gomics
|
||||||
|
|
||||||
|
* Add **Jumpmarks**
|
||||||
|
|
||||||
|
Temporary page marks for quickly jumping around the currently open archive.
|
||||||
|
See the `Jumpmarks` menu in the menu bar for more information.
|
||||||
|
|
||||||
|
* Add *experimental* **HTTP archive support**
|
||||||
|
|
||||||
|
See `File › Open URL (experimental)` for a detailed explanation.
|
||||||
|
A HTTP archive can alternatively be loaded by providing the URL as a program
|
||||||
|
argument (see `./gomicsv --help`) or by pasting the URL into the program.
|
||||||
|
|
||||||
|
* **Rework page navigation**
|
||||||
|
|
||||||
|
* Click the left/right half of the image area to go to the previous/next page.
|
||||||
|
|
||||||
|
* Drag with the middle mouse button to pan around the image.
|
||||||
|
|
||||||
|
* Add integration with **[Kamite]** (a desktop Japanese immersion companion)
|
||||||
|
|
||||||
|
Right-click on text block for automatic OCR. Right-hold-click to initialize
|
||||||
|
manual block recognition selection. *Must be first enabled in Preferences.*
|
||||||
|
|
||||||
|
* Add **Remember reading position**
|
||||||
|
|
||||||
|
Automatically saves the last reading position for each archive and resumes
|
||||||
|
reading at that position. Can be disabled in Preferences.
|
||||||
|
|
||||||
|
* Add **Save image as…**
|
||||||
|
|
||||||
|
* Add **Copy image to clipboard**
|
||||||
|
|
||||||
|
* Add **Hide UI** (`Alt+M` to hide/unhide).
|
||||||
|
|
||||||
|
* Polish the look of preferences UI.
|
||||||
|
|
||||||
|
* Make the Background color preference functional.
|
||||||
|
|
||||||
|
* Reorganize the codebase.
|
||||||
|
|
||||||
|
* Various other minor tweaks (and breakages).
|
||||||
|
|
||||||
|
[Kamite]: https://github.com/fauu/Kamite
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Download the latest release package from the [Releases] page and extract it to
|
||||||
|
the location where you want to keep the program files (e.g., `/opt/gomicsv`).
|
||||||
|
|
||||||
|
Or build it from the source:
|
||||||
|
|
||||||
|
[Releases]: https://github.com/fauu/gomicsv/releases
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone "https://github.com/fauu/gomicsv"
|
||||||
|
cd gomicsv
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
A self-contained Gomics-v executable will be produced at `target/gomicsv`.
|
||||||
|
|
||||||
|
Building requires [go] and some GTK-related dependencies. See
|
||||||
|
[gomics: Requirements][gomics-requirements]. GTK-related build steps might take
|
||||||
|
up to 15 minutes on first compile.
|
||||||
|
|
||||||
|
[go]: https://go.dev/
|
||||||
|
[gomics-requirements]: https://github.com/salviati/gomics#requirements
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify it under
|
||||||
|
the terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
this program. If not, see <https://www.gnu.org/licenses/>.
|
259
app.go
Normal file
259
app.go
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/archive"
|
||||||
|
"github.com/fauu/gomicsv/imgdiff"
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppName = "gomicsv"
|
||||||
|
AppNameDisplay = "Gomics-v"
|
||||||
|
AppID = "com.github.fauu.gomicsv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type App struct {
|
||||||
|
S State
|
||||||
|
W Widgets
|
||||||
|
Config Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
BuildInfo BuildInfo
|
||||||
|
GTKApplication *gtk.Application
|
||||||
|
Archive archive.Archive
|
||||||
|
ArchivePos int
|
||||||
|
ArchivePath string
|
||||||
|
ArchiveName string
|
||||||
|
PixbufL, PixbufR *gdk.Pixbuf
|
||||||
|
GoToThumbPixbuf *gdk.Pixbuf
|
||||||
|
Scale float64
|
||||||
|
PageCache *pagecache.PageCache
|
||||||
|
ConfigDirPath string
|
||||||
|
UserDataDirPath string
|
||||||
|
ReadLaterDirPath string
|
||||||
|
ImageHashes map[int]imgdiff.Hash
|
||||||
|
Jumpmarks Jumpmarks
|
||||||
|
Cursor CursorsState
|
||||||
|
DragScroll DragScroll
|
||||||
|
SmartScrollInProgress bool
|
||||||
|
KamiteRightClickActionPending bool
|
||||||
|
RecentManager *gtk.RecentManager
|
||||||
|
BackgroundColorCssProvider *gtk.CssProvider
|
||||||
|
PageCacheTrimTimeoutHandle *glib.SourceHandle
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed about.jpg
|
||||||
|
var aboutImg []byte
|
||||||
|
|
||||||
|
//go:embed icon.png
|
||||||
|
var iconImg []byte
|
||||||
|
|
||||||
|
//go:embed gomicsv.ui
|
||||||
|
var uiDef string
|
||||||
|
|
||||||
|
type AppStartupParams struct {
|
||||||
|
Referer string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) Init(nonFlagArgs []string, startupParams AppStartupParams, buildInfo BuildInfo) *gtk.Application {
|
||||||
|
application, err := gtk.ApplicationNew(AppID, glib.APPLICATION_HANDLES_OPEN)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("creating GTK Aplication: %v", err)
|
||||||
|
}
|
||||||
|
glib.SetPrgname(AppID)
|
||||||
|
|
||||||
|
app.S.BuildInfo = buildInfo
|
||||||
|
|
||||||
|
application.Connect("startup", func(self *gtk.Application) {
|
||||||
|
app.ensureDirs()
|
||||||
|
|
||||||
|
app.loadConfig()
|
||||||
|
|
||||||
|
app.S.RecentManager, err = gtk.RecentManagerGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting default RecentManager: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.uiInit()
|
||||||
|
|
||||||
|
app.syncStateToConfig()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Runs after startup if no files were provided
|
||||||
|
application.Connect("activate", func(_ *gtk.Application) {
|
||||||
|
// Do nothing
|
||||||
|
})
|
||||||
|
|
||||||
|
// Runs after startup if some files were provided
|
||||||
|
application.Connect("open", func(_ *gtk.Application, filesPtr unsafe.Pointer, _count int, _ string) {
|
||||||
|
// Get first file's path and load it, ignoring the rest
|
||||||
|
offset := uintptr(0) // FUTURE: To get the i-th file pointer, set this to `i * unsafe.Sizeof(uintptr(0))`
|
||||||
|
ptr := (*unsafe.Pointer)(unsafe.Pointer(uintptr(filesPtr) + offset))
|
||||||
|
file := &glib.File{
|
||||||
|
Object: &glib.Object{
|
||||||
|
GObject: glib.ToGObject(*ptr),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
path := file.GetPath()
|
||||||
|
// `path` is empty when an URL is passed. In such case, try to get the URL directly from `args`
|
||||||
|
if path == "" && len(nonFlagArgs) >= 2 && util.IsLikelyHTTPURL(nonFlagArgs[1]) {
|
||||||
|
app.loadArchiveFromURL(nonFlagArgs[1], startupParams.Referer)
|
||||||
|
} else {
|
||||||
|
app.loadArchiveFromPath(path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.S.GTKApplication = application
|
||||||
|
|
||||||
|
return application
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) ensureDirs() {
|
||||||
|
configPath, err := getConfigLocation(AppName)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting config location: %v", err)
|
||||||
|
}
|
||||||
|
userDataPath, err := getUserDataLocation(AppName)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting user data location: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.ConfigDirPath = configPath
|
||||||
|
app.S.UserDataDirPath = userDataPath
|
||||||
|
app.S.ReadLaterDirPath = filepath.Join(userDataPath, ReadLaterDir)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(app.S.ConfigDirPath, 0755); err != nil {
|
||||||
|
log.Panicf("creting config directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(app.S.UserDataDirPath, 0755); err != nil {
|
||||||
|
log.Panicf("creating user data directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(app.S.ReadLaterDirPath, 0755); err != nil {
|
||||||
|
log.Panicf("creating read later directory: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) syncStateToConfig() {
|
||||||
|
app.setFullscreen(app.Config.Fullscreen)
|
||||||
|
app.setHideUI(app.Config.HideUI)
|
||||||
|
app.setZoomMode(app.Config.ZoomMode)
|
||||||
|
app.setDoublePage(app.Config.DoublePage)
|
||||||
|
app.setMangaMode(app.Config.MangaMode)
|
||||||
|
app.setBackgroundColor(app.Config.BackgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) handleKeyPress(key uint, shift bool, ctrl bool) {
|
||||||
|
switch key {
|
||||||
|
case gdk.KEY_v:
|
||||||
|
if ctrl {
|
||||||
|
app.maybeLoadArchiveFromClipboardURL()
|
||||||
|
}
|
||||||
|
case gdk.KEY_Down:
|
||||||
|
if ctrl {
|
||||||
|
app.nextArchive()
|
||||||
|
} else if shift {
|
||||||
|
app.scroll(0, 1)
|
||||||
|
} else {
|
||||||
|
app.skipForward()
|
||||||
|
}
|
||||||
|
case gdk.KEY_Up:
|
||||||
|
if ctrl {
|
||||||
|
app.previousArchive()
|
||||||
|
} else if shift {
|
||||||
|
app.scroll(0, -1)
|
||||||
|
} else {
|
||||||
|
app.skipBackward()
|
||||||
|
}
|
||||||
|
case gdk.KEY_Right:
|
||||||
|
if ctrl {
|
||||||
|
app.nextScene()
|
||||||
|
} else if shift {
|
||||||
|
app.scroll(1, 0)
|
||||||
|
} else {
|
||||||
|
app.nextPage()
|
||||||
|
}
|
||||||
|
case gdk.KEY_Left:
|
||||||
|
if ctrl {
|
||||||
|
app.previousScene()
|
||||||
|
} else if shift {
|
||||||
|
app.scroll(-1, 0)
|
||||||
|
} else {
|
||||||
|
app.previousPage()
|
||||||
|
}
|
||||||
|
case gdk.KEY_space:
|
||||||
|
if ctrl {
|
||||||
|
app.W.MenuItemPreviousPage.Activate()
|
||||||
|
} else {
|
||||||
|
app.W.MenuItemNextPage.Activate()
|
||||||
|
}
|
||||||
|
case gdk.KEY_F11:
|
||||||
|
app.W.MenuItemFullscreen.Activate()
|
||||||
|
case gdk.KEY_KP_Home:
|
||||||
|
app.W.MenuItemFirstPage.Activate()
|
||||||
|
case gdk.KEY_KP_End:
|
||||||
|
app.W.MenuItemLastPage.Activate()
|
||||||
|
case gdk.KEY_KP_Page_Up:
|
||||||
|
if ctrl {
|
||||||
|
app.W.MenuItemPreviousArchive.Activate()
|
||||||
|
} else {
|
||||||
|
app.W.MenuItemPreviousPage.Activate()
|
||||||
|
}
|
||||||
|
case gdk.KEY_KP_Next:
|
||||||
|
if ctrl {
|
||||||
|
app.W.MenuItemNextArchive.Activate()
|
||||||
|
} else {
|
||||||
|
app.W.MenuItemNextPage.Activate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setStatus(msg string) {
|
||||||
|
contextID := app.W.Statusbar.GetContextId("main")
|
||||||
|
app.W.Statusbar.Push(contextID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) showError(msg string) {
|
||||||
|
app.notificationShow(fmt.Sprintf("Error: %s", msg), LongNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) quit() {
|
||||||
|
app.Config.WindowWidth, app.Config.WindowHeight = app.W.MainWindow.GetSize()
|
||||||
|
|
||||||
|
app.saveConfig()
|
||||||
|
app.maybeSaveReadingPosition()
|
||||||
|
|
||||||
|
app.S.GTKApplication.Quit()
|
||||||
|
}
|
201
archive.go
Normal file
201
archive.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/archive"
|
||||||
|
"github.com/fauu/gomicsv/imgdiff"
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) loadArchiveFromURL(url string, httpReferer string) {
|
||||||
|
app.doLoadArchive(url, true, httpReferer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) loadArchiveFromPath(path string) {
|
||||||
|
app.doLoadArchive(path, false, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) doLoadArchive(path string, assumeHTTPURL bool, httpReferer string) {
|
||||||
|
if strings.TrimSpace(path) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if assumeHTTPURL && !util.IsLikelyHTTPURL(path) {
|
||||||
|
// For cases when a non-fully qualified URL is provided
|
||||||
|
path = "https://" + path
|
||||||
|
}
|
||||||
|
|
||||||
|
if !assumeHTTPURL && !filepath.IsAbs(path) {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting current working directory: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path = filepath.Join(wd, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.archiveIsLoaded() {
|
||||||
|
app.archiveClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.ImageHashes = make(map[int]imgdiff.Hash)
|
||||||
|
|
||||||
|
app.S.ArchivePath = path
|
||||||
|
if assumeHTTPURL {
|
||||||
|
app.S.ArchiveName = path
|
||||||
|
} else {
|
||||||
|
app.S.ArchiveName = filepath.Base(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
cache := pagecache.NewPageCache()
|
||||||
|
app.S.PageCache = &cache
|
||||||
|
interval := uint(pageCacheTrimInterval.Seconds())
|
||||||
|
handle := glib.TimeoutSecondsAdd(interval, app.trimPageCache)
|
||||||
|
app.S.PageCacheTrimTimeoutHandle = &handle
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if app.S.Archive, err = archive.NewArchive(path, &cache, httpReferer); err != nil {
|
||||||
|
app.showError(fmt.Sprintf("Couldn't open %s: %v", path, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.archiveHandleLenKnowledge(app.S.Archive.Len() != nil)
|
||||||
|
|
||||||
|
app.W.ButtonNextArchive.SetSensitive(!assumeHTTPURL)
|
||||||
|
app.W.ButtonPreviousArchive.SetSensitive(!assumeHTTPURL)
|
||||||
|
|
||||||
|
app.W.MenuItemCopyImageToClipboard.SetSensitive(true)
|
||||||
|
|
||||||
|
startPage := 0
|
||||||
|
if (!assumeHTTPURL && app.Config.RememberPosition) || (assumeHTTPURL && app.Config.RememberPositionHTTP) {
|
||||||
|
savedArchivePos, err := app.loadReadingPosition(path)
|
||||||
|
if err == nil {
|
||||||
|
startPage = savedArchivePos
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
log.Printf("Error loading reading position: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.doSetPage(startPage)
|
||||||
|
|
||||||
|
if !assumeHTTPURL {
|
||||||
|
err := os.Chdir(app.S.ArchivePath) // UNCLEAR(fau): Is this for relative paths to work or something?
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Couldn't chdir into archive path")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.RememberRecent {
|
||||||
|
u := &url.URL{Path: path, Scheme: "file"}
|
||||||
|
ok := app.S.RecentManager.AddItem(u.String())
|
||||||
|
if !ok {
|
||||||
|
log.Printf("Couldn't add %s as a recent item", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) archiveIsLoaded() bool {
|
||||||
|
return app.S.Archive != nil && !reflect.ValueOf(app.S.Archive).IsNil() && app.S.ArchivePath != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) archiveGetBaseName() string {
|
||||||
|
var name string
|
||||||
|
if app.S.Archive.Kind() == archive.HTTPKind {
|
||||||
|
name = app.S.ArchivePath
|
||||||
|
} else {
|
||||||
|
name = filepath.Base(app.S.ArchivePath)
|
||||||
|
if ext := filepath.Ext(name); len(ext) > 1 {
|
||||||
|
name = strings.TrimSuffix(name, ext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) archiveClose() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.maybeSaveReadingPosition()
|
||||||
|
|
||||||
|
app.S.Archive.Close()
|
||||||
|
|
||||||
|
app.S.Archive = nil
|
||||||
|
app.S.ArchiveName = ""
|
||||||
|
app.S.ArchivePath = ""
|
||||||
|
app.S.ArchivePos = 0
|
||||||
|
|
||||||
|
app.S.PageCache = nil
|
||||||
|
|
||||||
|
app.S.ImageHashes = nil
|
||||||
|
|
||||||
|
app.clearJumpmarks()
|
||||||
|
|
||||||
|
// Cancel page cache trim timeout
|
||||||
|
if app.S.PageCacheTrimTimeoutHandle != nil {
|
||||||
|
glib.SourceRemove(*app.S.PageCacheTrimTimeoutHandle)
|
||||||
|
app.S.PageCacheTrimTimeoutHandle = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
app.W.ImageL.Clear()
|
||||||
|
app.W.ImageR.Clear()
|
||||||
|
app.S.PixbufL = nil
|
||||||
|
app.S.PixbufR = nil
|
||||||
|
app.S.Cursor.reset()
|
||||||
|
app.W.MenuItemCopyImageToClipboard.SetSensitive(false)
|
||||||
|
app.setStatus("")
|
||||||
|
app.W.MainWindow.SetTitle(AppNameDisplay)
|
||||||
|
|
||||||
|
util.GC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) archiveHandleLenKnowledge(known bool) {
|
||||||
|
app.W.ButtonLastPage.SetSensitive(known)
|
||||||
|
app.W.MenuItemLastPage.SetSensitive(known)
|
||||||
|
app.W.MenuItemRandom.SetSensitive(known)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) maybeLoadArchiveFromClipboardURL() {
|
||||||
|
clipboard, err := gtk.ClipboardGet(gdk.GdkAtomIntern("CLIPBOARD", true))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting clipboard: %v", err)
|
||||||
|
} else {
|
||||||
|
text, err := clipboard.WaitForText()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting clipboard text: %v", err)
|
||||||
|
} else {
|
||||||
|
if util.IsLikelyHTTPURL(text) {
|
||||||
|
app.loadArchiveFromURL(text, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
archive/archive.go
Normal file
71
archive/archive.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrBounds = errors.New("Image index out of bounds.")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Archive interface {
|
||||||
|
Load(i int, autorotate bool, nPreload int) (*gdk.Pixbuf, error)
|
||||||
|
Kind() Kind
|
||||||
|
Name(i int) (string, error)
|
||||||
|
Len() *int // nil represents unknown length
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxArchiveEntries = 4096 * 64
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewArchive(path string, pageCache *pagecache.PageCache, httpReferer string) (Archive, error) {
|
||||||
|
if util.IsLikelyHTTPURL(path) {
|
||||||
|
return NewHTTP(path, pageCache, httpReferer)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.IsDir() {
|
||||||
|
return NewDir(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(path))[1:]
|
||||||
|
switch ext {
|
||||||
|
case "zip", "cbz":
|
||||||
|
return NewZip(path)
|
||||||
|
case "7z", "rar", "tar", "tgz", "gz", "tbz2", "cb7", "cbr", "cbt", "lha":
|
||||||
|
return nil, errors.New("Archive type not supported, please unpack it first")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("Unknown archive type")
|
||||||
|
}
|
115
archive/dir.go
Normal file
115
archive/dir.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pixbuf"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dir struct {
|
||||||
|
filenames filenames
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reads filenames from a directory, and sorts them */
|
||||||
|
func NewDir(path string) (*Dir, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ar := new(Dir)
|
||||||
|
|
||||||
|
ar.name = filepath.Base(path)
|
||||||
|
ar.path = path
|
||||||
|
|
||||||
|
dir, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
filenames, err := dir.Readdirnames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ar.filenames = make([]string, 0, len(filenames))
|
||||||
|
|
||||||
|
for _, name := range filenames {
|
||||||
|
if !extensionMatches(name, imageExtensions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ar.filenames = append(ar.filenames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ar.filenames) == 0 {
|
||||||
|
return nil, errors.New(ar.name + ": no images in the directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(ar.filenames)
|
||||||
|
|
||||||
|
return ar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) checkbounds(i int) error {
|
||||||
|
if i < 0 || i >= len(ar.filenames) {
|
||||||
|
return ErrBounds
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) Load(i int, autorotate bool, _nPreload int) (*gdk.Pixbuf, error) {
|
||||||
|
if err := ar.checkbounds(i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(ar.path, ar.filenames[i])
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
return pixbuf.Load(f, autorotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) Name(i int) (string, error) {
|
||||||
|
if err := ar.checkbounds(i); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ar.filenames[i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) Kind() Kind {
|
||||||
|
return Unpacked
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) Len() *int {
|
||||||
|
l := len(ar.filenames)
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Dir) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
261
archive/http.go
Normal file
261
archive/http.go
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/pixbuf"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTP struct {
|
||||||
|
urlTemplate string
|
||||||
|
referer string
|
||||||
|
firstPageOffset int
|
||||||
|
pageCache *pagecache.PageCache
|
||||||
|
fetchInProgress map[int]bool
|
||||||
|
fetchInProgressMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTP(url string, pageCache *pagecache.PageCache, referer string) (*HTTP, error) {
|
||||||
|
if !isPageURLTemplate(url) {
|
||||||
|
var ok bool
|
||||||
|
url, ok = tryMakeURLTemplateFromSampleURL(url)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("Couldn't determine the URL template from the sample URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newHTTP := HTTP{
|
||||||
|
urlTemplate: url,
|
||||||
|
referer: referer,
|
||||||
|
pageCache: pageCache,
|
||||||
|
fetchInProgress: make(map[int]bool),
|
||||||
|
fetchInProgressMutex: sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
firstPageIdx := 0
|
||||||
|
var firstPixbuf *gdk.Pixbuf
|
||||||
|
for firstPageIdx < 2 {
|
||||||
|
var err error
|
||||||
|
firstPixbuf, err = newHTTP.downloadImage(firstPageIdx, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("First image not located at index %d", firstPageIdx)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
firstPageIdx++
|
||||||
|
}
|
||||||
|
if firstPixbuf == nil {
|
||||||
|
return nil, errors.New("Couldn't locate the first image")
|
||||||
|
}
|
||||||
|
|
||||||
|
newHTTP.firstPageOffset = firstPageIdx
|
||||||
|
pageCache.Insert(0, firstPixbuf, pagecache.KeepReasonPreload)
|
||||||
|
|
||||||
|
return &newHTTP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) Load(i int, autorotate bool, nPreload int) (*gdk.Pixbuf, error) {
|
||||||
|
var pixbuf *gdk.Pixbuf
|
||||||
|
var err error
|
||||||
|
cached, isCached := ar.pageCache.Get(i)
|
||||||
|
if !isCached {
|
||||||
|
if downloading := ar.getAndSetPageFetchInProgress(i, true); downloading {
|
||||||
|
// Wait until downloading done
|
||||||
|
var tries = 10
|
||||||
|
for range time.Tick(time.Millisecond * 500) {
|
||||||
|
cached, isCached = ar.pageCache.Get(i)
|
||||||
|
if isCached {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if tries <= 0 {
|
||||||
|
return nil, fmt.Errorf("Ran out of tries when waiting for page %d to download", i)
|
||||||
|
}
|
||||||
|
tries--
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pixbuf, err = ar.downloadImage(i+ar.firstPageOffset, autorotate)
|
||||||
|
ar.setPageFetchInProgress(i, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pixbuf == nil {
|
||||||
|
pixbuf = cached.Pixbuf
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
pixbuf, err = pixbuf.ApplyEmbeddedOrientation()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preloadStart := i - nPreload
|
||||||
|
preloadEnd := i + nPreload
|
||||||
|
for j := preloadStart; j <= preloadEnd; j++ {
|
||||||
|
if j < 0 || j == i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := ar.pageCache.Get(j); !ok {
|
||||||
|
if downloading := ar.getAndSetPageFetchInProgress(j, true); !downloading {
|
||||||
|
go func(k int) {
|
||||||
|
pixbuf, err = ar.downloadImage(k+ar.firstPageOffset, autorotate)
|
||||||
|
ar.setPageFetchInProgress(k, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't preload image: %v", err)
|
||||||
|
}
|
||||||
|
ar.pageCache.Insert(k, pixbuf, pagecache.KeepReasonPreload)
|
||||||
|
}(j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isCached {
|
||||||
|
ar.pageCache.Insert(i, pixbuf, pagecache.KeepReasonPreload)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) Kind() Kind {
|
||||||
|
return HTTPKind
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) Name(i int) (string, error) {
|
||||||
|
return ar.urlTemplate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) Len() *int {
|
||||||
|
// Length unknown
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) getAndSetPageFetchInProgress(i int, value bool) bool {
|
||||||
|
ar.fetchInProgressMutex.Lock()
|
||||||
|
defer ar.fetchInProgressMutex.Unlock()
|
||||||
|
_, ok := ar.fetchInProgress[i]
|
||||||
|
ar.doSetPageFetchInProgress(i, value)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) setPageFetchInProgress(i int, value bool) {
|
||||||
|
ar.fetchInProgressMutex.Lock()
|
||||||
|
defer ar.fetchInProgressMutex.Unlock()
|
||||||
|
ar.doSetPageFetchInProgress(i, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) doSetPageFetchInProgress(i int, value bool) {
|
||||||
|
if value {
|
||||||
|
ar.fetchInProgress[i] = true
|
||||||
|
} else {
|
||||||
|
delete(ar.fetchInProgress, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *HTTP) downloadImage(i int, autorotate bool) (*gdk.Pixbuf, error) {
|
||||||
|
url := fmt.Sprintf(ar.urlTemplate, i)
|
||||||
|
headers := map[string]string{"User-Agent": userAgent}
|
||||||
|
if ar.referer != "" {
|
||||||
|
headers["Referer"] = ar.referer
|
||||||
|
}
|
||||||
|
res, err := httpGet(url, headers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
pixbuf, err := pixbuf.Load(res.Body, autorotate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pagePlaceholderRegexp = regexp.MustCompile(`%\d{0,2}d`)
|
||||||
|
|
||||||
|
func isPageURLTemplate(s string) bool {
|
||||||
|
return pagePlaceholderRegexp.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageParamRegexp = regexp.MustCompile(`(?:\/(\d{1,3})(?:\/|[^\d\w]))|(?:(\d{1,3})\.(?:jp|png|gif))`)
|
||||||
|
|
||||||
|
func tryMakeURLTemplateFromSampleURL(url string) (template string, ok bool) {
|
||||||
|
matches := pageParamRegexp.FindAllStringSubmatchIndex(url, -1)
|
||||||
|
if len(matches) < 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
lastMatch := matches[len(matches)-1]
|
||||||
|
if len(lastMatch) <= 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for i := 2; i < len(lastMatch); i += 2 {
|
||||||
|
if lastMatch[i] != -1 {
|
||||||
|
start, end := lastMatch[i], lastMatch[i+1]
|
||||||
|
return url[:start] + "%d" + url[end:], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpClient = &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpGet(reqURL string, headers map[string]string) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest("GET", reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating request: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range headers {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("GET %s", reqURL)
|
||||||
|
res, err := httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("performing request: %v", err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
log.Printf("Got status code: %d %s", res.StatusCode, res.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
27
archive/kind.go
Normal file
27
archive/kind.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
type Kind = int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Packed Kind = iota
|
||||||
|
Unpacked
|
||||||
|
HTTPKind
|
||||||
|
)
|
173
archive/util.go
Normal file
173
archive/util.go
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/natsort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Loader interface {
|
||||||
|
Load(i int) (*gdk.Pixbuf, error)
|
||||||
|
Name(i int) (string, error)
|
||||||
|
Len() int
|
||||||
|
}
|
||||||
|
|
||||||
|
var archiveExtensions = []string{".zip", ".cbz"}
|
||||||
|
var imageExtensions []string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
imageExtensions = make([]string, 0)
|
||||||
|
formats := gdk.PixbufGetFormats()
|
||||||
|
for _, format := range formats {
|
||||||
|
imageExtensions = append(imageExtensions, format.GetExtensions()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range imageExtensions {
|
||||||
|
imageExtensions[i] = "." + imageExtensions[i] // gdk pixbuf format extensions don't have the leading "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extensionMatches(p string, extensions []string) bool {
|
||||||
|
pext := strings.ToLower(filepath.Ext(p))
|
||||||
|
for _, ext := range extensions {
|
||||||
|
if pext == ext {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringArray []string
|
||||||
|
|
||||||
|
func (p stringArray) Len() int { return len(p) }
|
||||||
|
func (p stringArray) Less(i, j int) bool { return strings.ToLower(p[i]) < strings.ToLower(p[j]) }
|
||||||
|
func (p stringArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
|
||||||
|
func ListInDirectory(dir string) (anames []string, err error) {
|
||||||
|
file, err := os.Open(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fi, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fi.IsDir() {
|
||||||
|
err = errors.New(dir + " is not a directory!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
names, err := file.Readdirnames(-1)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
anames = make([]string, 0, len(names))
|
||||||
|
for _, name := range names {
|
||||||
|
var fi os.FileInfo
|
||||||
|
fi, err = os.Stat(filepath.Join(dir, name))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !extensionMatches(name, archiveExtensions) && !fi.IsDir() {
|
||||||
|
// TODO(utkan): Don't add empty archives
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
anames = append(anames, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(stringArray(anames)) // TODO(utkan): Can use natsort for archives as well
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
*os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFile(f *os.File) *File {
|
||||||
|
return &File{f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) Size() (int64, error) {
|
||||||
|
fi, err := r.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return fi.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) SetSize(n int64) error {
|
||||||
|
return r.Truncate(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *File) Ext() string {
|
||||||
|
ext := filepath.Ext(r.Name())
|
||||||
|
if len(ext) <= 1 || ext[0] != '.' {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return ext[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Buffer struct {
|
||||||
|
bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuffer(data []byte) *Buffer {
|
||||||
|
return &Buffer{*bytes.NewBuffer(data)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
return offset, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) SetSize(int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Buffer) Size() (int64, error) {
|
||||||
|
return int64(b.Len()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func strcmp(a, b string, nat bool) bool {
|
||||||
|
if nat {
|
||||||
|
return natsort.Less(a, b)
|
||||||
|
}
|
||||||
|
return a < b
|
||||||
|
}
|
||||||
|
|
||||||
|
type filenames []string
|
||||||
|
|
||||||
|
func (p filenames) Len() int { return len(p) }
|
||||||
|
func (p filenames) Less(i, j int) bool { return strcmp(p[i], p[j], true) }
|
||||||
|
func (p filenames) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
112
archive/zip.go
Normal file
112
archive/zip.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"errors"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pixbuf"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Zip struct {
|
||||||
|
files []*zip.File // File elements sorted by their Names
|
||||||
|
reader *zip.ReadCloser
|
||||||
|
name string // Name of the Zip file
|
||||||
|
}
|
||||||
|
|
||||||
|
type zipfile []*zip.File
|
||||||
|
|
||||||
|
func (p zipfile) Len() int { return len(p) }
|
||||||
|
func (p zipfile) Less(i, j int) bool { return strcmp(p[i].Name, p[j].Name, true) }
|
||||||
|
func (p zipfile) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||||
|
|
||||||
|
// NewZip reads filenames from a given zip archive and sorts them
|
||||||
|
func NewZip(name string) (*Zip, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ar := new(Zip)
|
||||||
|
|
||||||
|
ar.name = filepath.Base(name)
|
||||||
|
ar.files = make([]*zip.File, 0, MaxArchiveEntries)
|
||||||
|
ar.reader, err = zip.OpenReader(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range ar.reader.File {
|
||||||
|
if !extensionMatches(f.Name, imageExtensions) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ar.files = append(ar.files, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ar.files) == 0 {
|
||||||
|
return nil, errors.New(ar.name + ": no images in the zip file")
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(zipfile(ar.files))
|
||||||
|
|
||||||
|
return ar, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) checkbounds(i int) error {
|
||||||
|
if i < 0 || i >= len(ar.files) {
|
||||||
|
return ErrBounds
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) Load(i int, autorotate bool, _nPreload int) (*gdk.Pixbuf, error) {
|
||||||
|
if err := ar.checkbounds(i); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := ar.files[i].Open()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
return pixbuf.Load(f, autorotate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) Kind() Kind {
|
||||||
|
return Packed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) Name(i int) (string, error) {
|
||||||
|
if err := ar.checkbounds(i); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ar.files[i].Name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) Len() *int {
|
||||||
|
l := len(ar.files)
|
||||||
|
return &l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ar *Zip) Close() error {
|
||||||
|
return ar.reader.Close()
|
||||||
|
}
|
92
bookmarks.go
Normal file
92
bookmarks.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var bookmarkMenuItems []*gtk.MenuItem
|
||||||
|
|
||||||
|
type Bookmark struct {
|
||||||
|
Path string
|
||||||
|
Page uint
|
||||||
|
TotalPages *uint
|
||||||
|
Added time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) addBookmark() {
|
||||||
|
defer app.rebuildBookmarksMenu()
|
||||||
|
|
||||||
|
for i := range app.Config.Bookmarks {
|
||||||
|
b := &app.Config.Bookmarks[i]
|
||||||
|
if b.Path == app.S.ArchivePath {
|
||||||
|
b.Page = uint(app.S.ArchivePos + 1)
|
||||||
|
b.TotalPages = util.IntPtrToUintPtr(app.S.Archive.Len())
|
||||||
|
b.Added = time.Now()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Config.Bookmarks = append(app.Config.Bookmarks, Bookmark{
|
||||||
|
Path: app.S.ArchivePath,
|
||||||
|
TotalPages: util.IntPtrToUintPtr(app.S.Archive.Len()),
|
||||||
|
Page: uint(app.S.ArchivePos + 1),
|
||||||
|
Added: time.Now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) rebuildBookmarksMenu() {
|
||||||
|
for i := range bookmarkMenuItems {
|
||||||
|
app.W.MenuBookmarks.Remove(bookmarkMenuItems[i])
|
||||||
|
bookmarkMenuItems[i].Destroy()
|
||||||
|
}
|
||||||
|
bookmarkMenuItems = nil
|
||||||
|
util.GC()
|
||||||
|
|
||||||
|
for i := range app.Config.Bookmarks {
|
||||||
|
bookmark := &app.Config.Bookmarks[i]
|
||||||
|
base := filepath.Base(bookmark.Path)
|
||||||
|
totalPages := "?"
|
||||||
|
if bookmark.TotalPages != nil {
|
||||||
|
totalPages = fmt.Sprint(*bookmark.TotalPages)
|
||||||
|
}
|
||||||
|
label := fmt.Sprintf("%s (%d/%s)", base, bookmark.Page, totalPages)
|
||||||
|
bookmarkMenuItem, err := gtk.MenuItemNewWithLabel(label)
|
||||||
|
if err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bookmarkMenuItem.Connect("activate", func() {
|
||||||
|
if app.S.ArchivePath != bookmark.Path {
|
||||||
|
app.loadArchiveFromPath(bookmark.Path)
|
||||||
|
}
|
||||||
|
app.setPage(int(bookmark.Page) - 1)
|
||||||
|
})
|
||||||
|
bookmarkMenuItems = append(bookmarkMenuItems, bookmarkMenuItem)
|
||||||
|
app.W.MenuBookmarks.Append(bookmarkMenuItem)
|
||||||
|
}
|
||||||
|
app.W.MenuBookmarks.ShowAll()
|
||||||
|
}
|
23
build_info.go
Normal file
23
build_info.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
type BuildInfo struct {
|
||||||
|
Version string
|
||||||
|
Date string
|
||||||
|
}
|
70
cmd/gomicsv/main.go
Normal file
70
cmd/gomicsv/main.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv"
|
||||||
|
flag "github.com/spf13/pflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
buildDate = ""
|
||||||
|
versionString = ""
|
||||||
|
|
||||||
|
referer = flag.String("referer", "", "HTTP Referer value to use for requests when the provided path is a URL")
|
||||||
|
help = flag.BoolP("help", "h", false, "Print usage message and exit")
|
||||||
|
version = flag.BoolP("version", "v", false, "Print program version and exit")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Usage = func() {
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
|
"Usage: %s [path] [options]:\n"+
|
||||||
|
" path Path to the comic to load at startup. Could be filesystem path, a URL of one of the images or a URL template for all of the images\n"+
|
||||||
|
" Options:\n",
|
||||||
|
gomicsv.AppName,
|
||||||
|
)
|
||||||
|
flag.PrintDefaults()
|
||||||
|
}
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if *help {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *version {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s version %s", gomicsv.AppNameDisplay, versionString)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonFlagArgs := []string{os.Args[0]}
|
||||||
|
nonFlagArgs = append(nonFlagArgs, flag.Args()...)
|
||||||
|
initParams := gomicsv.AppStartupParams{
|
||||||
|
Referer: *referer,
|
||||||
|
}
|
||||||
|
|
||||||
|
app := gomicsv.App{}
|
||||||
|
gtkApplication := app.Init(nonFlagArgs, initParams, gomicsv.BuildInfo{Version: versionString, Date: buildDate})
|
||||||
|
os.Exit(gtkApplication.Run(nonFlagArgs))
|
||||||
|
}
|
67
color.go
Normal file
67
color.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Color struct {
|
||||||
|
R byte `json:"R"`
|
||||||
|
G byte `json:"G"`
|
||||||
|
B byte `json:"B"`
|
||||||
|
A byte `json:"A"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewColorFromGdkRGBA(rgba *gdk.RGBA) Color {
|
||||||
|
return Color{
|
||||||
|
R: colorFloat64ToByte(rgba.GetRed()),
|
||||||
|
G: colorFloat64ToByte(rgba.GetGreen()),
|
||||||
|
B: colorFloat64ToByte(rgba.GetBlue()),
|
||||||
|
A: colorFloat64ToByte(rgba.GetAlpha()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (color Color) ToCSS() string {
|
||||||
|
return fmt.Sprintf("rgba(%d, %d, %d, %d)", color.R, color.G, color.B, color.A)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (color Color) ToPixelInt() uint32 {
|
||||||
|
return uint32(color.R)<<24 + uint32(color.G)<<16 + uint32(color.B)<<8 + uint32(color.A)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (color Color) ToGdkRGBA() gdk.RGBA {
|
||||||
|
return *gdk.NewRGBA(
|
||||||
|
colorByteToFloat64(color.R),
|
||||||
|
colorByteToFloat64(color.G),
|
||||||
|
colorByteToFloat64(color.B),
|
||||||
|
colorByteToFloat64(color.A),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorFloat64ToByte(f float64) byte {
|
||||||
|
return byte(math.Round(f * 255.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorByteToFloat64(b byte) float64 {
|
||||||
|
return float64(b) / 255.0
|
||||||
|
}
|
325
config.go
Normal file
325
config.go
Normal file
|
@ -0,0 +1,325 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConfigFilename = "config"
|
||||||
|
ReadLaterDir = "read-later"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ZoomMode string
|
||||||
|
Enlarge bool
|
||||||
|
Shrink bool
|
||||||
|
LastDirectory string
|
||||||
|
Fullscreen bool
|
||||||
|
HideUI bool
|
||||||
|
WindowWidth int
|
||||||
|
WindowHeight int
|
||||||
|
Random bool
|
||||||
|
Seamless bool
|
||||||
|
HFlip bool
|
||||||
|
VFlip bool
|
||||||
|
DoublePage bool
|
||||||
|
MangaMode bool
|
||||||
|
BackgroundColor Color
|
||||||
|
NSkip int
|
||||||
|
NPreload int
|
||||||
|
RememberRecent bool
|
||||||
|
RememberPosition bool
|
||||||
|
RememberPositionHTTP bool
|
||||||
|
OneWide bool
|
||||||
|
EmbeddedOrientation bool
|
||||||
|
Interpolation int
|
||||||
|
ImageDiffThres float32
|
||||||
|
SceneScanSkip int
|
||||||
|
SmartScroll bool
|
||||||
|
HideIdleCursor bool
|
||||||
|
KamiteEnabled bool
|
||||||
|
KamitePort int
|
||||||
|
Bookmarks []Bookmark
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) configFilePath() string {
|
||||||
|
return filepath.Join(app.S.ConfigDirPath, ConfigFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) loadConfig() {
|
||||||
|
app.Config.setDefaults()
|
||||||
|
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting the current user: %v", err)
|
||||||
|
}
|
||||||
|
app.Config.LastDirectory = currentUser.HomeDir
|
||||||
|
|
||||||
|
if err := app.Config.load(app.configFilePath()); err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
log.Panicf("loading config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) saveConfig() {
|
||||||
|
if err := app.Config.save(app.configFilePath()); err != nil {
|
||||||
|
log.Printf("Error saving config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) setDefaults() {
|
||||||
|
c.ZoomMode = "FitToWidth"
|
||||||
|
c.Shrink = true
|
||||||
|
c.Enlarge = true
|
||||||
|
c.WindowWidth = 640
|
||||||
|
c.WindowHeight = 480
|
||||||
|
c.NSkip = 10
|
||||||
|
c.NPreload = 2
|
||||||
|
c.Seamless = true
|
||||||
|
c.RememberRecent = true
|
||||||
|
c.RememberPosition = false
|
||||||
|
c.RememberPositionHTTP = false
|
||||||
|
c.BackgroundColor = Color{
|
||||||
|
R: 0,
|
||||||
|
G: 0,
|
||||||
|
B: 0,
|
||||||
|
A: 255,
|
||||||
|
}
|
||||||
|
c.Interpolation = 2
|
||||||
|
c.EmbeddedOrientation = true
|
||||||
|
c.ImageDiffThres = 0.4
|
||||||
|
c.SceneScanSkip = 5
|
||||||
|
c.SmartScroll = false
|
||||||
|
c.HideIdleCursor = true
|
||||||
|
c.KamiteEnabled = false
|
||||||
|
c.KamitePort = 4110
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) load(path string) error {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
d := json.NewDecoder(f)
|
||||||
|
if err = d.Decode(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) save(path string) error {
|
||||||
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(c, "", "\t")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = f.Write(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setZoomMode(mode string) {
|
||||||
|
switch mode {
|
||||||
|
case "FitToWidth":
|
||||||
|
app.W.MenuItemFitToWidth.SetActive(true)
|
||||||
|
case "FitToHeight":
|
||||||
|
app.W.MenuItemFitToHeight.SetActive(true)
|
||||||
|
case "BestFit":
|
||||||
|
app.W.MenuItemBestFit.SetActive(true)
|
||||||
|
default:
|
||||||
|
app.W.MenuItemOriginal.SetActive(true)
|
||||||
|
mode = "Original"
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Config.ZoomMode = mode
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setHFlip(hflip bool) {
|
||||||
|
app.Config.HFlip = hflip
|
||||||
|
app.blit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setVFlip(vflip bool) {
|
||||||
|
app.Config.VFlip = vflip
|
||||||
|
app.blit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setFullscreen(fullscreen bool) {
|
||||||
|
app.Config.Fullscreen = fullscreen
|
||||||
|
if fullscreen {
|
||||||
|
app.W.Statusbar.Hide()
|
||||||
|
app.W.Toolbar.Hide()
|
||||||
|
//app.Menubar.Hide() // BUG: menubar visible on fullscreen
|
||||||
|
app.W.MainWindow.Fullscreen()
|
||||||
|
} else {
|
||||||
|
app.W.Statusbar.Show()
|
||||||
|
app.W.Toolbar.Show()
|
||||||
|
//app.Menubar.Show()
|
||||||
|
app.W.MainWindow.Unfullscreen()
|
||||||
|
}
|
||||||
|
app.W.MenuItemFullscreen.SetActive(fullscreen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setHideUI(hideUI bool) {
|
||||||
|
app.Config.HideUI = hideUI
|
||||||
|
|
||||||
|
if hideUI {
|
||||||
|
app.W.Menubar.Hide()
|
||||||
|
app.W.Toolbar.Hide()
|
||||||
|
app.W.Statusbar.Hide()
|
||||||
|
} else {
|
||||||
|
app.W.Menubar.Show()
|
||||||
|
app.W.Toolbar.Show()
|
||||||
|
app.W.Statusbar.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force child size recalculation
|
||||||
|
// FIXME: This sometimes doesn't have an effect
|
||||||
|
app.W.MainContainer.Hide()
|
||||||
|
app.W.MainContainer.Show()
|
||||||
|
|
||||||
|
app.W.MenuItemHideUI.SetActive(hideUI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setShrink(shrink bool) {
|
||||||
|
app.Config.Shrink = shrink
|
||||||
|
app.W.MenuItemShrink.SetActive(shrink)
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setEnlarge(enlarge bool) {
|
||||||
|
app.Config.Enlarge = enlarge
|
||||||
|
app.W.MenuItemEnlarge.SetActive(enlarge)
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setRandom(random bool) {
|
||||||
|
app.Config.Random = random
|
||||||
|
app.W.MenuItemRandom.SetActive(random)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setSeamless(seamless bool) {
|
||||||
|
app.Config.Seamless = seamless
|
||||||
|
app.W.MenuItemSeamless.SetActive(seamless)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setDoublePage(doublePage bool) {
|
||||||
|
app.W.ImageR.SetVisible(doublePage)
|
||||||
|
app.Config.DoublePage = doublePage
|
||||||
|
app.doSetPage(app.S.ArchivePos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setMangaMode(mangaMode bool) {
|
||||||
|
app.Config.MangaMode = mangaMode
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setBackgroundColor(color Color) {
|
||||||
|
app.Config.BackgroundColor = color
|
||||||
|
|
||||||
|
screen, err := gdk.ScreenGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting default screen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.BackgroundColorCssProvider != nil {
|
||||||
|
gtk.RemoveProviderForScreen(screen, app.S.BackgroundColorCssProvider)
|
||||||
|
app.S.BackgroundColorCssProvider = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
provider, err := gtk.CssProviderNew()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("creating CssProvider: %v", err)
|
||||||
|
}
|
||||||
|
err = provider.LoadFromData(fmt.Sprintf("#ScrolledWindow { background-color: %s; }", color.ToCSS()))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("adding css to provider: %v", err)
|
||||||
|
}
|
||||||
|
gtk.AddProviderForScreen(screen, provider, 1)
|
||||||
|
app.S.BackgroundColorCssProvider = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setInterpolation(interpolation int) {
|
||||||
|
app.Config.Interpolation = interpolation
|
||||||
|
app.blit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setRememberRecent(rememberRecent bool) {
|
||||||
|
app.Config.RememberRecent = rememberRecent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setRememberPosition(rememberPosition bool) {
|
||||||
|
app.Config.RememberPosition = rememberPosition
|
||||||
|
// TODO: Clear remembered when unset
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setRememberPositionHTTP(rememberPositionHTTP bool) {
|
||||||
|
app.Config.RememberPositionHTTP = rememberPositionHTTP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setOneWide(oneWide bool) {
|
||||||
|
app.Config.OneWide = oneWide
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setSmartScroll(smartScroll bool) {
|
||||||
|
app.Config.SmartScroll = smartScroll
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setHideIdleCursor(hideIdleCursor bool) {
|
||||||
|
app.Config.HideIdleCursor = hideIdleCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setEmbeddedOrientation(embeddedOrientation bool) {
|
||||||
|
app.Config.EmbeddedOrientation = embeddedOrientation
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setKamiteEnabled(kamiteEnable bool) {
|
||||||
|
app.Config.KamiteEnabled = kamiteEnable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setKamitePort(kamitePort int) {
|
||||||
|
app.Config.KamitePort = kamitePort
|
||||||
|
}
|
107
cursor.go
Normal file
107
cursor.go
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CursorsState struct {
|
||||||
|
Current Cursor
|
||||||
|
Cache CursorCache
|
||||||
|
LastMoved time.Time
|
||||||
|
Visible bool
|
||||||
|
ForceVisible bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
cursorNone = iota
|
||||||
|
cursorDefault
|
||||||
|
cursorGrabbing
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cursor int
|
||||||
|
|
||||||
|
type CursorCache = map[Cursor]*gdk.Cursor
|
||||||
|
|
||||||
|
func (cursor *CursorsState) init() {
|
||||||
|
cursor.Cache = CursorCache{
|
||||||
|
cursorNone: loadCursor("none"),
|
||||||
|
cursorDefault: loadCursor("default"),
|
||||||
|
cursorGrabbing: loadCursor("grabbing"),
|
||||||
|
}
|
||||||
|
cursor.Current = cursorDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cursor *CursorsState) reset() {
|
||||||
|
cursor.LastMoved = time.Now()
|
||||||
|
cursor.Visible = false
|
||||||
|
cursor.ForceVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) setCursor(cursor Cursor) {
|
||||||
|
win, err := app.W.ImageViewport.GetWindow()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting Image Viewport's Window: %v", err)
|
||||||
|
}
|
||||||
|
if cursor != cursorNone {
|
||||||
|
app.S.Cursor.Current = cursor
|
||||||
|
}
|
||||||
|
win.SetCursor(app.S.Cursor.Cache[cursor])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) hideCursor() {
|
||||||
|
app.setCursor(cursorNone)
|
||||||
|
app.S.Cursor.Visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) showCursor() {
|
||||||
|
app.setCursor(app.S.Cursor.Current)
|
||||||
|
app.S.Cursor.Visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) updateCursorVisibility() bool {
|
||||||
|
shouldBeHidden := false
|
||||||
|
if !app.S.DragScroll.InProgress && app.Config.HideIdleCursor && !app.S.Cursor.ForceVisible {
|
||||||
|
shouldBeHidden = time.Since(app.S.Cursor.LastMoved).Seconds() > 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldBeHidden && app.S.Cursor.Visible {
|
||||||
|
app.hideCursor()
|
||||||
|
} else if !shouldBeHidden && !app.S.Cursor.Visible {
|
||||||
|
app.showCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCursor(name string) *gdk.Cursor {
|
||||||
|
disp, err := gdk.DisplayGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting default display: %v", err)
|
||||||
|
}
|
||||||
|
c, err := gdk.CursorNewFromName(disp, name)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("creating cursor: %v", err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module github.com/fauu/gomicsv
|
||||||
|
|
||||||
|
replace github.com/fauu/gomicsv => ./
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/flytam/filenamify v1.1.0
|
||||||
|
github.com/gotk3/gotk3 v0.6.1
|
||||||
|
github.com/spf13/pflag v1.0.5
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6
|
||||||
|
)
|
16
go.sum
Normal file
16
go.sum
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/diamondburned/gotk4 v0.0.3-0.20220418035147-8a785583d00a h1:u+75gz/SWdpIWJaD0TFR4lgmz+s0JUkKfLXgJesf+GQ=
|
||||||
|
github.com/diamondburned/gotk4 v0.0.3-0.20220418035147-8a785583d00a/go.mod h1:WbOkeM91aOubLbxqcbYwd7q/HCKxAmqObvT5Dhpzm1Y=
|
||||||
|
github.com/diamondburned/gotk4/pkg v0.0.0-20220418035147-8a785583d00a h1:UrzOwwQiOLckSyU1ETP66Qz9XV/vvVCn0RS1YTY1prk=
|
||||||
|
github.com/diamondburned/gotk4/pkg v0.0.0-20220418035147-8a785583d00a/go.mod h1:rLH6FHos690jFgAM/GYEpMykuE/9NmN6zOvFlr8JTvE=
|
||||||
|
github.com/flytam/filenamify v1.1.0 h1:iEOcC/1UgxJf4lp2E2CWtYO7TMyYmgb2RPSTou89FLs=
|
||||||
|
github.com/flytam/filenamify v1.1.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
|
||||||
|
github.com/gotk3/gotk3 v0.6.0 h1:Aqlq4/6VabNwtCyA9M9zFNad5yHAqCi5heWnZ9y+3dA=
|
||||||
|
github.com/gotk3/gotk3 v0.6.0/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/gotk3/gotk3 v0.6.1 h1:GJ400a0ecEEWrzjBvzBzH+pB/esEMIGdB9zPSmBdoeo=
|
||||||
|
github.com/gotk3/gotk3 v0.6.1/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q=
|
||||||
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng=
|
||||||
|
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
|
||||||
|
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
1201
gomicsv.ui
Normal file
1201
gomicsv.ui
Normal file
File diff suppressed because it is too large
Load diff
103
goto.go
Normal file
103
goto.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) goToDialogInit() {
|
||||||
|
_, err := app.W.GoToDialog.AddButton("_Cancel", gtk.RESPONSE_CANCEL)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
goButton, err := app.W.GoToDialog.AddButton("_Go", gtk.RESPONSE_ACCEPT)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
|
||||||
|
app.W.GoToDialog.SetDefault(goButton)
|
||||||
|
|
||||||
|
app.W.GoToSpinButton.Connect("value-changed", func() {
|
||||||
|
app.W.GoToScrollbar.SetValue(app.W.GoToSpinButton.GetValue())
|
||||||
|
app.goToDialogUpdateThumbnail()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.GoToScrollbar.Connect("value-changed", func() {
|
||||||
|
app.W.GoToSpinButton.SetValue(app.W.GoToScrollbar.GetValue())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) goToDialogUpdateThumbnail() {
|
||||||
|
n := int(app.W.GoToSpinButton.GetValue() - 1)
|
||||||
|
pixbuf, err := app.S.Archive.Load(n, app.Config.EmbeddedOrientation, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting thumbnail: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h := util.Fit(pixbuf.GetWidth(), pixbuf.GetHeight(), 128, 128)
|
||||||
|
|
||||||
|
scaled, err := pixbuf.ScaleSimple(w, h, interpolations[app.Config.Interpolation])
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error scaling thumbnail: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.GoToThumbPixbuf = scaled
|
||||||
|
app.W.GoToThumbnailImage.SetFromPixbuf(scaled)
|
||||||
|
|
||||||
|
util.GC()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) goToDialogRun() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.Archive.Len() == nil {
|
||||||
|
app.goToDialogUpdateSpinButton(9999)
|
||||||
|
app.W.GoToScrollbar.Hide()
|
||||||
|
} else {
|
||||||
|
app.goToDialogUpdateSpinButton(*app.S.Archive.Len())
|
||||||
|
app.W.GoToScrollbar.SetRange(1, float64(*app.S.Archive.Len()))
|
||||||
|
app.W.GoToScrollbar.SetValue(float64(app.S.ArchivePos) + 1)
|
||||||
|
app.W.GoToScrollbar.SetIncrements(1, float64(*app.S.Archive.Len()))
|
||||||
|
app.W.GoToScrollbar.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
app.W.GoToSpinButton.GrabFocus()
|
||||||
|
|
||||||
|
app.goToDialogUpdateThumbnail()
|
||||||
|
|
||||||
|
res := gtk.ResponseType(app.W.GoToDialog.Run())
|
||||||
|
app.W.GoToDialog.Hide()
|
||||||
|
if res == gtk.RESPONSE_ACCEPT {
|
||||||
|
app.setPage(int(app.W.GoToSpinButton.GetValue()) - 1)
|
||||||
|
|
||||||
|
app.W.GoToThumbnailImage.Clear()
|
||||||
|
app.S.GoToThumbPixbuf = nil
|
||||||
|
util.GC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) goToDialogUpdateSpinButton(archiveLen int) {
|
||||||
|
app.W.GoToSpinButton.SetRange(1, float64(archiveLen))
|
||||||
|
app.W.GoToSpinButton.SetValue(float64(app.S.ArchivePos) + 1)
|
||||||
|
app.W.GoToSpinButton.SetIncrements(1, float64(app.Config.NSkip))
|
||||||
|
}
|
102
gtk.go
Normal file
102
gtk.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDefaultPointerDevice() (*gdk.Device, error) {
|
||||||
|
display, err := gdk.DisplayGetDefault()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
seat, err := display.GetDefaultSeat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pointerDevice, err := seat.GetPointer()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pointerDevice, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pixbufCopyPixels(src *gdk.Pixbuf, dst *gdk.Pixbuf, dstStartX int) {
|
||||||
|
nChannels := src.GetNChannels() // ASSUMPTION: Number of channels matches
|
||||||
|
srcRowstride, dstRowstride := src.GetRowstride(), dst.GetRowstride()
|
||||||
|
srcPixels, dstPixels := src.GetPixels(), dst.GetPixels()
|
||||||
|
for y := 0; y < src.GetHeight(); y++ {
|
||||||
|
for x := 0; x < src.GetWidth(); x++ {
|
||||||
|
xOffset := x * nChannels
|
||||||
|
srcIdxBase := y*srcRowstride + xOffset
|
||||||
|
dstIdxBase := y*dstRowstride + dstStartX*nChannels + xOffset
|
||||||
|
for i := 0; i < nChannels; i++ {
|
||||||
|
dstPixels[dstIdxBase+i] = srcPixels[srcIdxBase+i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuWithAccels struct {
|
||||||
|
Menu *gtk.Menu
|
||||||
|
Path string
|
||||||
|
Items []MenuItemWithAccels
|
||||||
|
}
|
||||||
|
|
||||||
|
type MenuItemWithAccels struct {
|
||||||
|
Item *gtk.MenuItem
|
||||||
|
Accel Accel
|
||||||
|
}
|
||||||
|
|
||||||
|
type Accel struct {
|
||||||
|
Key uint
|
||||||
|
Mods gdk.ModifierType
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMenuAccels(mainWindow *gtk.ApplicationWindow, menuSetups []MenuWithAccels) error {
|
||||||
|
for _, menuSetup := range menuSetups {
|
||||||
|
accelGroup, err := gtk.AccelGroupNew()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mainWindow.AddAccelGroup(accelGroup)
|
||||||
|
|
||||||
|
menuSetup.Menu.SetAccelPath(menuSetup.Path)
|
||||||
|
menuSetup.Menu.SetAccelGroup(accelGroup)
|
||||||
|
|
||||||
|
for _, item := range menuSetup.Items {
|
||||||
|
itemPath := fmt.Sprintf("%s/%s", menuSetup.Path, item.Item.GetLabel())
|
||||||
|
gtk.AccelMapAddEntry(itemPath, item.Accel.Key, item.Accel.Mods)
|
||||||
|
accelGroup.ConnectByPath(itemPath, item.Item.Activate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDialogAddButtonErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("adding a button to a dialog: %v", err)
|
||||||
|
}
|
||||||
|
}
|
295
image.go
Normal file
295
image.go
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const savedImagePNGQuality = 9
|
||||||
|
|
||||||
|
var interpolations = []gdk.InterpType{gdk.INTERP_NEAREST, gdk.INTERP_TILES, gdk.INTERP_BILINEAR, gdk.INTERP_HYPER}
|
||||||
|
|
||||||
|
func (app *App) pixbufLoaded() bool {
|
||||||
|
if app.Config.DoublePage && !app.shouldForceSinglePage() {
|
||||||
|
return app.S.PixbufL != nil && app.S.PixbufR != nil
|
||||||
|
}
|
||||||
|
return app.S.PixbufL != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) pixbufSize() (w, h int) {
|
||||||
|
if !app.pixbufLoaded() {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &app.S
|
||||||
|
|
||||||
|
if app.Config.DoublePage && !app.shouldForceSinglePage() {
|
||||||
|
return s.PixbufL.GetWidth() + s.PixbufR.GetWidth(), util.Max(s.PixbufL.GetHeight(), s.PixbufR.GetHeight())
|
||||||
|
}
|
||||||
|
return s.PixbufL.GetWidth(), s.PixbufL.GetHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) updateStatus() {
|
||||||
|
s := &app.S
|
||||||
|
|
||||||
|
if !app.pixbufLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
zoom := int(100 * app.S.Scale)
|
||||||
|
|
||||||
|
lenStr := "?"
|
||||||
|
if s.Archive.Len() != nil {
|
||||||
|
lenStr = fmt.Sprint(*s.Archive.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
markedStr := ""
|
||||||
|
if app.currentPageIsJumpmarked() {
|
||||||
|
markedStr = " MARKED "
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg, title string
|
||||||
|
if app.Config.DoublePage && !app.shouldForceSinglePage() {
|
||||||
|
leftPath, _ := s.Archive.Name(s.ArchivePos)
|
||||||
|
left := filepath.Base(leftPath)
|
||||||
|
rightPath, _ := s.Archive.Name(s.ArchivePos + 1)
|
||||||
|
right := filepath.Base(rightPath)
|
||||||
|
|
||||||
|
leftIndex := s.ArchivePos + 1
|
||||||
|
rightIndex := s.ArchivePos + 2
|
||||||
|
|
||||||
|
leftw, lefth := s.PixbufL.GetWidth(), s.PixbufL.GetHeight()
|
||||||
|
rightw, righth := s.PixbufR.GetWidth(), s.PixbufR.GetHeight()
|
||||||
|
|
||||||
|
if app.Config.MangaMode {
|
||||||
|
left, right = right, left
|
||||||
|
leftIndex, rightIndex = rightIndex, leftIndex
|
||||||
|
leftw, rightw = rightw, leftw
|
||||||
|
}
|
||||||
|
msg = fmt.Sprintf("%d+%d / %s %s | %dx%d - %dx%d (%d%%) | %s | %s - %s", leftIndex, rightIndex, lenStr, markedStr, leftw, lefth, rightw, righth, zoom, s.ArchiveName, left, right)
|
||||||
|
title = fmt.Sprintf("[%d+%d / %s] %s", leftIndex, rightIndex, lenStr, s.ArchiveName)
|
||||||
|
} else {
|
||||||
|
imgPath, _ := s.Archive.Name(s.ArchivePos)
|
||||||
|
w, h := s.PixbufL.GetWidth(), s.PixbufL.GetHeight()
|
||||||
|
msg = fmt.Sprintf("%d / %s %s | %dx%d (%d%%) | %s | %s", s.ArchivePos+1, lenStr, markedStr, w, h, zoom, s.ArchiveName, imgPath)
|
||||||
|
title = fmt.Sprintf("[%d / %s] %s", s.ArchivePos+1, lenStr, s.ArchiveName)
|
||||||
|
}
|
||||||
|
app.setStatus(msg)
|
||||||
|
|
||||||
|
app.W.MainWindow.SetTitle(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) getImageAreaInnerSize() (width, height int) {
|
||||||
|
alloc := app.W.ScrolledWindow.GetAllocation()
|
||||||
|
return alloc.GetWidth() - 4, alloc.GetHeight() - 4 // 2u of padding from the ScrolledWindow and 2u from the Viewport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) getScaledSize() (scale float64) {
|
||||||
|
if !app.pixbufLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrw, scrh := app.getImageAreaInnerSize()
|
||||||
|
|
||||||
|
w, h := app.pixbufSize()
|
||||||
|
switch app.Config.ZoomMode {
|
||||||
|
case "FitToWidth":
|
||||||
|
needscale := (app.Config.Enlarge && w < scrw) || (app.Config.Shrink && w > scrw)
|
||||||
|
if needscale {
|
||||||
|
return float64(scrw) / float64(w)
|
||||||
|
}
|
||||||
|
case "FitToHeight":
|
||||||
|
return float64(scrh) / float64(h)
|
||||||
|
case "BestFit":
|
||||||
|
needscale := (app.Config.Enlarge && (w < scrw && h < scrh)) || (app.Config.Shrink && (w > scrw || h > scrh))
|
||||||
|
if needscale {
|
||||||
|
fw, _ := util.Fit(w, h, scrw, scrh)
|
||||||
|
return float64(fw) / float64(w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) shouldForceSinglePage() bool {
|
||||||
|
if app.S.PixbufR == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return app.Config.OneWide && (app.S.PixbufL.GetWidth() > app.S.PixbufL.GetHeight() || app.S.PixbufR.GetWidth() > app.S.PixbufR.GetHeight())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) blit() {
|
||||||
|
if !app.pixbufLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.Scale = app.getScaledSize()
|
||||||
|
|
||||||
|
// Check whether the scale of the left image is different from the old one?
|
||||||
|
|
||||||
|
if app.Config.DoublePage && !app.shouldForceSinglePage() {
|
||||||
|
left := app.S.PixbufL
|
||||||
|
right := app.S.PixbufR
|
||||||
|
|
||||||
|
if app.Config.MangaMode {
|
||||||
|
left, right = right, left
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.doBlit(app.W.ImageL, left, app.S.Scale); err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.doBlit(app.W.ImageR, right, app.S.Scale); err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.W.ImageR.Clear()
|
||||||
|
if err := app.doBlit(app.W.ImageL, app.S.PixbufL, app.S.Scale); err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.Scale != 1 || app.Config.HFlip || app.Config.VFlip {
|
||||||
|
util.GC()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) doBlit(image *gtk.Image, pixbuf *gdk.Pixbuf, scale float64) (err error) {
|
||||||
|
image.Clear()
|
||||||
|
|
||||||
|
if app.Config.HFlip {
|
||||||
|
pixbuf, err = pixbuf.Flip(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.VFlip {
|
||||||
|
pixbuf, err = pixbuf.Flip(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scale != 1 {
|
||||||
|
w, h := pixbuf.GetWidth(), pixbuf.GetHeight()
|
||||||
|
pixbuf, err = pixbuf.ScaleSimple(int(float64(w)*scale), int(float64(h)*scale), interpolations[app.Config.Interpolation])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
image.SetFromPixbuf(pixbuf)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveImageErrorMsgTpl = "Couldn't save image: %v"
|
||||||
|
|
||||||
|
func (app *App) saveImage(path string) {
|
||||||
|
if app.S.PixbufR == nil {
|
||||||
|
if app.S.PixbufL == nil {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if err := app.S.PixbufL.SavePNG(path, savedImagePNGQuality); err != nil {
|
||||||
|
app.showError(fmt.Sprintf(saveImageErrorMsgTpl, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know we're in double page mode
|
||||||
|
stichedPixbuf, err := app.getStichedPixbuf()
|
||||||
|
if err != nil {
|
||||||
|
app.showError(fmt.Sprintf(saveImageErrorMsgTpl, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := stichedPixbuf.SavePNG(path, savedImagePNGQuality); err != nil {
|
||||||
|
app.showError(fmt.Sprintf(saveImageErrorMsgTpl, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.notificationShow(fmt.Sprintf("Saved to %s", path), ShortNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyImageSuccessMsg = "Copied image to clipboard"
|
||||||
|
|
||||||
|
func (app *App) copyImageToClipboard() {
|
||||||
|
clipboard, err := gtk.ClipboardGet(gdk.GdkAtomIntern("CLIPBOARD", true))
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting clipboard: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.PixbufR == nil {
|
||||||
|
if app.S.PixbufL != nil {
|
||||||
|
clipboard.SetImage(app.S.PixbufL)
|
||||||
|
app.notificationShow(copyImageSuccessMsg, ShortNotification)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know we're in double page mode
|
||||||
|
stichedPixbuf, err := app.getStichedPixbuf()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error stiching images: %v", err)
|
||||||
|
app.showError("Couldn't copy image to clipboard")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clipboard.SetImage(stichedPixbuf)
|
||||||
|
app.notificationShow(copyImageSuccessMsg, ShortNotification)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getStichedPixbuf creates a Pixbuf combining the left and right image Pixbufs
|
||||||
|
func (app *App) getStichedPixbuf() (*gdk.Pixbuf, error) {
|
||||||
|
l, r := app.S.PixbufL, app.S.PixbufR
|
||||||
|
if app.Config.MangaMode {
|
||||||
|
l, r = r, l
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASSUMPTION: Both Pixbufs have the same parameters except for size
|
||||||
|
mergedPixbuf, err := gdk.PixbufNew(
|
||||||
|
l.GetColorspace(),
|
||||||
|
l.GetHasAlpha(),
|
||||||
|
l.GetBitsPerSample(),
|
||||||
|
l.GetWidth()+r.GetWidth(),
|
||||||
|
util.Max(l.GetHeight(), r.GetHeight()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating pixbuf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedPixbuf.Fill(app.Config.BackgroundColor.ToPixelInt())
|
||||||
|
|
||||||
|
// POLISH: If heights are unequal, center the smaller image vertically, just as it appears in the program
|
||||||
|
pixbufCopyPixels(l, mergedPixbuf, 0)
|
||||||
|
pixbufCopyPixels(r, mergedPixbuf, l.GetWidth())
|
||||||
|
|
||||||
|
return mergedPixbuf, nil
|
||||||
|
}
|
94
image_area.go
Normal file
94
image_area.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kamiteLongRightClickDelay = 375
|
||||||
|
|
||||||
|
func (app *App) imageAreaInit() {
|
||||||
|
app.W.ScrolledWindow.SetEvents(app.W.ScrolledWindow.GetEvents() | int(gdk.BUTTON_PRESS_MASK) | int(gdk.BUTTON_RELEASE_MASK))
|
||||||
|
|
||||||
|
app.W.ScrolledWindow.Connect("scroll-event", func(self *gtk.ScrolledWindow, event *gdk.Event) {
|
||||||
|
se := &gdk.EventScroll{Event: event}
|
||||||
|
app.scroll(se.DeltaX(), se.DeltaY())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.ScrolledWindow.Connect("button-press-event", func(self *gtk.ScrolledWindow, event *gdk.Event) bool {
|
||||||
|
be := &gdk.EventButton{Event: event}
|
||||||
|
switch be.Button() {
|
||||||
|
case 1:
|
||||||
|
if (int)(be.X()) < self.GetAllocatedWidth()/2 {
|
||||||
|
app.previousPage()
|
||||||
|
} else {
|
||||||
|
app.nextPage()
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
if app.Config.KamiteEnabled {
|
||||||
|
app.S.KamiteRightClickActionPending = true
|
||||||
|
glib.TimeoutAdd(kamiteLongRightClickDelay, func() bool {
|
||||||
|
if app.S.KamiteRightClickActionPending {
|
||||||
|
app.kamiteRecognizeManualBlock()
|
||||||
|
app.S.KamiteRightClickActionPending = false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Empty
|
||||||
|
case 2:
|
||||||
|
app.dragScrollStart(be.X(), be.Y(), self.GetVAdjustment().GetValue(), self.GetHAdjustment().GetValue())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.ScrolledWindow.Connect("button-release-event", func(_ *gtk.ScrolledWindow, event *gdk.Event) bool {
|
||||||
|
be := &gdk.EventButton{Event: event}
|
||||||
|
switch be.Button() {
|
||||||
|
case 1:
|
||||||
|
// Empty
|
||||||
|
case 3:
|
||||||
|
if app.Config.KamiteEnabled {
|
||||||
|
if app.S.KamiteRightClickActionPending {
|
||||||
|
app.kamiteRecognizeImageUnderCursorBlock()
|
||||||
|
app.S.KamiteRightClickActionPending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
app.dragScrollEnd()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.ScrolledWindow.Connect("motion-notify-event", func(sw *gtk.ScrolledWindow, event *gdk.Event) bool {
|
||||||
|
if app.S.DragScroll.InProgress {
|
||||||
|
app.dragScrollUpdate(sw, &gdk.EventButton{Event: event})
|
||||||
|
}
|
||||||
|
return false // Let it be handled for MainWindow
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) handleImageAreaResize() {
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
104
imgdiff/imgdiff.go
Normal file
104
imgdiff/imgdiff.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package imgdiff
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"math/bits"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dhashImageWidth = 9
|
||||||
|
dhashImageHeight = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
type Hash uint64 // Assuming (dhashImageWidth-1)*dhashImageHeight <= 64
|
||||||
|
|
||||||
|
func Distance(h1, h2 Hash) int {
|
||||||
|
return bits.OnesCount64(uint64(h1 ^ h2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
if dhashImageHeight*(dhashImageWidth-1) > 64 {
|
||||||
|
panic("dhashImageHeight is too large")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pixbufToGrayscaleImage(p *gdk.Pixbuf) *image.Gray {
|
||||||
|
nchan := p.GetNChannels()
|
||||||
|
data := p.GetPixels()
|
||||||
|
w, h := p.GetWidth(), p.GetHeight()
|
||||||
|
rowstride := p.GetRowstride()
|
||||||
|
im := image.NewGray(image.Rect(0, 0, w, h))
|
||||||
|
|
||||||
|
if nchan == 1 {
|
||||||
|
for ih := 0; ih < h; ih++ {
|
||||||
|
for iw := 0; iw < w; iw++ {
|
||||||
|
y := data[ih*rowstride+iw*nchan]
|
||||||
|
im.SetGray(iw, ih, color.Gray{Y: y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if nchan == 3 || nchan == 4 {
|
||||||
|
for ih := 0; ih < h; ih++ {
|
||||||
|
for iw := 0; iw < w; iw++ {
|
||||||
|
r := data[ih*rowstride+iw*nchan]
|
||||||
|
g := data[ih*rowstride+iw*nchan+1]
|
||||||
|
b := data[ih*rowstride+iw*nchan+2]
|
||||||
|
y := uint8((19595*uint32(r) + 38470*uint32(g) + 7471*uint32(b) + 1<<15) >> 16)
|
||||||
|
im.SetGray(iw, ih, color.Gray{Y: y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic("unknown image depth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return im
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://www.hackerfactor.com/blog/?/archives/529-Kind-of-Like-That.html
|
||||||
|
func DHash(p *gdk.Pixbuf) Hash {
|
||||||
|
q, err := p.ScaleSimple(dhashImageWidth, dhashImageHeight, gdk.INTERP_TILES)
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
gray := pixbufToGrayscaleImage(q)
|
||||||
|
|
||||||
|
data := make([]byte, dhashImageWidth*dhashImageHeight)
|
||||||
|
for iy := 0; iy < dhashImageHeight; iy++ {
|
||||||
|
for ix := 0; ix < dhashImageWidth; ix++ {
|
||||||
|
data[iy*dhashImageWidth+ix] = gray.GrayAt(ix, iy).Y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hash Hash
|
||||||
|
|
||||||
|
for iy := 0; iy < dhashImageHeight; iy++ {
|
||||||
|
for ix := 0; ix < dhashImageWidth-1; ix++ {
|
||||||
|
o := iy * dhashImageWidth
|
||||||
|
if data[o+ix+1] > data[o+ix] {
|
||||||
|
hash |= 1 << uint(iy*dhashImageHeight+ix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash
|
||||||
|
}
|
215
jumpmarks.go
Normal file
215
jumpmarks.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Jumpmarks struct {
|
||||||
|
list []int
|
||||||
|
cycle JumpmarksCycle
|
||||||
|
}
|
||||||
|
|
||||||
|
type JumpmarksCycle struct {
|
||||||
|
page *int
|
||||||
|
returnPage *int
|
||||||
|
dontClear bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
cycleDirectionBackward = iota
|
||||||
|
cycleDirectionForward
|
||||||
|
)
|
||||||
|
|
||||||
|
type JumpmarkCycleDirection int
|
||||||
|
|
||||||
|
func (jumpmarks Jumpmarks) has(page int) bool {
|
||||||
|
for _, mark := range jumpmarks.list {
|
||||||
|
// ASSUMPTION: The slice is sorted
|
||||||
|
if mark < page {
|
||||||
|
continue
|
||||||
|
} else if mark == page {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jumpmarks Jumpmarks) size() int {
|
||||||
|
return len(jumpmarks.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jumpmarks *Jumpmarks) toggle(page int) bool {
|
||||||
|
for i, mark := range jumpmarks.list {
|
||||||
|
// ASSUMPTION: The slice is sorted
|
||||||
|
if mark < page {
|
||||||
|
continue
|
||||||
|
} else if mark == page {
|
||||||
|
// Is in the marked list. Remove by swapping with the last element
|
||||||
|
jumpmarks.list[i] = jumpmarks.list[len(jumpmarks.list)-1]
|
||||||
|
jumpmarks.list = jumpmarks.list[:len(jumpmarks.list)-1]
|
||||||
|
sort.Ints(jumpmarks.list)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Isn't in the marked list. Add
|
||||||
|
jumpmarks.list = append(jumpmarks.list, page)
|
||||||
|
sort.Ints(jumpmarks.list)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) clearJumpmarks() {
|
||||||
|
// The following isn't needed until we use this function elsewhere than during archive closing, which we currently don't
|
||||||
|
// for _, mark := range app.S.Jumpmarks.list {
|
||||||
|
// app.S.PageCache.UnforbidRemoval(mark)
|
||||||
|
// }
|
||||||
|
app.S.Jumpmarks = Jumpmarks{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jumpmarkMenuItems []*gtk.MenuItem
|
||||||
|
|
||||||
|
func (app *App) currentPageIsJumpmarked() bool {
|
||||||
|
return app.S.Jumpmarks.has(app.S.ArchivePos + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) toggleJumpmark() {
|
||||||
|
page := app.S.ArchivePos + 1
|
||||||
|
currentMarked := app.S.Jumpmarks.toggle(page)
|
||||||
|
|
||||||
|
var prefix string
|
||||||
|
if currentMarked {
|
||||||
|
app.S.PageCache.Keep(app.S.ArchivePos, pagecache.KeepReasonJumpmark)
|
||||||
|
prefix = "Marked"
|
||||||
|
} else {
|
||||||
|
app.S.PageCache.DontKeep(app.S.ArchivePos, pagecache.KeepReasonJumpmark)
|
||||||
|
prefix = "Unmarked"
|
||||||
|
}
|
||||||
|
app.notificationShow(fmt.Sprintf("%s page %d", prefix, page), ShortNotification)
|
||||||
|
|
||||||
|
app.updateJumpmarkToggleLabel(currentMarked)
|
||||||
|
app.updateJumpmarkCycleMenuItems()
|
||||||
|
app.rebuildJumpmarksMenuList()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) cycleJumpmarks(direction JumpmarkCycleDirection) {
|
||||||
|
if app.S.Jumpmarks.size() == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextPage := -1
|
||||||
|
if app.S.Jumpmarks.cycle.page != nil {
|
||||||
|
nextPage = *app.S.Jumpmarks.cycle.page
|
||||||
|
} else {
|
||||||
|
curr := app.S.ArchivePos + 1
|
||||||
|
app.S.Jumpmarks.cycle.returnPage = &curr
|
||||||
|
}
|
||||||
|
if direction == cycleDirectionForward {
|
||||||
|
nextPage++
|
||||||
|
} else {
|
||||||
|
nextPage--
|
||||||
|
}
|
||||||
|
if nextPage < 0 {
|
||||||
|
nextPage = app.S.Jumpmarks.size() - 1
|
||||||
|
} else if nextPage >= app.S.Jumpmarks.size() {
|
||||||
|
nextPage = 0
|
||||||
|
}
|
||||||
|
app.S.Jumpmarks.cycle.page = &nextPage
|
||||||
|
app.S.Jumpmarks.cycle.dontClear = true
|
||||||
|
app.setPage(app.S.Jumpmarks.list[nextPage] - 1)
|
||||||
|
app.S.Jumpmarks.cycle.dontClear = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) returnFromCyclingJumpmarks() {
|
||||||
|
p := app.S.Jumpmarks.cycle.returnPage
|
||||||
|
if p != nil {
|
||||||
|
app.setPage(*p - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) jumpmarksHandleSetPage(page int) {
|
||||||
|
jumpmarks := &app.S.Jumpmarks
|
||||||
|
if !jumpmarks.cycle.dontClear {
|
||||||
|
jumpmarks.cycle.page = nil
|
||||||
|
jumpmarks.cycle.returnPage = nil
|
||||||
|
}
|
||||||
|
app.updateJumpmarkToggleLabel(app.currentPageIsJumpmarked())
|
||||||
|
app.updateJumpmarkCycleMenuItems()
|
||||||
|
|
||||||
|
s := false
|
||||||
|
if jumpmarks.cycle.returnPage != nil && page != *jumpmarks.cycle.returnPage {
|
||||||
|
s = true
|
||||||
|
}
|
||||||
|
app.W.MenuItemJumpmarksReturnFromCycling.SetSensitive(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) updateJumpmarkToggleLabel(currentMarked bool) {
|
||||||
|
var w string
|
||||||
|
if currentMarked {
|
||||||
|
w = "Unmark"
|
||||||
|
} else {
|
||||||
|
w = "Mark"
|
||||||
|
}
|
||||||
|
label := fmt.Sprintf("%s current page", w)
|
||||||
|
app.W.MenuItemToggleJumpmark.SetLabel(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) updateJumpmarkCycleMenuItems() {
|
||||||
|
s := false
|
||||||
|
if app.S.Jumpmarks.size() > 0 {
|
||||||
|
s = true
|
||||||
|
}
|
||||||
|
app.W.MenuItemCycleJumpmarksBackward.SetSensitive(s)
|
||||||
|
app.W.MenuItemCycleJumpmarksForward.SetSensitive(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) rebuildJumpmarksMenuList() {
|
||||||
|
for _, item := range jumpmarkMenuItems {
|
||||||
|
app.W.MenuJumpmarks.Remove(item)
|
||||||
|
item.Destroy()
|
||||||
|
}
|
||||||
|
jumpmarkMenuItems = nil
|
||||||
|
util.GC()
|
||||||
|
|
||||||
|
for _, mark := range app.S.Jumpmarks.list {
|
||||||
|
label := fmt.Sprintf("%d", mark)
|
||||||
|
menuItem, err := gtk.MenuItemNewWithLabel(label)
|
||||||
|
if err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
page := mark - 1 // Make a new variable so that the correct value gets passed to the callback
|
||||||
|
menuItem.Connect("activate", func() {
|
||||||
|
app.setPage(page)
|
||||||
|
})
|
||||||
|
jumpmarkMenuItems = append(jumpmarkMenuItems, menuItem)
|
||||||
|
app.W.MenuJumpmarks.Append(menuItem)
|
||||||
|
menuItem.Show()
|
||||||
|
}
|
||||||
|
}
|
175
kamite.go
Normal file
175
kamite.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kamiteRecognizeImageSnipScale = 0.8
|
||||||
|
kamiteCMDEndpointBaseTpl = "http://localhost:%d/cmd/"
|
||||||
|
kamiteOCRImageEndpoint = "ocr/image"
|
||||||
|
kamiteOCRManualBlockEndpoint = "ocr/manual-block"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) kamiteRecognizeManualBlock() {
|
||||||
|
endpoint := kamiteMakeEndpointURL(app.Config.KamitePort, kamiteOCRManualBlockEndpoint)
|
||||||
|
_, err := http.PostForm(endpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error making HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) kamiteRecognizeImageUnderCursorBlock() {
|
||||||
|
// 1. Determine global pointer coordinates
|
||||||
|
pointerDevice, err := getDefaultPointerDevice()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting the default pointer device: %v", err)
|
||||||
|
}
|
||||||
|
swx0, swy0, err := app.W.ScrolledWindow.Widget.TranslateCoordinates(app.W.MainWindow, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("translating widget coordinates: %v", err)
|
||||||
|
}
|
||||||
|
mainWindowWindow, err := app.W.MainWindow.GetWindow()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting GdkWindow of MainWindow widget: %v", err)
|
||||||
|
}
|
||||||
|
_, x, y, _ := mainWindowWindow.GetDevicePosition(pointerDevice)
|
||||||
|
|
||||||
|
// 2. Check if cursor is over the image container
|
||||||
|
if (x < swx0 || x > swx0+app.W.ScrolledWindow.GetAllocatedWidth()) ||
|
||||||
|
(y < swy0 || y > swy0+app.W.ScrolledWindow.GetAllocatedHeight()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Determine over which of the images the cursor is and set the source Pixbuf accordingly
|
||||||
|
lx0, ly0, err := app.W.ImageL.Widget.TranslateCoordinates(app.W.ScrolledWindow, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("translating widget coordinates: %v", err)
|
||||||
|
}
|
||||||
|
rx0, ry0, err := app.W.ImageR.Widget.TranslateCoordinates(app.W.ScrolledWindow, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("translating widget coordinates: %v", err)
|
||||||
|
}
|
||||||
|
scrolledWindowWindow, err := app.W.ScrolledWindow.GetWindow()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting GdkWindow of ScrolledWindow widget: %v", err)
|
||||||
|
}
|
||||||
|
_, x, y, _ = scrolledWindowWindow.GetDevicePosition(pointerDevice)
|
||||||
|
xOffset, yOffset := 0, 0
|
||||||
|
var srcPixbuf *gdk.Pixbuf
|
||||||
|
if (x > lx0 && x < lx0+app.W.ImageL.GetAllocatedWidth()) &&
|
||||||
|
(y > ly0 && y < ly0+app.W.ImageL.GetAllocatedHeight()) {
|
||||||
|
srcPixbuf = app.W.ImageL.GetPixbuf()
|
||||||
|
xOffset = lx0
|
||||||
|
yOffset = ly0
|
||||||
|
} else if (x > rx0 && x < rx0+app.W.ImageR.GetAllocatedWidth()) &&
|
||||||
|
(y > ry0 && y < ry0+app.W.ImageR.GetAllocatedHeight()) {
|
||||||
|
srcPixbuf = app.W.ImageR.GetPixbuf()
|
||||||
|
xOffset = rx0
|
||||||
|
yOffset = ry0
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
targetX, targetY := x-xOffset, y-yOffset // Relative to the source Pixbuf
|
||||||
|
|
||||||
|
srcW, srcH := srcPixbuf.GetWidth(), srcPixbuf.GetHeight()
|
||||||
|
srcNChannels := srcPixbuf.GetNChannels()
|
||||||
|
srcPixels := srcPixbuf.GetPixels()
|
||||||
|
srcRowstride := srcPixbuf.GetRowstride()
|
||||||
|
|
||||||
|
// 4. Grab area around the cursor
|
||||||
|
snipDim := app.kamiteSnipDimension()
|
||||||
|
snipSourceX0, snipSourceY0 := targetX-(snipDim/2), targetY-(snipDim/2)
|
||||||
|
snipBytes := make([]int, snipDim*snipDim)
|
||||||
|
for y := 0; y < snipDim; y++ {
|
||||||
|
for x := 0; x < snipDim; x++ {
|
||||||
|
srcX, srcY := snipSourceX0+x, snipSourceY0+y
|
||||||
|
var r, g, b byte
|
||||||
|
if srcX < 0 || srcY < 0 || srcX >= srcW || srcY >= srcH {
|
||||||
|
// Beyond source Pixbuf bounds
|
||||||
|
r, g, b = 255, 255, 255
|
||||||
|
} else {
|
||||||
|
idx := srcY*srcRowstride + srcX*srcNChannels
|
||||||
|
r, g, b = srcPixels[idx], srcPixels[idx+1], srcPixels[idx+2]
|
||||||
|
}
|
||||||
|
snipBytes[y*snipDim+x] = int(r)<<16 | int(g)<<8 | int(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Send
|
||||||
|
go kamiteSendOCRImageCommand(
|
||||||
|
app.Config.KamitePort,
|
||||||
|
snipBytes,
|
||||||
|
snipDim,
|
||||||
|
snipDim,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) kamiteSnipDimension() int {
|
||||||
|
w, _ := app.getImageAreaInnerSize()
|
||||||
|
return int(kamiteRecognizeImageSnipScale * float64(w))
|
||||||
|
}
|
||||||
|
|
||||||
|
type KamiteOCRImageCommandParams struct {
|
||||||
|
Pixels string `json:"pixels"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func kamiteSendOCRImageCommand(port int, imgBytes []int, w, h int) {
|
||||||
|
stringBytes := []string{}
|
||||||
|
for i := range imgBytes {
|
||||||
|
s := strconv.Itoa(imgBytes[i])
|
||||||
|
stringBytes = append(stringBytes, s)
|
||||||
|
}
|
||||||
|
bytesString := strings.Join(stringBytes, ",")
|
||||||
|
|
||||||
|
paramsJSON, err := json.Marshal(KamiteOCRImageCommandParams{
|
||||||
|
Pixels: bytesString,
|
||||||
|
Width: w,
|
||||||
|
Height: h,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error encoding Kamite command params: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = http.PostForm(
|
||||||
|
kamiteMakeEndpointURL(port, kamiteOCRImageEndpoint),
|
||||||
|
url.Values{"params": {string(paramsJSON)}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error making HTTP request: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func kamiteMakeEndpointURL(port int, suffix string) string {
|
||||||
|
// TODO(fau): Go 1.19+ url.JoinPath()
|
||||||
|
return fmt.Sprintf(kamiteCMDEndpointBaseTpl, port) + suffix
|
||||||
|
}
|
29
md5.go
Normal file
29
md5.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
func md5String(s string) string {
|
||||||
|
hash := md5.Sum([]byte(s))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
349
menu.go
Normal file
349
menu.go
Normal file
|
@ -0,0 +1,349 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pixbuf"
|
||||||
|
"github.com/flytam/filenamify"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) menuInit() {
|
||||||
|
app.menuInitOpenDialog()
|
||||||
|
app.menuInitOpenURLDialog()
|
||||||
|
app.menuInitSaveImageDialog()
|
||||||
|
|
||||||
|
app.W.MenuItemQuit.Connect("activate", app.quit)
|
||||||
|
app.W.MenuItemClose.Connect("activate", app.archiveClose)
|
||||||
|
app.W.MenuItemNextPage.Connect("activate", app.nextPage)
|
||||||
|
app.W.MenuItemPreviousPage.Connect("activate", app.previousPage)
|
||||||
|
app.W.MenuItemFirstPage.Connect("activate", app.firstPage)
|
||||||
|
app.W.MenuItemLastPage.Connect("activate", app.lastPage)
|
||||||
|
app.W.MenuItemNextArchive.Connect("activate", app.nextArchive)
|
||||||
|
app.W.MenuItemPreviousArchive.Connect("activate", app.previousArchive)
|
||||||
|
app.W.MenuItemSkipForward.Connect("activate", app.skipForward)
|
||||||
|
app.W.MenuItemSkipBackward.Connect("activate", app.skipBackward)
|
||||||
|
|
||||||
|
app.W.MenuItemEnlarge.Connect("toggled", func() {
|
||||||
|
app.setEnlarge(app.W.MenuItemEnlarge.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemShrink.Connect("toggled", func() {
|
||||||
|
app.setShrink(app.W.MenuItemShrink.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemFullscreen.Connect("toggled", func() {
|
||||||
|
app.setFullscreen(app.W.MenuItemFullscreen.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemHideUI.Connect("toggled", func() {
|
||||||
|
app.setHideUI(app.W.MenuItemHideUI.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemSeamless.Connect("toggled", func() {
|
||||||
|
app.setSeamless(app.W.MenuItemSeamless.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemRandom.Connect("toggled", func() {
|
||||||
|
app.setRandom(app.W.MenuItemRandom.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemHFlip.Connect("toggled", func() {
|
||||||
|
app.setHFlip(app.W.MenuItemHFlip.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemVFlip.Connect("toggled", func() {
|
||||||
|
app.setVFlip(app.W.MenuItemVFlip.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemMangaMode.Connect("toggled", func() {
|
||||||
|
app.setMangaMode(app.W.MenuItemMangaMode.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemDoublePage.Connect("toggled", func() {
|
||||||
|
app.setDoublePage(app.W.MenuItemDoublePage.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemOriginal.Connect("toggled", func() {
|
||||||
|
if app.W.MenuItemOriginal.GetActive() {
|
||||||
|
app.setZoomMode("Original")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemBestFit.Connect("toggled", func() {
|
||||||
|
if app.W.MenuItemBestFit.GetActive() {
|
||||||
|
app.setZoomMode("BestFit")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemFitToWidth.Connect("toggled", func() {
|
||||||
|
if app.W.MenuItemFitToWidth.GetActive() {
|
||||||
|
app.setZoomMode("FitToWidth")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemFitToHeight.Connect("toggled", func() {
|
||||||
|
if app.W.MenuItemFitToHeight.GetActive() {
|
||||||
|
app.setZoomMode("FitToHeight")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemCopyImageToClipboard.Connect("activate", func() {
|
||||||
|
app.copyImageToClipboard()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemAddBookmark.Connect("activate", app.addBookmark)
|
||||||
|
|
||||||
|
app.W.MenuItemToggleJumpmark.Connect("activate", app.toggleJumpmark)
|
||||||
|
|
||||||
|
app.W.MenuItemCycleJumpmarksBackward.Connect("activate", func() {
|
||||||
|
app.cycleJumpmarks(cycleDirectionBackward)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemCycleJumpmarksForward.Connect("activate", func() {
|
||||||
|
app.cycleJumpmarks(cycleDirectionForward)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MenuItemJumpmarksReturnFromCycling.Connect("activate", app.returnFromCyclingJumpmarks)
|
||||||
|
|
||||||
|
app.W.MenuItemPreferences.Connect("activate", app.preferencesDialogRun)
|
||||||
|
|
||||||
|
app.W.MenuItemAbout.Connect("activate", func() {
|
||||||
|
app.S.Cursor.ForceVisible = true
|
||||||
|
app.W.AboutDialog.Run()
|
||||||
|
app.W.AboutDialog.Hide()
|
||||||
|
app.S.Cursor.ForceVisible = false
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.AboutDialog.SetLogo(pixbuf.MustLoad(aboutImg))
|
||||||
|
if len(app.S.BuildInfo.Version) >= 0 {
|
||||||
|
versionStr := fmt.Sprintf("Version: %s (built: %s)\nCompiler version: %s", app.S.BuildInfo.Version, app.S.BuildInfo.Date, runtime.Version())
|
||||||
|
app.W.AboutDialog.SetVersion(versionStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.menuSetupAccels()
|
||||||
|
|
||||||
|
app.rebuildBookmarksMenu()
|
||||||
|
|
||||||
|
app.goToDialogInit()
|
||||||
|
|
||||||
|
app.W.MenuItemGoTo.Connect("activate", app.goToDialogRun)
|
||||||
|
|
||||||
|
app.W.RecentChooserMenu.Connect("item-activated", func() {
|
||||||
|
uri := app.W.RecentChooserMenu.GetCurrentUri()
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.loadArchiveFromPath(u.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var FILE_CHOOSER_RESPONSE_ACCEPT gtk.ResponseType = 100
|
||||||
|
|
||||||
|
func (app *App) menuInitOpenDialog() {
|
||||||
|
app.W.MenuItemOpen.Connect("activate", func() {
|
||||||
|
res := gtk.ResponseType(app.W.ArchiveFileChooserDialog.Run())
|
||||||
|
app.W.ArchiveFileChooserDialog.Hide()
|
||||||
|
if res == FILE_CHOOSER_RESPONSE_ACCEPT {
|
||||||
|
filename := app.W.ArchiveFileChooserDialog.GetFilename()
|
||||||
|
if filename == "" {
|
||||||
|
var err error
|
||||||
|
filename, err = app.W.ArchiveFileChooserDialog.GetCurrentFolder()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error getting FileChooser CurrentFolder")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filename != "" {
|
||||||
|
app.loadArchiveFromPath(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := app.W.ArchiveFileChooserDialog.AddButton("_Open", FILE_CHOOSER_RESPONSE_ACCEPT)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
_, err = app.W.ArchiveFileChooserDialog.AddButton("_Cancel", gtk.RESPONSE_CANCEL)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) menuInitOpenURLDialog() {
|
||||||
|
app.W.MenuItemOpenURL.Connect("activate", func() {
|
||||||
|
res := gtk.ResponseType(app.W.OpenURLDialog.Run())
|
||||||
|
app.W.OpenURLDialog.Hide()
|
||||||
|
if res == gtk.RESPONSE_ACCEPT {
|
||||||
|
url, err := app.W.OpenURLDialogURLEntry.GetText()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting Open URL Dialog URL Entry text: %v", err)
|
||||||
|
}
|
||||||
|
referer, err := app.W.OpenURLDialogRefererEntry.GetText()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting Open URL Dialog Referer Entry text: %v", err)
|
||||||
|
}
|
||||||
|
app.loadArchiveFromURL(url, referer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.OpenURLDialogExplanationLabel.SetMarkup(
|
||||||
|
"Specify the <i>direct</i> URL of the image of one of the comic pages, for example <tt>http://my-source/comic-1234/4.png</tt>." +
|
||||||
|
" The URL must be in such format, that successive pages can be accessed by inserting their respective numbers into the URL." +
|
||||||
|
" Otherwise, the particular source is not currently supported.\n" +
|
||||||
|
" If the program fails to" +
|
||||||
|
" guess the pattern followed by the page URLs, try again, but this time manually specifying where the page number" +
|
||||||
|
" is in the URL, by replacing it with the placeholder <tt>%d</tt>. For example, if the URL for page 7 is" +
|
||||||
|
" <tt>http://my-source/comic-1234?page=7&full=true</tt>, specify <tt>http://my-source/comic-1234?page=<b>%d</b>&full=true</tt>" +
|
||||||
|
" above. If the number needs to be padded with zeroes, you can specify its width, for example <tt>%03d</tt> for (<tt>001</tt>, <tt>002</tt>, …).\n" +
|
||||||
|
" Note that certain hosts might use various access restriction measures that could make this program unable to access" +
|
||||||
|
" the images, even if they’re accessible when viewed directly on the host’s website. Below are extra options that might" +
|
||||||
|
" be useful in this connection.",
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := app.W.OpenURLDialog.AddButton("_Cancel", gtk.RESPONSE_CANCEL)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
okButton, err := app.W.OpenURLDialog.AddButton("_Open", gtk.RESPONSE_ACCEPT)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
|
||||||
|
app.W.OpenURLDialog.SetDefault(okButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) menuInitSaveImageDialog() {
|
||||||
|
app.W.MenuItemSaveImage.Connect("activate", func() {
|
||||||
|
baseName, err := filenamify.FilenamifyV2(app.archiveGetBaseName())
|
||||||
|
baseName = strings.ReplaceAll(baseName, ".", "!")
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("filenamifying archive base name: %v", err)
|
||||||
|
}
|
||||||
|
filename := fmt.Sprintf("%s-%000d.png", baseName, app.S.ArchivePos+1)
|
||||||
|
app.W.SaveImageFileChooserDialog.SetCurrentName(filename)
|
||||||
|
|
||||||
|
res := gtk.ResponseType(app.W.SaveImageFileChooserDialog.Run())
|
||||||
|
app.W.SaveImageFileChooserDialog.Hide()
|
||||||
|
if res == gtk.RESPONSE_ACCEPT {
|
||||||
|
filename := app.W.SaveImageFileChooserDialog.GetFilename()
|
||||||
|
if filename != "" {
|
||||||
|
app.saveImage(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := app.W.SaveImageFileChooserDialog.AddButton("_Save", gtk.RESPONSE_ACCEPT)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
_, err = app.W.SaveImageFileChooserDialog.AddButton("_Cancel", gtk.RESPONSE_CANCEL)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) menuSetupAccels() {
|
||||||
|
// NOTE: This can't be done in the glade file using the <accelerator> tag under the respective
|
||||||
|
// menu items, because then the bindings stop working when the menubar is hidden.
|
||||||
|
// Unfortunately, only primary keybindings can be set here. Auxilliary ones are bound under
|
||||||
|
// the MainWindow key-press-event signal handler.
|
||||||
|
accels := []MenuWithAccels{
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuFile,
|
||||||
|
Path: menuMakeAccelPath("File"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemOpen, Accel{gdk.KEY_O, gdk.CONTROL_MASK}},
|
||||||
|
{app.W.MenuItemOpenURL, Accel{gdk.KEY_O, gdk.CONTROL_MASK | gdk.SHIFT_MASK}},
|
||||||
|
{app.W.MenuItemClose, Accel{gdk.KEY_W, gdk.CONTROL_MASK}},
|
||||||
|
{app.W.MenuItemSaveImage, Accel{gdk.KEY_F9, 0}},
|
||||||
|
{app.W.MenuItemQuit, Accel{gdk.KEY_Q, gdk.CONTROL_MASK}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuEdit,
|
||||||
|
Path: menuMakeAccelPath("Edit"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemCopyImageToClipboard, Accel{gdk.KEY_C, gdk.CONTROL_MASK}},
|
||||||
|
{app.W.MenuItemPreferences, Accel{gdk.KEY_P, gdk.CONTROL_MASK}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuView,
|
||||||
|
Path: menuMakeAccelPath("View"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{&app.W.MenuItemHideUI.MenuItem, Accel{gdk.KEY_M, gdk.MOD1_MASK}},
|
||||||
|
{&app.W.MenuItemShrink.MenuItem, Accel{gdk.KEY_S, 0}},
|
||||||
|
{&app.W.MenuItemEnlarge.MenuItem, Accel{gdk.KEY_E, 0}},
|
||||||
|
{&app.W.MenuItemBestFit.MenuItem, Accel{gdk.KEY_B, 0}},
|
||||||
|
{&app.W.MenuItemOriginal.MenuItem, Accel{gdk.KEY_O, 0}},
|
||||||
|
{&app.W.MenuItemFitToWidth.MenuItem, Accel{gdk.KEY_W, 0}},
|
||||||
|
{&app.W.MenuItemFitToHeight.MenuItem, Accel{gdk.KEY_H, 0}},
|
||||||
|
{&app.W.MenuItemFullscreen.MenuItem, Accel{gdk.KEY_F, 0}},
|
||||||
|
{&app.W.MenuItemRandom.MenuItem, Accel{gdk.KEY_R, 0}},
|
||||||
|
{&app.W.MenuItemDoublePage.MenuItem, Accel{gdk.KEY_D, 0}},
|
||||||
|
{&app.W.MenuItemVFlip.MenuItem, Accel{gdk.KEY_V, 0}},
|
||||||
|
{&app.W.MenuItemHFlip.MenuItem, Accel{gdk.KEY_V, gdk.SHIFT_MASK}},
|
||||||
|
{&app.W.MenuItemMangaMode.MenuItem, Accel{gdk.KEY_M, gdk.CONTROL_MASK}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuNavigation,
|
||||||
|
Path: menuMakeAccelPath("Navigation"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemPreviousPage, Accel{gdk.KEY_Page_Up, 0}},
|
||||||
|
{app.W.MenuItemNextPage, Accel{gdk.KEY_Page_Down, 0}},
|
||||||
|
{app.W.MenuItemFirstPage, Accel{gdk.KEY_Home, 0}},
|
||||||
|
{app.W.MenuItemLastPage, Accel{gdk.KEY_End, 0}},
|
||||||
|
{app.W.MenuItemPreviousArchive, Accel{gdk.KEY_Page_Up, gdk.CONTROL_MASK}},
|
||||||
|
{app.W.MenuItemNextArchive, Accel{gdk.KEY_Page_Down, gdk.CONTROL_MASK}},
|
||||||
|
{app.W.MenuItemGoTo, Accel{gdk.KEY_G, 0}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuBookmarks,
|
||||||
|
Path: menuMakeAccelPath("Bookmarks"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemAddBookmark, Accel{gdk.KEY_B, gdk.CONTROL_MASK}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuJumpmarks,
|
||||||
|
Path: menuMakeAccelPath("Jumpmarks"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemToggleJumpmark, Accel{gdk.KEY_M, 0}},
|
||||||
|
{app.W.MenuItemCycleJumpmarksBackward, Accel{gdk.KEY_bracketleft, 0}},
|
||||||
|
{app.W.MenuItemCycleJumpmarksForward, Accel{gdk.KEY_bracketright, 0}},
|
||||||
|
{app.W.MenuItemJumpmarksReturnFromCycling, Accel{gdk.KEY_BackSpace, 0}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Menu: app.W.MenuAbout,
|
||||||
|
Path: menuMakeAccelPath("About"),
|
||||||
|
Items: []MenuItemWithAccels{
|
||||||
|
{app.W.MenuItemAbout, Accel{gdk.KEY_F1, 0}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := setupMenuAccels(app.W.MainWindow, accels)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("setting up menu accels: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func menuMakeAccelPath(menuCategory string) string {
|
||||||
|
return fmt.Sprintf("<gomicsv>/%s", menuCategory)
|
||||||
|
}
|
136
natsort/sort.go
Normal file
136
natsort/sort.go
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package natsort provides an implementation of the natural sorting algorithm.
|
||||||
|
// See http://blog.codinghorror.com/sorting-for-humans-natural-sort-order/.
|
||||||
|
package natsort
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Strings sorts a slice of strings with Less.
|
||||||
|
func Strings(s []string) {
|
||||||
|
sort.Sort(stringSlice(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Less reports whether s is less than t.
|
||||||
|
func Less(s, t string) bool {
|
||||||
|
return LessRunes([]rune(s), []rune(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
// LessRunes reports whether s is less than t.
|
||||||
|
func LessRunes(s, t []rune) bool {
|
||||||
|
nprefix := commonPrefix(s, t)
|
||||||
|
if len(s) == nprefix && len(t) == nprefix {
|
||||||
|
// equal
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sEnd := leadDigits(s[nprefix:]) + nprefix
|
||||||
|
tEnd := leadDigits(t[nprefix:]) + nprefix
|
||||||
|
if sEnd > nprefix || tEnd > nprefix {
|
||||||
|
start := trailDigits(s[:nprefix])
|
||||||
|
if sEnd-start > 0 && tEnd-start > 0 {
|
||||||
|
// TODO(light): log errors?
|
||||||
|
sn := atoi(s[start:sEnd])
|
||||||
|
tn := atoi(t[start:tEnd])
|
||||||
|
if sn != tn {
|
||||||
|
return sn < tn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(s) == nprefix:
|
||||||
|
return true
|
||||||
|
case len(t) == nprefix:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return s[nprefix] < t[nprefix]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(r rune) bool {
|
||||||
|
return '0' <= r && r <= '9'
|
||||||
|
}
|
||||||
|
|
||||||
|
func atoi(r []rune) uint64 {
|
||||||
|
if len(r) < 1 {
|
||||||
|
panic(errors.New("atoi got an empty slice"))
|
||||||
|
}
|
||||||
|
const cutoff = uint64((1<<64-1)/10 + 1)
|
||||||
|
const maxVal = 1<<64 - 1
|
||||||
|
|
||||||
|
var n uint64
|
||||||
|
for _, d := range r {
|
||||||
|
v := uint64(d - '0')
|
||||||
|
if n >= cutoff {
|
||||||
|
return 1<<64 - 1
|
||||||
|
}
|
||||||
|
n *= 10
|
||||||
|
n1 := n + v
|
||||||
|
if n1 < n || n1 > maxVal {
|
||||||
|
// n+v overflows
|
||||||
|
return 1<<64 - 1
|
||||||
|
}
|
||||||
|
n = n1
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func commonPrefix(s, t []rune) int {
|
||||||
|
for i := range s {
|
||||||
|
if i >= len(t) {
|
||||||
|
return len(t)
|
||||||
|
}
|
||||||
|
if s[i] != t[i] {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trailDigits(r []rune) int {
|
||||||
|
for i := len(r) - 1; i >= 0; i-- {
|
||||||
|
if !isDigit(r[i]) {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func leadDigits(r []rune) int {
|
||||||
|
for i := range r {
|
||||||
|
if !isDigit(r[i]) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringSlice []string
|
||||||
|
|
||||||
|
func (ss stringSlice) Len() int {
|
||||||
|
return len(ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss stringSlice) Less(i, j int) bool {
|
||||||
|
return Less(ss[i], ss[j])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss stringSlice) Swap(i, j int) {
|
||||||
|
ss[i], ss[j] = ss[j], ss[i]
|
||||||
|
}
|
94
natsort/sort_test.go
Normal file
94
natsort/sort_test.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2014 Google Inc. All rights reserved.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package natsort
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLess(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s, t string
|
||||||
|
want cmp
|
||||||
|
}{
|
||||||
|
{"", "", eq},
|
||||||
|
{"a", "", gt},
|
||||||
|
{"a", "a", eq},
|
||||||
|
{"1", "10", lt},
|
||||||
|
{"20", "3", gt},
|
||||||
|
{"a1", "a10", lt},
|
||||||
|
{"a2", "a10", lt},
|
||||||
|
{"a5b2", "a5b7", lt},
|
||||||
|
{"a50b2", "a6b7", gt},
|
||||||
|
{"世20", "世界3", lt},
|
||||||
|
{"50a", "50b", lt},
|
||||||
|
{"a50", "a050", gt},
|
||||||
|
{"a01b3", "a1b2", lt},
|
||||||
|
{"thx1138", "thx1138", eq},
|
||||||
|
{"thx1138a", "thx1138b", lt},
|
||||||
|
{"thx1138a", "thx1138", gt},
|
||||||
|
|
||||||
|
// a < a0 < a1 < a1a < a1b < a2 < a10 < a20
|
||||||
|
{"a", "a0", lt},
|
||||||
|
{"a0", "a1", lt},
|
||||||
|
{"a1", "a1a", lt},
|
||||||
|
{"a1a", "a1b", lt},
|
||||||
|
{"a2", "a10", lt},
|
||||||
|
{"a10", "a20", lt},
|
||||||
|
|
||||||
|
// 1.001 < 1.002 < 1.010 < 1.02 < 1.1 < 1.3
|
||||||
|
{"1.001", "1.002", lt},
|
||||||
|
{"1.002", "1.010", lt},
|
||||||
|
{"1.010", "1.02", gt}, // TODO(light): should this be lt?
|
||||||
|
{"1.02", "1.1", gt}, // TODO(light): should this be lt?
|
||||||
|
{"1.1", "1.3", lt},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
v := bit(Less(test.s, test.t))
|
||||||
|
v |= bit(Less(test.t, test.s)) << 1
|
||||||
|
if cmp(v) != test.want {
|
||||||
|
t.Errorf("%[1]q %[3]v %[2]q, want %[1]q %[4]v %[2]q", test.s, test.t, cmp(v), test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bit(b bool) int {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmp int
|
||||||
|
|
||||||
|
const (
|
||||||
|
eq cmp = iota
|
||||||
|
lt
|
||||||
|
gt
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c cmp) String() string {
|
||||||
|
switch c {
|
||||||
|
case lt:
|
||||||
|
return "<"
|
||||||
|
case gt:
|
||||||
|
return ">"
|
||||||
|
case eq:
|
||||||
|
return "=="
|
||||||
|
default:
|
||||||
|
return "<>"
|
||||||
|
}
|
||||||
|
}
|
330
navigation.go
Normal file
330
navigation.go
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/archive"
|
||||||
|
"github.com/fauu/gomicsv/imgdiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) randomPage() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if app.S.Archive.Len() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setPage(rand.Int() % *app.S.Archive.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) previousPage() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
if app.Config.Seamless {
|
||||||
|
app.previousArchive()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.Random && app.S.Archive.Len() != nil {
|
||||||
|
app.randomPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 1
|
||||||
|
if app.Config.DoublePage && app.S.ArchivePos > 1 {
|
||||||
|
n = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.Seamless && app.S.ArchivePos+1 <= n {
|
||||||
|
app.previousArchive()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setPage(app.S.ArchivePos - n)
|
||||||
|
|
||||||
|
if app.Config.DoublePage &&
|
||||||
|
app.shouldForceSinglePage() &&
|
||||||
|
app.S.Archive.Len() != nil &&
|
||||||
|
*app.S.Archive.Len()-app.S.ArchivePos > 1 {
|
||||||
|
|
||||||
|
app.nextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) nextPage() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
if app.Config.Seamless {
|
||||||
|
app.nextArchive()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.Random && app.S.Archive.Len() != nil {
|
||||||
|
app.randomPage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n := 1
|
||||||
|
if app.Config.DoublePage &&
|
||||||
|
!app.shouldForceSinglePage() &&
|
||||||
|
app.S.Archive.Len() != nil &&
|
||||||
|
*app.S.Archive.Len() > app.S.ArchivePos+2 {
|
||||||
|
|
||||||
|
n = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.Config.Seamless &&
|
||||||
|
app.S.Archive.Len() != nil &&
|
||||||
|
*app.S.Archive.Len()-app.S.ArchivePos <= n {
|
||||||
|
|
||||||
|
app.nextArchive()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setPage(app.S.ArchivePos + n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) firstPage() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.setPage(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) lastPage() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.Archive.Len() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := -1
|
||||||
|
if app.Config.DoublePage && *app.S.Archive.Len() >= 2 {
|
||||||
|
offset = -2
|
||||||
|
}
|
||||||
|
app.setPage(*app.S.Archive.Len() + offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) imageHash(n int) (imgdiff.Hash, bool) {
|
||||||
|
if hash, ok := app.S.ImageHashes[n]; ok {
|
||||||
|
return hash, true
|
||||||
|
}
|
||||||
|
|
||||||
|
pixbuf, err := app.S.Archive.Load(n, app.Config.EmbeddedOrientation, 0)
|
||||||
|
if err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgdiff.DHash(pixbuf), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) skipForward() {
|
||||||
|
app.setPage(app.S.ArchivePos + app.Config.NSkip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) skipBackward() {
|
||||||
|
app.setPage(app.S.ArchivePos - app.Config.NSkip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) nextScene() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.Archive.Len() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.PixbufL == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash := imgdiff.DHash(app.S.PixbufL)
|
||||||
|
|
||||||
|
dn := app.Config.SceneScanSkip
|
||||||
|
if *app.S.Archive.Len()-1-app.S.ArchivePos <= dn {
|
||||||
|
dn = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := app.S.ArchivePos + 1; n < *app.S.Archive.Len(); n += dn {
|
||||||
|
h, ok := app.imageHash(n)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
distance := float32(imgdiff.Distance(hash, h)) / 64
|
||||||
|
|
||||||
|
if distance > app.Config.ImageDiffThres {
|
||||||
|
if dn == 1 || n == app.S.ArchivePos+1 {
|
||||||
|
app.doSetPage(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did we go too fast?
|
||||||
|
for l := n - 1; l >= app.S.ArchivePos+1; l-- {
|
||||||
|
h, ok := app.imageHash(l)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d := float32(imgdiff.Distance(hash, h)) / 64
|
||||||
|
if d <= app.Config.ImageDiffThres {
|
||||||
|
app.doSetPage(l + 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) previousScene() {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.PixbufL == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash := imgdiff.DHash(app.S.PixbufL)
|
||||||
|
|
||||||
|
dn := app.Config.SceneScanSkip
|
||||||
|
if app.S.ArchivePos <= dn {
|
||||||
|
dn = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for n := app.S.ArchivePos - 1; n >= 0; n -= dn {
|
||||||
|
h, ok := app.imageHash(n)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
distance := float32(imgdiff.Distance(hash, h)) / 64
|
||||||
|
|
||||||
|
if distance > app.Config.ImageDiffThres {
|
||||||
|
if dn == 1 || n == app.S.ArchivePos-1 {
|
||||||
|
app.doSetPage(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did we go too fast?
|
||||||
|
for l := n + 1; l <= app.S.ArchivePos-1; l++ {
|
||||||
|
h, ok := app.imageHash(l)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d := float32(imgdiff.Distance(hash, h)) / 64
|
||||||
|
if d <= app.Config.ImageDiffThres {
|
||||||
|
app.doSetPage(l - 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(fau): Distinguish a failiure from the "no next archive" condition and inform the user accordingly
|
||||||
|
func (app *App) nextArchive() bool {
|
||||||
|
newName, err := app.archiveNameRelativeToCurrent(1)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting next archive: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
app.loadArchiveFromPath(newName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(fau): Distinguish a failiure from the "no previous archive" condition and inform the user accordingly
|
||||||
|
func (app *App) previousArchive() bool {
|
||||||
|
newName, err := app.archiveNameRelativeToCurrent(-1)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error getting previous archive: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
app.loadArchiveFromPath(newName)
|
||||||
|
app.lastPage()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// currentArchiveIdx determines the index of the current archive in the directory. We need to do
|
||||||
|
// this every time, since the filesystem is mutable
|
||||||
|
func (app *App) currentArchiveIdx() (idx int, err error) {
|
||||||
|
dir, name := filepath.Split(app.S.ArchivePath)
|
||||||
|
if dir == "" {
|
||||||
|
dir, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arNames, err := archive.ListInDirectory(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
idx = -1
|
||||||
|
for i := 0; i < len(arNames); i++ {
|
||||||
|
if arNames[i] == name {
|
||||||
|
idx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx == -1 {
|
||||||
|
return 0, errors.New("Couldn't find the current archive in the current dir. Deleted, perhaps?")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// archiveNameRelativeToCurrent gets the name of the archive in the current directory whose
|
||||||
|
// relative position with regards to the current archive is equal to relIdx
|
||||||
|
// TODO(utkan): Use inotify to avoid obtaining list from the scratch all the time
|
||||||
|
func (app *App) archiveNameRelativeToCurrent(relIdx int) (newName string, err error) {
|
||||||
|
dir, _ := filepath.Split(app.S.ArchivePath)
|
||||||
|
if dir == "" {
|
||||||
|
dir, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arNames, err := archive.ListInDirectory(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currIdx, err := app.currentArchiveIdx()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := currIdx + relIdx
|
||||||
|
if idx < 0 || idx >= len(arNames) {
|
||||||
|
err = errors.New("No more archives in the directory")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newName = filepath.Join(dir, arNames[idx])
|
||||||
|
return
|
||||||
|
}
|
63
notification.go
Normal file
63
notification.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import "github.com/gotk3/gotk3/glib"
|
||||||
|
|
||||||
|
var (
|
||||||
|
notificationCloseAfterS = map[NotificationLength]uint{
|
||||||
|
ShortNotification: 2,
|
||||||
|
LongNotification: 8,
|
||||||
|
}
|
||||||
|
notificationCloseSourceHandle *glib.SourceHandle
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationLength = int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShortNotification = iota
|
||||||
|
LongNotification
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) notificationInit() {
|
||||||
|
app.W.NotificationCloseButton.Connect("clicked", func() {
|
||||||
|
app.W.NotificationRevealer.SetRevealChild(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) notificationShow(text string, length NotificationLength) {
|
||||||
|
app.W.NotificationLabel.SetText(text)
|
||||||
|
app.W.NotificationRevealer.SetRevealChild(true)
|
||||||
|
|
||||||
|
// Cancel previous close timeout
|
||||||
|
if notificationCloseSourceHandle != nil {
|
||||||
|
glib.SourceRemove(*notificationCloseSourceHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a timeout to automatically close the notificaiton
|
||||||
|
handle := glib.TimeoutSecondsAdd(notificationCloseAfterS[length], func() {
|
||||||
|
app.notificationHide()
|
||||||
|
})
|
||||||
|
notificationCloseSourceHandle = &handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) notificationHide() {
|
||||||
|
app.W.NotificationRevealer.SetRevealChild(false)
|
||||||
|
notificationCloseSourceHandle = nil
|
||||||
|
}
|
140
page.go
Normal file
140
page.go
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pagecache"
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pageCacheTrimInterval = 3 * time.Minute
|
||||||
|
preloadedPageKeepAtLeastFor = 7 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) setPage(n int) {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < 0 {
|
||||||
|
n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.S.Archive.Len() != nil && n >= *app.S.Archive.Len() {
|
||||||
|
n = *app.S.Archive.Len() - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == app.S.ArchivePos {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrev := false
|
||||||
|
if n == app.S.ArchivePos-1 || (app.Config.DoublePage && n == app.S.ArchivePos-2) {
|
||||||
|
isPrev = true
|
||||||
|
}
|
||||||
|
|
||||||
|
app.doSetPage(n)
|
||||||
|
|
||||||
|
var scrollFunc func()
|
||||||
|
if isPrev {
|
||||||
|
scrollFunc = app.scrollToEnd
|
||||||
|
} else {
|
||||||
|
scrollFunc = app.scrollToStart
|
||||||
|
}
|
||||||
|
glib.TimeoutAdd(0, scrollFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) doSetPage(n int) {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.jumpmarksHandleSetPage(n - 1)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
app.S.PixbufL, err = app.S.Archive.Load(n, app.Config.EmbeddedOrientation, app.Config.NPreload)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.ArchivePos = n
|
||||||
|
|
||||||
|
app.S.PixbufR = nil
|
||||||
|
if app.Config.DoublePage && (app.S.Archive.Len() == nil || *app.S.Archive.Len() > n+1) {
|
||||||
|
app.S.PixbufR, err = app.S.Archive.Load(n+1, app.Config.EmbeddedOrientation, app.Config.NPreload)
|
||||||
|
if err != nil {
|
||||||
|
app.showError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.GC()
|
||||||
|
|
||||||
|
app.blit()
|
||||||
|
app.updateStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) trimPageCache() {
|
||||||
|
if app.S.PageCache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark pages as no longer needed for preload unless they're recently fetched or near the current page
|
||||||
|
var preloadDontKeep []int
|
||||||
|
nearKeepStart := app.S.ArchivePos - app.Config.NPreload
|
||||||
|
nearKeepEnd := app.S.ArchivePos + app.Config.NPreload
|
||||||
|
for i, entry := range app.S.PageCache.Pages {
|
||||||
|
if time.Since(entry.Time) > preloadedPageKeepAtLeastFor && (i < nearKeepStart || i > nearKeepEnd) {
|
||||||
|
preloadDontKeep = append(preloadDontKeep, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(preloadDontKeep) > 0 {
|
||||||
|
app.S.PageCache.DontKeepSlice(preloadDontKeep, pagecache.KeepReasonPreload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim unneeded pages
|
||||||
|
app.S.PageCache.Trim()
|
||||||
|
|
||||||
|
// Log cache status
|
||||||
|
var indices []int
|
||||||
|
for i := range app.S.PageCache.Pages {
|
||||||
|
indices = append(indices, i+1)
|
||||||
|
}
|
||||||
|
sort.Ints(indices)
|
||||||
|
var strIndices []string
|
||||||
|
for _, index := range indices {
|
||||||
|
strIndices = append(strIndices, fmt.Sprint(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainingStr string
|
||||||
|
if len(strIndices) == 0 {
|
||||||
|
remainingStr = "-"
|
||||||
|
} else {
|
||||||
|
remainingStr = strings.Join(strIndices, ", ")
|
||||||
|
}
|
||||||
|
log.Printf("Trimmed page cache. Remaining pages: %s", remainingStr)
|
||||||
|
}
|
138
pagecache/page_cache.go
Normal file
138
pagecache/page_cache.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pagecache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeepReason uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeepReasonPreload KeepReason = 1 << iota
|
||||||
|
KeepReasonJumpmark
|
||||||
|
)
|
||||||
|
|
||||||
|
type PageCache struct {
|
||||||
|
Pages map[int]CachedPage
|
||||||
|
pagesMutex sync.Mutex
|
||||||
|
keep map[int]KeepReason
|
||||||
|
keepMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPageCache() PageCache {
|
||||||
|
return PageCache{
|
||||||
|
make(map[int]CachedPage),
|
||||||
|
sync.Mutex{},
|
||||||
|
make(map[int]KeepReason),
|
||||||
|
sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CachedPage struct {
|
||||||
|
Pixbuf *gdk.Pixbuf
|
||||||
|
Time time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCachedPage(pixbuf *gdk.Pixbuf) CachedPage {
|
||||||
|
return CachedPage{pixbuf, time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) Get(i int) (*CachedPage, bool) {
|
||||||
|
cache.pagesMutex.Lock()
|
||||||
|
defer cache.pagesMutex.Unlock()
|
||||||
|
if page, ok := cache.Pages[i]; ok {
|
||||||
|
return &page, true
|
||||||
|
} else {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) Insert(i int, pixbuf *gdk.Pixbuf, keepReason KeepReason) {
|
||||||
|
cache.set(i, pixbuf)
|
||||||
|
cache.Keep(i, keepReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) Keep(i int, reason KeepReason) {
|
||||||
|
cache.keepMutex.Lock()
|
||||||
|
defer cache.keepMutex.Unlock()
|
||||||
|
cache.keep[i] |= reason
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) DontKeep(i int, reason KeepReason) {
|
||||||
|
cache.keepMutex.Lock()
|
||||||
|
defer cache.keepMutex.Unlock()
|
||||||
|
cache.doDontKeep(i, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) DontKeepSlice(indices []int, reason KeepReason) {
|
||||||
|
cache.keepMutex.Lock()
|
||||||
|
defer cache.keepMutex.Unlock()
|
||||||
|
for _, i := range indices {
|
||||||
|
cache.doDontKeep(i, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) doDontKeep(i int, reason KeepReason) {
|
||||||
|
keep, ok := cache.keep[i]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keep &^= reason
|
||||||
|
if keep == 0 {
|
||||||
|
delete(cache.keep, i)
|
||||||
|
} else {
|
||||||
|
cache.keep[i] = keep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) Trim() {
|
||||||
|
var toRemove []int
|
||||||
|
for i := range cache.Pages {
|
||||||
|
if cache.keep[i] == 0 {
|
||||||
|
toRemove = append(toRemove, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(toRemove) > 0 {
|
||||||
|
cache.remove(toRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) set(i int, pixbuf *gdk.Pixbuf) {
|
||||||
|
cache.pagesMutex.Lock()
|
||||||
|
defer cache.pagesMutex.Unlock()
|
||||||
|
if pixbuf == nil {
|
||||||
|
delete(cache.Pages, i)
|
||||||
|
} else {
|
||||||
|
cache.Pages[i] = NewCachedPage(pixbuf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cache *PageCache) remove(indices []int) {
|
||||||
|
cache.pagesMutex.Lock()
|
||||||
|
defer cache.pagesMutex.Unlock()
|
||||||
|
for _, i := range indices {
|
||||||
|
delete(cache.Pages, i)
|
||||||
|
}
|
||||||
|
util.GC()
|
||||||
|
}
|
55
pixbuf/pixbuf.go
Normal file
55
pixbuf/pixbuf.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package pixbuf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Load(r io.Reader, autorotate bool) (*gdk.Pixbuf, error) {
|
||||||
|
w, _ := gdk.PixbufLoaderNew()
|
||||||
|
defer w.Close()
|
||||||
|
_, err := io.Copy(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pixbuf, err := w.GetPixbuf()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autorotate {
|
||||||
|
return pixbuf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixbuf.ApplyEmbeddedOrientation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func MustLoad(data []byte) *gdk.Pixbuf {
|
||||||
|
pixbuf, err := Load(bytes.NewBuffer(data), true)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("loading pixbuf: %v", err)
|
||||||
|
}
|
||||||
|
return pixbuf
|
||||||
|
}
|
122
preferences.go
Normal file
122
preferences.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) preferencesInit() {
|
||||||
|
_, err := app.W.PreferencesDialog.AddButton("_OK", gtk.RESPONSE_ACCEPT)
|
||||||
|
checkDialogAddButtonErr(err)
|
||||||
|
|
||||||
|
app.W.BackgroundColorButton.Connect("color-set", func(self *glib.Object) {
|
||||||
|
chooser := >k.ColorChooser{
|
||||||
|
Object: self,
|
||||||
|
}
|
||||||
|
app.setBackgroundColor(NewColorFromGdkRGBA(chooser.GetRGBA()))
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.PagesToSkipSpinButton.SetRange(1, 100)
|
||||||
|
app.W.PagesToSkipSpinButton.SetIncrements(1, 10)
|
||||||
|
app.W.PagesToSkipSpinButton.SetValue(float64(app.Config.NSkip))
|
||||||
|
app.W.PagesToSkipSpinButton.Connect("value-changed", func(self *gtk.SpinButton) {
|
||||||
|
app.Config.NSkip = int(self.GetValue())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.InterpolationComboBoxText.Connect("changed", func(self *gtk.ComboBoxText) {
|
||||||
|
app.setInterpolation(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.RememberRecentCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setRememberRecent(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.RememberPositionCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setRememberPosition(self.GetActive())
|
||||||
|
app.W.RememberPositionHTTPCheckButton.SetSensitive(self.GetActive())
|
||||||
|
if !self.GetActive() {
|
||||||
|
app.W.RememberPositionHTTPCheckButton.SetActive(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.RememberPositionHTTPCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setRememberPositionHTTP(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.OneWideCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setOneWide(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.SmartScrollCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setSmartScroll(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.EmbeddedOrientationCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setEmbeddedOrientation(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.HideIdleCursorCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setHideIdleCursor(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.KamiteEnabledCheckButton.Connect("toggled", func(self *gtk.CheckButton) {
|
||||||
|
app.setKamiteEnabled(self.GetActive())
|
||||||
|
app.W.KamitePortContainer.SetSensitive(self.GetActive())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.KamitePortEntry.Connect("insert-text", func(self *gtk.Entry, text string) {
|
||||||
|
// Allow only digits
|
||||||
|
for _, c := range text {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
self.StopEmission("insert-text")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
app.W.KamitePortEntry.Connect("changed", func(self *gtk.Entry) {
|
||||||
|
text, err := self.GetText()
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("getting Kamite port entry text: %v", err)
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
// POLISH: Restore default value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(text)
|
||||||
|
if err != nil {
|
||||||
|
log.Panicf("parsing Kamite port entry as int: %v", err)
|
||||||
|
}
|
||||||
|
app.setKamitePort(port)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) preferencesDialogRun() {
|
||||||
|
app.S.Cursor.ForceVisible = true
|
||||||
|
res := gtk.ResponseType(app.W.PreferencesDialog.Run())
|
||||||
|
app.W.PreferencesDialog.Hide()
|
||||||
|
if res == gtk.RESPONSE_ACCEPT {
|
||||||
|
app.saveConfig()
|
||||||
|
}
|
||||||
|
app.S.Cursor.ForceVisible = false
|
||||||
|
}
|
67
read_later.go
Normal file
67
read_later.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/archive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) maybeSaveReadingPosition() {
|
||||||
|
var http = app.S.Archive.Kind() == archive.HTTPKind
|
||||||
|
if (!http && !app.Config.RememberPosition) ||
|
||||||
|
(http && !app.Config.RememberPositionHTTP) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
readLaterFilePath := app.readLaterFilePath(app.S.ArchivePath)
|
||||||
|
f, err := os.Create(readLaterFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error creating read later file '%s': %v", readLaterFilePath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
fmt.Fprintf(f, "archive-pos=%d", app.S.ArchivePos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) loadReadingPosition(archivePath string) (int, error) {
|
||||||
|
readLaterFilePath := app.readLaterFilePath(archivePath)
|
||||||
|
f, err := os.Open(readLaterFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("opening read later file '%s': %v", readLaterFilePath, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var archivePos int
|
||||||
|
_, err = fmt.Fscanf(f, "archive-pos=%d", &archivePos)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("reading 'archive-pos' from read later file '%s': %v", readLaterFilePath, err)
|
||||||
|
}
|
||||||
|
return archivePos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) readLaterFilePath(archivePath string) string {
|
||||||
|
filename := strings.ToUpper(md5String(archivePath))
|
||||||
|
return filepath.Join(app.S.ReadLaterDirPath, filename)
|
||||||
|
}
|
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 262 KiB |
139
scrolling.go
Normal file
139
scrolling.go
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DragScroll struct {
|
||||||
|
InProgress bool
|
||||||
|
StartX float64
|
||||||
|
StartY float64
|
||||||
|
StartHAdjustmentVal float64
|
||||||
|
StartVAdjustmentVal float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) scroll(dx, dy float64) {
|
||||||
|
if !app.archiveIsLoaded() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imgw, imgh := app.getImageAreaInnerSize()
|
||||||
|
|
||||||
|
vadj := app.W.ScrolledWindow.GetVAdjustment()
|
||||||
|
hadj := app.W.ScrolledWindow.GetHAdjustment()
|
||||||
|
|
||||||
|
vIncrement := vadj.GetMinimumIncrement()
|
||||||
|
vVal := vadj.GetValue()
|
||||||
|
vMin := vadj.GetLower()
|
||||||
|
vMax := vadj.GetUpper() - float64(imgh) - 2
|
||||||
|
|
||||||
|
hIncrement := hadj.GetMinimumIncrement()
|
||||||
|
hVal := hadj.GetValue()
|
||||||
|
hMin := hadj.GetLower()
|
||||||
|
hMax := hadj.GetUpper() - float64(imgw) - 2
|
||||||
|
|
||||||
|
if dy > 0 {
|
||||||
|
if vVal >= vMax {
|
||||||
|
if app.Config.SmartScroll {
|
||||||
|
if app.S.SmartScrollInProgress {
|
||||||
|
app.nextPage()
|
||||||
|
app.S.SmartScrollInProgress = false
|
||||||
|
} else {
|
||||||
|
app.S.SmartScrollInProgress = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vadj.SetValue(util.Clamp(vVal+vIncrement, vMin, vMax))
|
||||||
|
app.W.ScrolledWindow.SetVAdjustment(vadj)
|
||||||
|
app.S.SmartScrollInProgress = false
|
||||||
|
}
|
||||||
|
} else if dy < 0 {
|
||||||
|
if vVal <= vMin {
|
||||||
|
if app.Config.SmartScroll {
|
||||||
|
if app.S.SmartScrollInProgress {
|
||||||
|
app.previousPage()
|
||||||
|
app.S.SmartScrollInProgress = false
|
||||||
|
} else {
|
||||||
|
app.S.SmartScrollInProgress = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vadj.SetValue(util.Clamp(vVal-vIncrement, vMin, vMax))
|
||||||
|
app.W.ScrolledWindow.SetVAdjustment(vadj)
|
||||||
|
app.S.SmartScrollInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dx > 0 && hVal < hMax {
|
||||||
|
hadj.SetValue(util.Clamp(hVal+hIncrement, hMin, hMax))
|
||||||
|
app.W.ScrolledWindow.SetHAdjustment(hadj)
|
||||||
|
} else if dx < 0 && hVal > hMin {
|
||||||
|
hadj.SetValue(util.Clamp(hVal-hIncrement, hMin, hMax))
|
||||||
|
app.W.ScrolledWindow.SetHAdjustment(hadj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) scrollToStart() {
|
||||||
|
app.W.ScrolledWindow.GetVAdjustment().SetValue(0) // Vertical: top
|
||||||
|
|
||||||
|
var newHadj float64 = 0
|
||||||
|
if app.Config.MangaMode {
|
||||||
|
imgw, _ := app.getImageAreaInnerSize()
|
||||||
|
newHadj = float64(imgw)
|
||||||
|
}
|
||||||
|
app.W.ScrolledWindow.GetHAdjustment().SetValue(newHadj) // Horizontal: left (non-manga) or right (manga) edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) scrollToEnd() {
|
||||||
|
imgw, imgh := app.getImageAreaInnerSize()
|
||||||
|
|
||||||
|
app.W.ScrolledWindow.GetVAdjustment().SetValue(float64(imgh)) // Vertical: bottom
|
||||||
|
|
||||||
|
var newHadj float64 = 0
|
||||||
|
if !app.Config.MangaMode {
|
||||||
|
newHadj = float64(imgw)
|
||||||
|
}
|
||||||
|
app.W.ScrolledWindow.GetHAdjustment().SetValue(newHadj) // Horizontal: left (manga) or right (non-manga) edge
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) dragScrollStart(x, y, vAdjustmentVal, hAdjustmentVal float64) {
|
||||||
|
app.S.DragScroll.InProgress = true
|
||||||
|
app.S.DragScroll.StartX = x
|
||||||
|
app.S.DragScroll.StartY = y
|
||||||
|
app.S.DragScroll.StartVAdjustmentVal = vAdjustmentVal
|
||||||
|
app.S.DragScroll.StartHAdjustmentVal = hAdjustmentVal
|
||||||
|
app.setCursor(cursorGrabbing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) dragScrollUpdate(sw *gtk.ScrolledWindow, event *gdk.EventButton) {
|
||||||
|
deltaX := app.S.DragScroll.StartX - event.X()
|
||||||
|
sw.GetHAdjustment().SetValue(app.S.DragScroll.StartHAdjustmentVal + deltaX)
|
||||||
|
deltaY := app.S.DragScroll.StartY - event.Y()
|
||||||
|
sw.GetVAdjustment().SetValue(app.S.DragScroll.StartVAdjustmentVal + deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) dragScrollEnd() {
|
||||||
|
app.S.DragScroll.InProgress = false
|
||||||
|
app.setCursor(cursorDefault)
|
||||||
|
}
|
63
standard_paths_linux.go
Normal file
63
standard_paths_linux.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getConfigLocation(appName string) (string, error) {
|
||||||
|
if appName == "" {
|
||||||
|
return "", errors.New("'appName' cannot be empty")
|
||||||
|
}
|
||||||
|
if envHome := os.Getenv("XDG_CONFIG_HOME"); envHome != "" {
|
||||||
|
return filepath.Join(envHome, appName), nil
|
||||||
|
}
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting user home directory: %v", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(homeDir, ".config", appName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getUserDataLocation(appName string) (string, error) {
|
||||||
|
if appName == "" {
|
||||||
|
return "", errors.New("'appName' cannot be empty")
|
||||||
|
}
|
||||||
|
if envHome := os.Getenv("XDG_DATA_HOME"); envHome != "" {
|
||||||
|
return filepath.Join(envHome, appName), nil
|
||||||
|
}
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting user home directory: %v", err)
|
||||||
|
}
|
||||||
|
dataRoot := filepath.Join(homeDir, ".local", "share")
|
||||||
|
if unix.Access(dataRoot, unix.W_OK) != nil {
|
||||||
|
return "", fmt.Errorf("accessing user data directory '%s'", dataRoot)
|
||||||
|
}
|
||||||
|
return filepath.Join(dataRoot, appName), nil
|
||||||
|
}
|
8
support/get-version.sh
Executable file
8
support/get-version.sh
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
VERSION_TAG=$(git tag -l | grep "v" | cut -c2-);
|
||||||
|
if [[ ${#VERSION_TAG} -ne 0 ]]; then
|
||||||
|
echo "$VERSION_TAG";
|
||||||
|
else
|
||||||
|
HASH=$(git rev-parse --short HEAD);
|
||||||
|
printf "git-%s" "$HASH"
|
||||||
|
fi;
|
30
toolbar.go
Normal file
30
toolbar.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
func (app *App) toolbarInit() {
|
||||||
|
app.W.ButtonNextPage.Connect("clicked", app.nextPage)
|
||||||
|
app.W.ButtonPreviousPage.Connect("clicked", app.previousPage)
|
||||||
|
app.W.ButtonFirstPage.Connect("clicked", app.firstPage)
|
||||||
|
app.W.ButtonLastPage.Connect("clicked", app.lastPage)
|
||||||
|
app.W.ButtonNextArchive.Connect("clicked", app.nextArchive)
|
||||||
|
app.W.ButtonPreviousArchive.Connect("clicked", app.previousArchive)
|
||||||
|
app.W.ButtonSkipForward.Connect("clicked", app.skipForward)
|
||||||
|
app.W.ButtonSkipBackward.Connect("clicked", app.skipBackward)
|
||||||
|
}
|
119
ui.go
Normal file
119
ui.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fauu/gomicsv/pixbuf"
|
||||||
|
"github.com/gotk3/gotk3/gdk"
|
||||||
|
"github.com/gotk3/gotk3/glib"
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *App) uiInit() {
|
||||||
|
if err := app.loadWidgets(); err != nil {
|
||||||
|
log.Panicf("loading widgets from the definition file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.S.Cursor.init()
|
||||||
|
|
||||||
|
app.notificationInit()
|
||||||
|
|
||||||
|
app.menuInit()
|
||||||
|
|
||||||
|
app.preferencesInit()
|
||||||
|
|
||||||
|
app.toolbarInit()
|
||||||
|
|
||||||
|
app.imageAreaInit()
|
||||||
|
|
||||||
|
app.W.MainWindow.SetApplication(app.S.GTKApplication)
|
||||||
|
app.W.MainWindow.SetDefaultSize(app.Config.WindowWidth, app.Config.WindowHeight)
|
||||||
|
app.W.MainWindow.SetIcon(pixbuf.MustLoad(iconImg))
|
||||||
|
|
||||||
|
var prevW, prevH int
|
||||||
|
app.W.MainWindow.Connect("size-allocate", func(_ glib.IObject, allocationPtr uintptr) {
|
||||||
|
alloc := gdk.WrapRectangle(allocationPtr)
|
||||||
|
w, h := alloc.GetWidth(), alloc.GetHeight()
|
||||||
|
if w == prevW && h == prevH {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
prevW, prevH = w, h
|
||||||
|
app.handleImageAreaResize()
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MainWindow.Connect("motion-notify-event", func(_ *gtk.ApplicationWindow, _ *gdk.Event) bool {
|
||||||
|
app.S.Cursor.LastMoved = time.Now()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
glib.TimeoutAdd(250, app.updateCursorVisibility)
|
||||||
|
|
||||||
|
app.W.MainWindow.Connect("key-press-event", func(_ *gtk.ApplicationWindow, event *gdk.Event) {
|
||||||
|
ke := &gdk.EventKey{Event: event}
|
||||||
|
shift := ke.State()&uint(gdk.SHIFT_MASK) != 0
|
||||||
|
ctrl := ke.State()&uint(gdk.CONTROL_MASK) != 0
|
||||||
|
app.handleKeyPress(ke.KeyVal(), shift, ctrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.W.MainWindow.Connect("delete-event", app.quit)
|
||||||
|
|
||||||
|
app.syncWidgetsToConfig()
|
||||||
|
|
||||||
|
app.W.MainWindow.ShowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *App) syncWidgetsToConfig() {
|
||||||
|
app.W.MenuItemEnlarge.SetActive(app.Config.Enlarge)
|
||||||
|
app.W.MenuItemShrink.SetActive(app.Config.Shrink)
|
||||||
|
app.W.MenuItemHFlip.SetActive(app.Config.HFlip)
|
||||||
|
app.W.MenuItemVFlip.SetActive(app.Config.VFlip)
|
||||||
|
app.W.MenuItemRandom.SetActive(app.Config.Random)
|
||||||
|
app.W.MenuItemSeamless.SetActive(app.Config.Seamless)
|
||||||
|
app.W.MenuItemDoublePage.SetActive(app.Config.DoublePage)
|
||||||
|
app.W.MenuItemMangaMode.SetActive(app.Config.MangaMode)
|
||||||
|
|
||||||
|
switch app.Config.ZoomMode {
|
||||||
|
case "FitToWidth":
|
||||||
|
app.W.MenuItemFitToWidth.SetActive(true)
|
||||||
|
case "FitToHeight":
|
||||||
|
app.W.MenuItemFitToHeight.SetActive(true)
|
||||||
|
case "BestFit":
|
||||||
|
app.W.MenuItemBestFit.SetActive(true)
|
||||||
|
default:
|
||||||
|
app.W.MenuItemOriginal.SetActive(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
rgba := app.Config.BackgroundColor.ToGdkRGBA()
|
||||||
|
app.W.BackgroundColorButton.SetRGBA(&rgba)
|
||||||
|
|
||||||
|
app.W.InterpolationComboBoxText.SetActive(app.Config.Interpolation)
|
||||||
|
app.W.OneWideCheckButton.SetActive(app.Config.OneWide)
|
||||||
|
app.W.RememberRecentCheckButton.SetActive(app.Config.RememberRecent)
|
||||||
|
app.W.RememberPositionCheckButton.SetActive(app.Config.RememberPosition)
|
||||||
|
app.W.RememberPositionHTTPCheckButton.SetActive(app.Config.RememberPositionHTTP)
|
||||||
|
app.W.RememberPositionHTTPCheckButton.SetSensitive(app.Config.RememberPosition && app.Config.RememberPositionHTTP)
|
||||||
|
app.W.EmbeddedOrientationCheckButton.SetActive(app.Config.EmbeddedOrientation)
|
||||||
|
app.W.HideIdleCursorCheckButton.SetActive(app.Config.HideIdleCursor)
|
||||||
|
app.W.KamiteEnabledCheckButton.SetActive(app.Config.KamiteEnabled)
|
||||||
|
app.W.KamitePortContainer.SetSensitive(app.Config.KamiteEnabled)
|
||||||
|
app.W.KamitePortEntry.SetText(fmt.Sprint(app.Config.KamitePort))
|
||||||
|
}
|
25
util/url.go
Normal file
25
util/url.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func IsLikelyHTTPURL(s string) bool {
|
||||||
|
return strings.HasPrefix(s, "http")
|
||||||
|
}
|
80
util/util.go
Normal file
80
util/util.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func Max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func Wrap(val, low, mod int) int {
|
||||||
|
val %= mod
|
||||||
|
if val < low {
|
||||||
|
val = mod + val
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func Clamp(val, low, high float64) float64 {
|
||||||
|
if val < low {
|
||||||
|
val = low
|
||||||
|
} else if val > high {
|
||||||
|
val = high
|
||||||
|
}
|
||||||
|
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fit(sw, sh, fw, fh int) (int, int) {
|
||||||
|
r := float64(sw) / float64(sh)
|
||||||
|
|
||||||
|
var nw, nh float64
|
||||||
|
if float64(fw) >= float64(fh)*r {
|
||||||
|
nw, nh = float64(fh)*r, float64(fh)
|
||||||
|
} else {
|
||||||
|
nw, nh = float64(fw), float64(fw)/r
|
||||||
|
}
|
||||||
|
return int(nw), int(nh)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntPtrToUintPtr(l *int) *uint {
|
||||||
|
if l != nil {
|
||||||
|
ul := uint(*l)
|
||||||
|
return &ul
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GC() {
|
||||||
|
runtime.GC()
|
||||||
|
runtime.GC()
|
||||||
|
}
|
160
widgets.go
Normal file
160
widgets.go
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2013-2021 Utkan Güngördü <utkan@freeconsole.org>
|
||||||
|
* Copyright (c) 2021-2022 Piotr Grabowski
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package gomicsv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/gotk3/gotk3/gtk"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Widgets struct {
|
||||||
|
MainWindow *gtk.ApplicationWindow `build:"MainWindow"`
|
||||||
|
MainContainer *gtk.Box `build:"MainContainer"`
|
||||||
|
Menubar *gtk.MenuBar `build:"Menubar"`
|
||||||
|
ScrolledWindow *gtk.ScrolledWindow `build:"ScrolledWindow"`
|
||||||
|
ImageViewport *gtk.Viewport `build:"ImageViewport"`
|
||||||
|
ImageBox *gtk.Box `build:"ImageBox"`
|
||||||
|
ImageL *gtk.Image `build:"ImageL"`
|
||||||
|
ImageR *gtk.Image `build:"ImageR"`
|
||||||
|
NotificationRevealer *gtk.Revealer `build:"NotificationRevealer"`
|
||||||
|
NotificationLabel *gtk.Label `build:"NotificationLabel"`
|
||||||
|
NotificationCloseButton *gtk.Button `build:"NotificationCloseButton"`
|
||||||
|
MenuAbout *gtk.Menu `build:"MenuAbout"`
|
||||||
|
AboutDialog *gtk.AboutDialog `build:"AboutDialog"`
|
||||||
|
MenuFile *gtk.Menu `build:"MenuFile"`
|
||||||
|
MenuEdit *gtk.Menu `build:"MenuEdit"`
|
||||||
|
MenuView *gtk.Menu `build:"MenuView"`
|
||||||
|
MenuNavigation *gtk.Menu `build:"MenuNavigation"`
|
||||||
|
MenuBookmarks *gtk.Menu `build:"MenuBookmarks"`
|
||||||
|
MenuJumpmarks *gtk.Menu `build:"MenuJumpmarks"`
|
||||||
|
Statusbar *gtk.Statusbar `build:"Statusbar"`
|
||||||
|
MenuItemOpen *gtk.MenuItem `build:"MenuItemOpen"`
|
||||||
|
MenuItemOpenURL *gtk.MenuItem `build:"MenuItemOpenURL"`
|
||||||
|
MenuItemClose *gtk.MenuItem `build:"MenuItemClose"`
|
||||||
|
MenuItemQuit *gtk.MenuItem `build:"MenuItemQuit"`
|
||||||
|
MenuItemSaveImage *gtk.MenuItem `build:"MenuItemSaveImage"`
|
||||||
|
ArchiveFileChooserDialog *gtk.FileChooserDialog `build:"ArchiveFileChooserDialog"`
|
||||||
|
SaveImageFileChooserDialog *gtk.FileChooserDialog `build:"SaveImageFileChooserDialog"`
|
||||||
|
OpenURLDialog *gtk.Dialog `build:"OpenURLDialog"`
|
||||||
|
OpenURLDialogURLEntry *gtk.Entry `build:"OpenURLDialogURLEntry"`
|
||||||
|
OpenURLDialogExplanationLabel *gtk.Label `build:"OpenURLDialogExplanationLabel"`
|
||||||
|
OpenURLDialogRefererEntry *gtk.Entry `build:"OpenURLDialogRefererEntry"`
|
||||||
|
Toolbar *gtk.Toolbar `build:"Toolbar"`
|
||||||
|
ButtonNextPage *gtk.ToolButton `build:"ButtonNextPage"`
|
||||||
|
ButtonPreviousPage *gtk.ToolButton `build:"ButtonPreviousPage"`
|
||||||
|
ButtonLastPage *gtk.ToolButton `build:"ButtonLastPage"`
|
||||||
|
ButtonFirstPage *gtk.ToolButton `build:"ButtonFirstPage"`
|
||||||
|
ButtonNextArchive *gtk.ToolButton `build:"ButtonNextArchive"`
|
||||||
|
ButtonPreviousArchive *gtk.ToolButton `build:"ButtonPreviousArchive"`
|
||||||
|
ButtonSkipForward *gtk.ToolButton `build:"ButtonSkipForward"`
|
||||||
|
ButtonSkipBackward *gtk.ToolButton `build:"ButtonSkipBackward"`
|
||||||
|
MenuItemNextPage *gtk.MenuItem `build:"MenuItemNextPage"`
|
||||||
|
MenuItemPreviousPage *gtk.MenuItem `build:"MenuItemPreviousPage"`
|
||||||
|
MenuItemLastPage *gtk.MenuItem `build:"MenuItemLastPage"`
|
||||||
|
MenuItemFirstPage *gtk.MenuItem `build:"MenuItemFirstPage"`
|
||||||
|
MenuItemNextArchive *gtk.MenuItem `build:"MenuItemNextArchive"`
|
||||||
|
MenuItemPreviousArchive *gtk.MenuItem `build:"MenuItemPreviousArchive"`
|
||||||
|
MenuItemSkipForward *gtk.MenuItem `build:"MenuItemSkipForward"`
|
||||||
|
MenuItemSkipBackward *gtk.MenuItem `build:"MenuItemSkipBackward"`
|
||||||
|
MenuItemEnlarge *gtk.CheckMenuItem `build:"MenuItemEnlarge"`
|
||||||
|
MenuItemShrink *gtk.CheckMenuItem `build:"MenuItemShrink"`
|
||||||
|
MenuItemFullscreen *gtk.CheckMenuItem `build:"MenuItemFullscreen"`
|
||||||
|
MenuItemHideUI *gtk.CheckMenuItem `build:"MenuItemHideUI"`
|
||||||
|
MenuItemSeamless *gtk.CheckMenuItem `build:"MenuItemSeamless"`
|
||||||
|
MenuItemRandom *gtk.CheckMenuItem `build:"MenuItemRandom"`
|
||||||
|
MenuItemCopyImageToClipboard *gtk.MenuItem `build:"MenuItemCopyImageToClipboard"`
|
||||||
|
MenuItemPreferences *gtk.MenuItem `build:"MenuItemPreferences"`
|
||||||
|
MenuItemHFlip *gtk.CheckMenuItem `build:"MenuItemHFlip"`
|
||||||
|
MenuItemVFlip *gtk.CheckMenuItem `build:"MenuItemVFlip"`
|
||||||
|
MenuItemMangaMode *gtk.CheckMenuItem `build:"MenuItemMangaMode"`
|
||||||
|
MenuItemDoublePage *gtk.CheckMenuItem `build:"MenuItemDoublePage"`
|
||||||
|
MenuItemGoTo *gtk.MenuItem `build:"MenuItemGoTo"`
|
||||||
|
MenuItemBestFit *gtk.RadioMenuItem `build:"MenuItemBestFit"`
|
||||||
|
MenuItemOriginal *gtk.RadioMenuItem `build:"MenuItemOriginal"`
|
||||||
|
MenuItemFitToWidth *gtk.RadioMenuItem `build:"MenuItemFitToWidth"`
|
||||||
|
MenuItemFitToHeight *gtk.RadioMenuItem `build:"MenuItemFitToHeight"`
|
||||||
|
MenuItemAbout *gtk.MenuItem `build:"MenuItemAbout"`
|
||||||
|
GoToThumbnailImage *gtk.Image `build:"GoToThumbnailImage"`
|
||||||
|
GoToDialog *gtk.Dialog `build:"GoToDialog"`
|
||||||
|
GoToSpinButton *gtk.SpinButton `build:"GoToSpinButton"`
|
||||||
|
GoToScrollbar *gtk.Scrollbar `build:"GoToScrollbar"`
|
||||||
|
PreferencesDialog *gtk.Dialog `build:"PreferencesDialog"`
|
||||||
|
BackgroundColorButton *gtk.ColorButton `build:"BackgroundColorButton"`
|
||||||
|
PagesToSkipSpinButton *gtk.SpinButton `build:"PagesToSkipSpinButton"`
|
||||||
|
InterpolationComboBoxText *gtk.ComboBoxText `build:"InterpolationComboBoxText"`
|
||||||
|
RememberRecentCheckButton *gtk.CheckButton `build:"RememberRecentCheckButton"`
|
||||||
|
RememberPositionCheckButton *gtk.CheckButton `build:"RememberPositionCheckButton"`
|
||||||
|
RememberPositionHTTPCheckButton *gtk.CheckButton `build:"RememberPositionHTTPCheckButton"`
|
||||||
|
OneWideCheckButton *gtk.CheckButton `build:"OneWideCheckButton"`
|
||||||
|
SmartScrollCheckButton *gtk.CheckButton `build:"SmartScrollCheckButton"`
|
||||||
|
EmbeddedOrientationCheckButton *gtk.CheckButton `build:"EmbeddedOrientationCheckButton"`
|
||||||
|
HideIdleCursorCheckButton *gtk.CheckButton `build:"HideIdleCursorCheckButton"`
|
||||||
|
KamiteEnabledCheckButton *gtk.CheckButton `build:"KamiteEnabledCheckButton"`
|
||||||
|
KamitePortContainer *gtk.Box `build:"KamitePortContainer"`
|
||||||
|
KamitePortEntry *gtk.Entry `build:"KamitePortEntry"`
|
||||||
|
MenuItemAddBookmark *gtk.MenuItem `build:"AddBookmarkMenuItem"`
|
||||||
|
MenuItemToggleJumpmark *gtk.MenuItem `build:"ToggleJumpmarkMenuItem"`
|
||||||
|
MenuItemCycleJumpmarksBackward *gtk.MenuItem `build:"CycleJumpmarksBackwardMenuItem"`
|
||||||
|
MenuItemCycleJumpmarksForward *gtk.MenuItem `build:"CycleJumpmarksForwardMenuItem"`
|
||||||
|
MenuItemJumpmarksReturnFromCycling *gtk.MenuItem `build:"JumpmarksReturnFromCyclingMenuItem"`
|
||||||
|
RecentChooserMenu *gtk.RecentChooserMenu `build:"RecentChooserMenu"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadWidgets fills the Widgets struct based on the glade UI definition file
|
||||||
|
func (app *App) loadWidgets() (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = r.(error)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
builder, err := gtk.BuilderNew()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = builder.AddFromString(uiDef); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
widgets := &Widgets{}
|
||||||
|
|
||||||
|
widgetsStruct := reflect.ValueOf(widgets).Elem()
|
||||||
|
|
||||||
|
for i := 0; i < widgetsStruct.NumField(); i++ {
|
||||||
|
field := widgetsStruct.Field(i)
|
||||||
|
widget := widgetsStruct.Type().Field(i).Tag.Get("build")
|
||||||
|
if widget == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := builder.GetObject(widget)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := reflect.ValueOf(obj).Convert(field.Type())
|
||||||
|
field.Set(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.W = *widgets
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue