Add initial codebase

This commit is contained in:
Suhan Paradkar 2021-12-19 09:22:01 +05:30
parent 2b257962bf
commit 3c4ff58dcd
1069 changed files with 95159 additions and 1 deletions

195
CHANGELOG Executable file
View File

@ -0,0 +1,195 @@
v15.9 to v16.5: https://gitlab.com/AwaisKing/instagrabber/-/releases
From v16.6: https://github.com/austinhuang0131/barinsta/releases
v15.9
note: there will be no F-Droid updates from this version (v15.9) and onward, download updates from repo's Releases page.
+ added user stories in Feed
+ added frame to currently showing slider item
+ removed tap to pause/resume from Post viewer, replaced with controller
+ fixed swipe not working when posts are opened from stories
+ fixed comments not showing for slider items
v15.8
+ added user's website in profile
+ fixed caption mentions length (@kernoeb)
+ fixed some translations (@kernoeb)
+ fixd feed captions merging with "... more"
v15.4
+ ADDED FRENCH AND SPANISH!!!
+-> Huge thanks to @kernoeb (Telegram) for French translation and @sguinetti (GitLab) for Spanish translation!!
+ added custom post time format support!
+ fixed flickering after changing settings
+ fixed posts not showing after searching from a private profile
+ fixed stories and profile pictures not downloading in user folders even when option was enabled
+ fixed issues with feed, discover and post viewer
+ fixed search suggestions crashes
v15.2
+ fixed feed video not pausing when opened in post viewer
+ added 1 new profile picture view mode
+ added fields in LogCollector to better understand how things went wrong
+ better comments, story, feed, discover and suggestion fetchers
v15.0
+ added support for Instagram.com urls! (:
+ added "Downloaded" check on posts if they've already been downloaded (as per suggestion)
+ fixed highlights scrolling issues
+ fixed comments issues
+ fixed posts weren't showing after searching anything from a private account
+ fixed suggestions not showing sometimes
+ fixed Stories viewer swipe issues
+ fixed Import/Export dialog not showing on old phones
+ added user's name in suggestions search list
+ added comment likes in Comments viewer
+ sending logs won't add empty logs to archive anymore!
+ fixed no new line in logs
+ handled Login WebView lifecycles
+ a better way of handling highlight swipes
+ removed useless code parts
v14.5
+ added changelog after update
+ added swipe support in both Discover/Explore and Feed pages
+ added Send Logs button in Settings to send logs when something doesn't work
+ added clickable user profile in Post Viewer
+ fixed weirdly collapsing toolbar when toolbar is shown at the top
v14.0
+ added theme selection support
+ added import/export settings, favorites and logins functionality (thanks to Airikr [@edgren] on Telegram)
+ added support for downloading slider items from User Feed page
+ added support for usernames and hashtags in user's biography/about text
+ added multiple selection in Discover/Explore page
+ added post date for feed items
+ added some more date formats
+ copyable feed item caption (long tap)
+ fixed late refresh indicator in Followers/Following comparison mode
+ changed feed item size to squares
+ fixed some caption text issues having mentions and hashtags
+ removed clipboard listener (stopped working after some changes)
+ added log collector for different crash scenarios
v13.7
+ fixed custom download folder selection issues
v13.3
+ added discover/explore page (only for logged in users)
+ added function to remove IPTC tracking data from downloaded pictures (thanks to Airikr [@edgren] on Telegram)
+ added multiple accounts support (quick access) and favorites (a suggestion from Saurabh on Telegram)
+ added custom download folder option, you can select where to download posts (a suggestion from Airikr)
+ added desktop mode toggle in Login activity (a suggestion from Eymen on Telegram)
+ added post date in post viewer (a suggestion from W on Telegram)
+ added post time format settings [ Settings > Post Time Settings ]
+ fixed some icons and layouts
+ removed color from slider items in feed
+ removed useless methods and properties
+ better way of handling multiple stories and posts (sliders) in post viewer
+ tried to make notifications grouped together.. hope they work
+ some other fixes and additions which i probably forgot, cause i'm a human ffs
v13.0
+ fixed crash when searching hashtags
+ added lazy loading for hashtags
+ added Show Feed option in Settings (feed may crash app on some phones)
+ added null check in Download async
+ better/adapatable icon
+ fixed sheet dialog themes
v12.7
+ (probably) fixed inflating issue in some devices
v12.5
+ some small performance improvements
v12.0
+ added feed!! (only for logged in users)
+ fixed multiple hashtags with no spaces between them
+ stopped activity from recreating when nothing changed
+ changed highlights to RecyclerView instead of HorizontalScrollView
+ changed some numbers and precisions cause she left me on read
v11.0
+ added crash reporting library
+ added mute/unmute for session
+ better profile picture viewer + profile picture info
+ better swipe gesture
+ fixed mention and hashtag issues
v10.0
NOTE: YOU MAY NEED TO LOGIN TO VIEW PROFILES CAUSE OF INSTAGRAM CHANGES.
+ added direct download multiple posts dialog
+ fixed notification problems
+ fixed some direct download problems
+ fixed batch download and username folder not creating in some activities
+ fixed update checker
v9.0
+ added search in comments viewer
+ added settings to auto or lazy load posts
+ added user & hashtag stack when back pressed
+ added loading icon when loading posts
+ profile info bar sticks to top
+ fixed highlights and profile picture size in portrait and landscape mode
+ fixed posts loading from other user when restarting app
+ fixed posts size changing when scrolled
+ users & hashtag search shows only users or hashtag when first char is @ or #
+ scrolls to top when back pressed
v8.0
+ added pull-to-refresh layout in main posts, followers/following viewer
+ added search in followers/following viewer
+ added video views in post viewer
+ added animation when showing highlights (if available)
+ fixed long usernames not animating (marquee)
+ fixed padding and size in portrait and landscape modes
+ fixed accounts showing personal followers/following when account has 0 followers/following
+ fixed double ripple when tapped on profile picture
+ smaller story border around profile picture
v7.0
+ added comments viewer!!
+ added highest quality post fetcher
+ added loading indicator where it was missing before
+ fixed highlight name alignment
+ fixed swiping on posts opened via Share or link
v6.0
+ added story highlights!! (issue #5)
+ added button to view posts posted in stories
+ added verified badge for accounts
+ fixed posts & story swiping issues
+ fixed slow loading and stuff
+ fixed different margins and sizes
+ fixed activity not recreating after settings dialogs closed
v5.0
+ added followers / following checker
+ added search view suggestions for usernames & hashtags
+ added sliding profile container
+ fixed batch download permission issue (issue #4)
+ fixed some small screen panning issues
+ fixed search view width
+ fixed update checker
v4.0
+ fixed Login and Visit project page button codes.
v3.0
+ fixed posts merged from different accounts when searched while posts are loading!
+ view stories (only if you're logged in)
+ directly download posts
+ choose between two pfp (profile picture) viewer methods
+ fixed search box not showing up when toolbar is at bottom
+ automatically checks for updates
v2.0
+ fixed Login crashes
v1.0
+ first ever changelog
+ basic stuff like downloading profile pics, posts and copying captions and bio.
+ batch download posts

621
LICENSE Executable file
View File

@ -0,0 +1,621 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 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

View File

@ -1,3 +1,27 @@
# Instabar
An instagram client, based on Barinsta
[![Open Source Love](https://badges.frapsoft.com/os/v3/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badges/)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](./LICENSE)
Instagram client; based on Barinsta.
## License
This app's predecessor, Barinsta, was originally made by [Austin Huang](https://github.com/austinhuang) on GitHub.
Instabar
Copyright (C) 2020-2021 Suhan Paradkar <12suhangp34@gmaail.com>
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/>.

1
SECURITY.md Normal file
View File

@ -0,0 +1 @@
If there is a security issue with the latest version, please let me know using GitHub issues (bug), or email `im [at] austinhuang [dot] me` if confidential. Use [this PGP key](https://github.com/austinhuang0131/austinhuang0131.github.io/blob/master/assets/key.asc) if you know how to.

6
app/.classpath Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-12/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

1
app/.gitignore vendored Executable file
View File

@ -0,0 +1 @@
/build

34
app/.project Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1600117114944</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

View File

@ -0,0 +1,2 @@
connection.project.dir=..
eclipse.preferences.version=1

262
app/build.gradle Executable file
View File

@ -0,0 +1,262 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: "androidx.navigation.safeargs"
apply plugin: 'kotlin-kapt'
apply from: 'sentry.gradle'
def getGitHash = { ->
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
}
android {
compileSdkVersion 30
defaultConfig {
applicationId 'awais.instagrabber'
minSdkVersion 21
targetSdkVersion 30
versionCode 65
versionName '19.2.4'
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
vectorDrawables.generatedDensities = []
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
buildFeatures { viewBinding true }
aaptOptions { additionalParameters '--no-version-vectors' }
buildTypes {
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
flavorDimensions "repo"
productFlavors {
github {
dimension "repo"
// versionNameSuffix "-github" // appended in assemble task
buildConfigField("String", "dsn", SENTRY_DSN)
buildConfigField("boolean", "isPre", "false")
}
fdroid {
dimension "repo"
versionNameSuffix "-fdroid"
buildConfigField("boolean", "isPre", "false")
}
}
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable project.hasProperty("split") && !gradle.startParameter.taskNames.isEmpty() && gradle.startParameter.taskNames.get(0).contains('Release')
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for x86 and x86_64.
// Resets the list of ABIs that Gradle should create APKs for to none.
reset()
// Specifies a list of ABIs that Gradle should create APKs for.
include "x86", "x86_64", "arm64-v8a", "armeabi-v7a"
// Specifies that we want to also generate a universal APK that includes all ABIs.
universalApk true
}
}
android.applicationVariants.all { variant ->
if (variant.flavorName != "github") return
variant.outputs.all { output ->
def builtType = variant.buildType.name
def versionName = variant.versionName
// def versionCode = variant.versionCode
def flavor = variant.flavorName
def flavorBuiltType = "${flavor}_${builtType}"
def suffix
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
suffix = "${versionName}-${flavorBuiltType}" // eg. 19.1.0-github_debug or release
}
if (builtType.toString() == 'release' && project.hasProperty("pre")) {
buildConfigField("boolean", "isPre", "true")
flavorBuiltType = "${getGitHash()}-${flavor}"
// For x86 and x86_64, the versionNames are already overridden
if (versionName.contains(flavorBuiltType)) {
suffix = "${versionName}"
} else {
// append latest commit short hash for pre-release
suffix = "${versionName}.${flavorBuiltType}" // eg. 19.1.0.b123456-github
}
}
output.versionNameOverride = suffix
def abi = output.getFilter(com.android.build.OutputFile.ABI)
// println(abi + ", " + versionName + ", " + flavor + ", " + builtType + ", " + suffix)
outputFileName = abi == null ? "barinsta_${suffix}.apk" : "barinsta_${suffix}_${abi}.apk"
}
}
packagingOptions {
// Exclude file to avoid
// Error: Duplicate files during packaging of APK
exclude 'META-INF/LICENSE.md'
exclude 'META-INF/LICENSE-notice.md'
exclude 'META-INF/atomicfu.kotlin_module'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
testOptions.unitTests {
includeAndroidResources = true
}
}
configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
def exoplayer_version = '2.14.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-dash:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.viewpager2:viewpager2:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.preference:preference:1.1.1"
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'com.google.guava:guava:27.0.1-android'
def core_version = "1.6.0"
implementation "androidx.core:core:$core_version"
// Fragment
implementation "androidx.fragment:fragment-ktx:1.3.5"
// Lifecycle
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
// Navigation
implementation "androidx.navigation:navigation-fragment-ktx:$rootProject.nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$rootProject.nav_version"
// Room
def room_version = "2.3.0"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-guava:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
// CameraX
def camerax_version = "1.1.0-alpha06"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha26"
// EmojiCompat
def emoji_compat_version = "1.1.0"
implementation "androidx.emoji:emoji:$emoji_compat_version"
implementation "androidx.emoji:emoji-appcompat:$emoji_compat_version"
// Work
def work_version = '2.5.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "ru.gildor.coroutines:kotlin-coroutines-okhttp:1.0"
implementation 'com.facebook.fresco:fresco:2.5.0'
implementation 'com.facebook.fresco:animated-webp:2.5.0'
implementation 'com.facebook.fresco:webpsupport:2.5.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'org.apache.commons:commons-imaging:1.0-alpha2'
implementation 'com.github.skydoves:balloon:1.3.5'
implementation 'com.github.ammargitham:AutoLinkTextViewV2:3.2.0'
implementation 'com.github.ammargitham:uCrop:2.3-non-native'
implementation 'com.github.ammargitham:android-gpuimage:2.1.1-beta4'
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
githubImplementation 'io.sentry:sentry-android:5.0.1'
testImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
testImplementation "androidx.test.ext:junit-ktx:1.1.3"
testImplementation "androidx.test:core-ktx:1.4.0"
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "org.robolectric:robolectric:4.5.1"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
androidTestImplementation 'org.junit.jupiter:junit-jupiter:5.7.2'
androidTestImplementation 'androidx.test:core:1.4.0'
androidTestImplementation 'com.android.support:support-annotations:28.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation "androidx.room:room-testing:2.3.0"
androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0'
}

29
app/lint.xml Executable file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="IconColors">
<ignore path="src/main/res/drawable-night/download.png" />
</issue>
<issue id="IconLocation">
<ignore path="src/main/res/drawable/adm.png" />
<ignore path="src/main/res/drawable/check.png" />
<ignore path="src/main/res/drawable/collapse.png" />
<ignore path="src/main/res/drawable/comments.png" />
<ignore path="src/main/res/drawable/download.png" />
<ignore path="src/main/res/drawable/expand.png" />
<ignore path="src/main/res/drawable/lock.png" />
<ignore path="src/main/res/drawable/lw.png" />
<ignore path="src/main/res/drawable/ms.png" />
<ignore path="src/main/res/drawable/mute.png" />
<ignore path="src/main/res/drawable/qdb.png" />
<ignore path="src/main/res/drawable/rev.png" />
<ignore path="src/main/res/drawable/revl.png" />
<ignore path="src/main/res/drawable/settings.png" />
<ignore path="src/main/res/drawable/slider.png" />
<ignore path="src/main/res/drawable/tesv.png" />
<ignore path="src/main/res/drawable/vdz.png" />
<ignore path="src/main/res/drawable/verified.png" />
<ignore path="src/main/res/drawable/video.png" />
<ignore path="src/main/res/drawable/video_views.png" />
<ignore path="src/main/res/drawable/vol.png" />
</issue>
</lint>

2
app/local.properties Normal file
View File

@ -0,0 +1,2 @@
sdk.dir=/opt/android-sdk
sdk-location=/opt/android-sdk

29
app/proguard-rules.pro vendored Executable file
View File

@ -0,0 +1,29 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#noinspection ShrinkerUnresolvedReference
#-keep class !com.google.android.exoplayer2.**, ** { *; }
-dontobfuscate
# prevent shrinking retrofit response entities
-keep class awais.instagrabber.repositories.responses.** { *; }

View File

@ -0,0 +1,114 @@
{
"formatVersion": 1,
"database": {
"version": 4,
"identityHash": "538d64adaeb8c3a98db9204955932e59",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '538d64adaeb8c3a98db9204955932e59')"
]
}
}

View File

@ -0,0 +1,161 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "0b38e12b76bb081ec837191c5ef5b54e",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "dm_last_notified",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "threadId",
"columnName": "thread_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastNotifiedMsgTs",
"columnName": "last_notified_msg_ts",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotifiedAt",
"columnName": "last_notified_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_dm_last_notified_thread_id",
"unique": true,
"columnNames": [
"thread_id"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0b38e12b76bb081ec837191c5ef5b54e')"
]
}
}

View File

@ -0,0 +1,227 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "232e618b3bfcb4661336b359d036c455",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` TEXT, `username` TEXT, `cookie` TEXT, `full_name` TEXT, `profile_pic` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "cookie",
"columnName": "cookie",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "fullName",
"columnName": "full_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "profilePic",
"columnName": "profile_pic",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "favorites",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query_text` TEXT, `type` TEXT, `display_name` TEXT, `pic_url` TEXT, `date_added` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query_text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "displayName",
"columnName": "display_name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "dateAdded",
"columnName": "date_added",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "dm_last_notified",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thread_id` TEXT, `last_notified_msg_ts` INTEGER, `last_notified_at` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "threadId",
"columnName": "thread_id",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastNotifiedMsgTs",
"columnName": "last_notified_msg_ts",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastNotifiedAt",
"columnName": "last_notified_at",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_dm_last_notified_thread_id",
"unique": true,
"columnNames": [
"thread_id"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_dm_last_notified_thread_id` ON `${TABLE_NAME}` (`thread_id`)"
}
],
"foreignKeys": []
},
{
"tableName": "recent_searches",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `ig_id` TEXT NOT NULL, `name` TEXT NOT NULL, `username` TEXT, `pic_url` TEXT, `type` TEXT NOT NULL, `last_searched_on` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "igId",
"columnName": "ig_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "picUrl",
"columnName": "pic_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastSearchedOn",
"columnName": "last_searched_on",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_recent_searches_ig_id_type",
"unique": true,
"columnNames": [
"ig_id",
"type"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_recent_searches_ig_id_type` ON `${TABLE_NAME}` (`ig_id`, `type`)"
}
],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '232e618b3bfcb4661336b359d036c455')"
]
}
}

13
app/sentry.gradle Normal file
View File

@ -0,0 +1,13 @@
def dsnKey = 'DSN'
def defaultDsn = '\"\"'
final Properties properties = new Properties()
File propertiesFile = rootProject.file('sentry.properties')
if (!propertiesFile.exists()) {
propertiesFile.createNewFile()
}
properties.load(new FileInputStream(propertiesFile))
ext{
SENTRY_DSN = properties.getProperty(dsnKey, defaultDsn)
}

View File

@ -0,0 +1,50 @@
package awais.instagrabber.db;
import androidx.room.Room;
import androidx.room.migration.Migration;
import androidx.room.testing.MigrationTestHelper;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException;
import static awais.instagrabber.db.AppDatabase.MIGRATION_4_5;
import static awais.instagrabber.db.AppDatabase.MIGRATION_5_6;
@RunWith(AndroidJUnit4.class)
public class MigrationTest {
private static final String TEST_DB = "migration-test";
private static final Migration[] ALL_MIGRATIONS = new Migration[]{MIGRATION_4_5, MIGRATION_5_6};
@Rule
public MigrationTestHelper helper;
public MigrationTest() {
final String canonicalName = AppDatabase.class.getCanonicalName();
helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
canonicalName,
new FrameworkSQLiteOpenHelperFactory());
}
@Test
public void migrateAll() throws IOException {
// Create earliest version of the database. Have to start with 4 since that is the version we migrated to Room.
SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 4);
db.close();
// Open latest version of the database. Room will validate the schema
// once all migrations execute.
AppDatabase appDb = Room.databaseBuilder(InstrumentationRegistry.getInstrumentation().getTargetContext(),
AppDatabase.class,
TEST_DB)
.addMigrations(ALL_MIGRATIONS).build();
appDb.getOpenHelper().getWritableDatabase();
appDb.close();
}
}

View File

@ -0,0 +1,81 @@
package awais.instagrabber.db.dao
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.runner.AndroidJUnit4
import awais.instagrabber.db.AppDatabase
import awais.instagrabber.db.entities.RecentSearch
import awais.instagrabber.models.enums.FavoriteType
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.Assertions
import org.junit.runner.RunWith
import java.time.LocalDateTime
@RunWith(AndroidJUnit4::class)
class RecentSearchDaoTest {
private lateinit var db: AppDatabase
private lateinit var dao: RecentSearchDao
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
dao = db.recentSearchDao()
}
@After
fun closeDb() {
db.close()
}
@ExperimentalCoroutinesApi
@Test
fun writeQueryDelete() = runBlockingTest {
val recentSearch = insertRecentSearch(1, "1", "test1", FavoriteType.HASHTAG)
val byIgIdAndType = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG)
Assertions.assertNotNull(byIgIdAndType)
Assertions.assertEquals(recentSearch, byIgIdAndType)
dao.deleteRecentSearch(byIgIdAndType ?: throw NullPointerException())
val deleted = dao.getRecentSearchByIgIdAndType("1", FavoriteType.HASHTAG)
Assertions.assertNull(deleted)
}
@ExperimentalCoroutinesApi
@Test
fun queryAllOrdered() = runBlockingTest {
val insertListReversed: List<RecentSearch> = listOf(
insertRecentSearch(1, "1", "test1", FavoriteType.HASHTAG),
insertRecentSearch(2, "2", "test2", FavoriteType.LOCATION),
insertRecentSearch(3, "3", "test3", FavoriteType.USER),
insertRecentSearch(4, "4", "test4", FavoriteType.USER),
insertRecentSearch(5, "5", "test5", FavoriteType.USER)
).asReversed() // important
val fromDb: List<RecentSearch?> = dao.getAllRecentSearches()
Assertions.assertIterableEquals(insertListReversed, fromDb)
}
private fun insertRecentSearch(id: Int, igId: String, name: String, type: FavoriteType): RecentSearch {
val recentSearch = RecentSearch(
id,
igId,
name,
null,
null,
type,
LocalDateTime.now()
)
runBlocking { dao.insertRecentSearch(recentSearch) }
return recentSearch
}
}

View File

@ -0,0 +1,15 @@
package awaisomereport
import androidx.test.runner.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
internal class CrashReporterHelperTest {
@Test
fun getErrorContent() {
val errorContent = CrashReporterHelper.getReportContent(Exception())
print(errorContent)
}
}

View File

@ -0,0 +1,40 @@
package awais.instagrabber.fragments.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.fragments.settings.IFlavorSettings;
import awais.instagrabber.fragments.settings.SettingCategory;
public final class FlavorSettings implements IFlavorSettings {
private static FlavorSettings instance;
private FlavorSettings() {
}
public static FlavorSettings getInstance() {
if (instance == null) {
instance = new FlavorSettings();
}
return instance;
}
@NonNull
@Override
public List<Preference> getPreferences(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager,
@NonNull final SettingCategory settingCategory) {
// switch (settingCategory) {
// default:
// break;
// }
return Collections.emptyList();
}
}

View File

@ -0,0 +1,64 @@
package awais.instagrabber.utils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private static final Object LOCK = new Object();
private static final String TAG = UpdateChecker.class.getSimpleName();
private static UpdateChecker instance;
public static UpdateChecker getInstance() {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new UpdateChecker();
}
}
}
return instance;
}
/**
* Needs to be called asynchronously
*
* @return the latest version from f-droid
*/
@Nullable
public String getLatestVersion() {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("https://f-droid.org/api/v1/packages/me.austinhuang.instagrabber").openConnection();
conn.setUseCaches(false);
conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me");
conn.connect();
final int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
final JSONObject data = new JSONObject(NetworkUtils.readFromConnection(conn));
return "v" + data.getJSONArray("packages").getJSONObject(0).getString("versionName");
// if (BuildConfig.VERSION_CODE < data.getInt("suggestedVersionCode")) {
// }
}
} catch (final Exception e) {
Log.e(TAG, "", e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
public void onDownload(@NonNull final AppCompatActivity context) {
Utils.openURL(context, "https://f-droid.org/packages/me.austinhuang.instagrabber/");
}
}

View File

@ -0,0 +1,14 @@
package awaisomereport
import android.app.Application
class CrashHandler(private val application: Application) : ICrashHandler {
override fun uncaughtException(
t: Thread,
exception: Throwable,
defaultExceptionHandler: Thread.UncaughtExceptionHandler
) {
CrashReporterHelper.startErrorReporterActivity(application, exception)
defaultExceptionHandler.uncaughtException(t, exception)
}
}

View File

@ -0,0 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="awais.instagrabber">
<application>
<meta-data
android:name="io.sentry.auto-init"
android:value="false" />
</application>
</manifest>

View File

@ -0,0 +1,83 @@
package awais.instagrabber.fragments.settings;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import com.google.common.collect.ImmutableList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import static awais.instagrabber.fragments.settings.PreferenceKeys.PREF_ENABLE_SENTRY;
import static awais.instagrabber.utils.Utils.settingsHelper;
public final class FlavorSettings implements IFlavorSettings {
private static FlavorSettings instance;
private FlavorSettings() {
}
public static FlavorSettings getInstance() {
if (instance == null) {
instance = new FlavorSettings();
}
return instance;
}
@NonNull
@Override
public List<Preference> getPreferences(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager,
@NonNull final SettingCategory settingCategory) {
switch (settingCategory) {
case GENERAL:
return getGeneralPrefs(context, fragmentManager);
default:
break;
}
return Collections.emptyList();
}
private List<Preference> getGeneralPrefs(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager) {
return ImmutableList.of(
getSentryPreference(context, fragmentManager)
);
}
private Preference getSentryPreference(@NonNull final Context context,
@NonNull final FragmentManager fragmentManager) {
if (!settingsHelper.hasPreference(PREF_ENABLE_SENTRY)) {
// disabled by default
settingsHelper.putBoolean(PREF_ENABLE_SENTRY, false);
}
return PreferenceHelper.getSwitchPreference(
context,
PREF_ENABLE_SENTRY,
R.string.enable_sentry,
R.string.sentry_summary,
false,
(preference, newValue) -> {
if (!(newValue instanceof Boolean)) return true;
final boolean enabled = (Boolean) newValue;
if (enabled) {
final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
111,
0,
R.string.sentry_start_next_launch,
R.string.ok,
0,
0);
dialogFragment.show(fragmentManager, "sentry_dialog");
}
return true;
});
}
}

View File

@ -0,0 +1,61 @@
package awais.instagrabber.utils;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateChecker {
private static final Object LOCK = new Object();
private static final String TAG = UpdateChecker.class.getSimpleName();
private static UpdateChecker instance;
public static UpdateChecker getInstance() {
if (instance == null) {
synchronized (LOCK) {
if (instance == null) {
instance = new UpdateChecker();
}
}
}
return instance;
}
/**
* Needs to be called asynchronously
*
* @return the latest version from Github
*/
@Nullable
public String getLatestVersion() {
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) new URL("https://github.com/austinhuang0131/barinsta/releases/latest").openConnection();
conn.setInstanceFollowRedirects(false);
conn.setUseCaches(false);
conn.setRequestProperty("User-Agent", "https://Barinsta.AustinHuang.me / mailto:Barinsta@AustinHuang.me");
conn.connect();
final int responseCode = conn.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
return "v" + conn.getHeaderField("Location").split("/v")[1];
// return !version.equals(BuildConfig.VERSION_NAME);
}
} catch (final Exception e) {
Log.e(TAG, "", e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
return null;
}
public void onDownload(@NonNull final Context context) {
Utils.openURL(context, "https://github.com/austinhuang0131/instagrabber/releases/latest");
}
}

View File

@ -0,0 +1,54 @@
package awaisomereport
import android.app.Application
import awais.instagrabber.BuildConfig
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.utils.Utils
import io.sentry.SentryEvent
import io.sentry.SentryLevel
import io.sentry.SentryOptions.BeforeSendCallback
import io.sentry.android.core.SentryAndroid
import io.sentry.android.core.SentryAndroidOptions
class CrashHandler(private val application: Application) : ICrashHandler {
private var enabled = false
init {
enabled = if (!Utils.settingsHelper.hasPreference(PreferenceKeys.PREF_ENABLE_SENTRY)) {
// disabled by default (change to true if we need enabled by default)
false
} else {
Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_SENTRY)
}
if (enabled) {
SentryAndroid.init(application) { options: SentryAndroidOptions ->
options.dsn = BuildConfig.dsn
options.setDiagnosticLevel(SentryLevel.ERROR)
options.beforeSend = BeforeSendCallback { event: SentryEvent, _: Any? ->
// Removing unneeded info from event
event.contexts.device?.apply {
name = null
timezone = null
isCharging = null
bootTime = null
freeStorage = null
batteryTemperature = null
}
event
}
}
}
}
override fun uncaughtException(
t: Thread,
exception: Throwable,
defaultExceptionHandler: Thread.UncaughtExceptionHandler
) {
// When enabled, Sentry auto captures unhandled exceptions
if (!enabled) {
CrashReporterHelper.startErrorReporterActivity(application, exception)
}
defaultExceptionHandler.uncaughtException(t, exception)
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">تمكين الحراسة</string>
<string name="sentry_summary">الحراسة هي مستمع/معالج للأخطاء الذي يرسل الخطأ/الاحداث إلى Sentry.io</string>
<string name="sentry_start_next_launch">ستبدأ الحراسة عند التشغيل التالي</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Habilita el Sentry</string>
<string name="sentry_summary">Sentry és un oient/intèrpret d\'error que envia asíncronament l\'error/esdeveniment a Sentry.io</string>
<string name="sentry_start_next_launch">Sentry s\'iniciarà al pròxim llançament</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Povolit Sentry</string>
<string name="sentry_summary">Sentry je listener/handler, který zaznamenává chyby a asynchronně je posílá na Sentry.io</string>
<string name="sentry_start_next_launch">Sentry se spustí při příštím spuštění</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Aktiviere Sentry</string>
<string name="sentry_summary">Sentry ist ein Listener/Handler für Fehler, der den Fehler/das Ereignis asynchron an Sentry.io sendet</string>
<string name="sentry_start_next_launch">Sentry startet beim nächsten Start</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Ενεργοποίηση Sentry</string>
<string name="sentry_summary">Το Sentry είναι διαχειριστής σφαλμάτων ασύγχρονης αποστολής του σφάλματος/συμβάντος στο Sentry.io</string>
<string name="sentry_start_next_launch">Το Sentry θα ξεκινήσει στην επόμενη εκκίνηση</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Activar Sentry</string>
<string name="sentry_summary">Sentry es un oyente/manejador de errores que asincrónicamente envía el error/evento a Sentry.io</string>
<string name="sentry_start_next_launch">Sentry comenzará en el próximo inicio</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">فعالسازی Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry در اجرای بعدی، شروع خواهد شد</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Activer Sentry</string>
<string name="sentry_summary">Sentry est un écouteur/gestionnaire d\'erreurs qui envoie de manière asynchrone l\'erreur/l\'événement à Sentry.io</string>
<string name="sentry_start_next_launch">Sentry commencera au prochain lancement</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Hidupkan Sentry</string>
<string name="sentry_summary">Sentry adalah sebuah pendengar/penanganan eror yang secara asinkronis mengirimkan eror/kejadian ke Sentry.io</string>
<string name="sentry_start_next_launch">Sentry akan dihidupkan pada peluncuran berikutnya</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Abilita Sentry</string>
<string name="sentry_summary">Sentry è un ascoltatore/gestore di errori che invia asincronicamente l\'errore/evento a Sentry.io</string>
<string name="sentry_start_next_launch">Sentry comincerà al prossimo lancio</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Sentry 활성화</string>
<string name="sentry_summary">Sentry는 Sentry.io에 오류를 비동기적으로 보내는 오류 처리기입니다</string>
<string name="sentry_start_next_launch">Sentry는 다음 출시에 시작됩니다</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Овозможи Sentry</string>
<string name="sentry_summary">Sentry е слушач на грешки кој асинхроно ги испраќа на Sentry.io страната</string>
<string name="sentry_start_next_launch">Sentry ќе биде овозможен на следно отварање</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Sentry inschakelen</string>
<string name="sentry_summary">Sentry is een luister/handler voor fouten die asynchroon de fout/gebeurtenis versturen naar Sentry.io</string>
<string name="sentry_start_next_launch">Sentry zal starten bij de volgende lancering</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Sentryକୁ ସକ୍ଷମ କରନ୍ତୁ</string>
<string name="sentry_summary">ତ୍ରୁଟି ପାଇଁ ସେଣ୍ଟ୍ରି ହେଉଛି ଏକ ଶ୍ରୋତା ଯାହା ତ୍ରୁଟି / ଘଟଣାକୁ Sentry.io କୁ ପଠାଏ |</string>
<string name="sentry_start_next_launch">ପରବର୍ତ୍ତୀ ଲଞ୍ଚଠାରୁ ସେଣ୍ଟ୍ରି ଆରମ୍ଭ ହେବ |</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Włącz Sentry</string>
<string name="sentry_summary">Sentry jest słuchaczem/obsługą błędów, które asynchronicznie wysyłają błąd/zdarzenie do Sentry.io</string>
<string name="sentry_start_next_launch">Sentry rozpocznie się przy następnym uruchomieniu</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Ativar Sentry</string>
<string name="sentry_summary">Sentry é um ouvinte/gestor de erros que assincronicamente envia o erro/evento para Sentry.io</string>
<string name="sentry_start_next_launch">Sentry começará no próximo início</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Включить Sentry</string>
<string name="sentry_summary">Sentry - это обработчик событий, который асинхронно отправляет сообщения об ошибках/поломках на Sentry.io</string>
<string name="sentry_start_next_launch">Sentry включится при следующем запуске приложения</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Bật Sentry</string>
<string name="sentry_summary">Sentry là một thiết bị nghe/giải quyết cho những lỗi mà gửi những lỗi/sự kiện đến Sentry.io một cách tách biệt</string>
<string name="sentry_start_next_launch">Sentry sẽ được bật vào lần khởi động kế tiếp</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">启用 Sentry</string>
<string name="sentry_summary">Sentry 会将错误报告发送至 Sentry.io</string>
<string name="sentry_start_next_launch">启用 Sentry 将在下次启动应用时生效</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">啟用 Sentry</string>
<string name="sentry_summary">Sentry 會將錯誤報告發送至 Sentry.io</string>
<string name="sentry_start_next_launch">下次啟用應用程式時將會開啟 Sentry</string>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="enable_sentry">Enable Sentry</string>
<string name="sentry_summary">Sentry is a listener/handler for errors that asynchronously sends out the error/event to Sentry.io</string>
<string name="sentry_start_next_launch">Sentry will start on next launch</string>
</resources>

143
app/src/main/AndroidManifest.xml Executable file
View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="awais.instagrabber">
<uses-permission android:name="android.permission.INTERNET" />
<!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<!--<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />-->
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
<application
android:name=".InstaGrabberApplication"
android:allowBackup="true"
android:backupAgent=".backup.BarinstaBackupAgent"
android:fullBackupContent="@xml/backup_descriptor"
android:fullBackupOnly="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme.Launcher"
tools:ignore="UnusedAttribute">
<activity
android:name=".activities.MainActivity"
android:launchMode="singleTop"
android:taskAffinity=".Main">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<!-- <action android:name="android.intent.action.SEARCH" /> -->
<!-- <action android:name="android.intent.action.WEB_SEARCH" /> -->
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="instagr.am" />
<data android:host="www.instagr.am" />
<data android:host="instagram.com" />
<data android:host="www.instagram.com" />
<data android:pathPrefix="/" />
<data android:pathPrefix="/p" />
<data android:pathPrefix="/explore/tags" />
<data android:pathPrefix="/explore/locations" />
</intent-filter>
</activity>
<activity
android:name="awaisomereport.ErrorReporterActivity"
android:allowEmbedded="false"
android:allowTaskReparenting="false"
android:autoRemoveFromRecents="true"
android:clearTaskOnLaunch="true"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:documentLaunchMode="never"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true"
android:launchMode="singleTask"
android:lockTaskMode="never"
android:noHistory="false"
android:screenOrientation="portrait"
android:taskAffinity="awais.instagrabber.errorreport"
android:theme="@android:style/Theme.DeviceDefault.Dialog" />
<activity
android:name=".activities.Login"
android:label="@string/login"
android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.Light.White">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.MainActivity" />
</activity>
<activity
android:name=".activities.CameraActivity"
android:label="Camera"
android:parentActivityName=".activities.MainActivity"
android:theme="@style/AppTheme.Light.White">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activities.MainActivity" />
</activity>
<activity
android:name=".utils.ProcessPhoenix"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".activities.DirectorySelectActivity" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<service
android:name=".services.ActivityCheckerService"
android:exported="false" />
<service
android:name=".services.DeleteImageIntentService"
android:exported="false" />
<service
android:name=".services.DMSyncService"
android:exported="false" />
<receiver
android:name=".services.DMSyncAlarmReceiver"
android:enabled="true"
android:exported="false" />
<!--<receiver android:name=".services.BootCompletedReceiver" >-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.BOOT_COMPLETED" />-->
<!-- </intent-filter>-->
<!--</receiver>-->
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
<meta-data
android:name="fontProviderRequests"
android:value="Noto Color Emoji Compat" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -0,0 +1,70 @@
package awais.instagrabber
import android.app.Application
import android.content.ClipboardManager
import android.util.Log
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT
import awais.instagrabber.fragments.settings.PreferenceKeys.CUSTOM_DATE_TIME_FORMAT_ENABLED
import awais.instagrabber.fragments.settings.PreferenceKeys.DATE_TIME_FORMAT
import awais.instagrabber.utils.*
import awais.instagrabber.utils.LocaleUtils.currentLocale
import awais.instagrabber.utils.Utils.settingsHelper
import awais.instagrabber.utils.extensions.TAG
import awaisomereport.CrashReporter
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import java.net.CookieHandler
import java.time.format.DateTimeFormatter
import java.util.*
@Suppress("unused")
class InstaGrabberApplication : Application() {
override fun onCreate() {
super.onCreate()
CookieHandler.setDefault(NET_COOKIE_MANAGER)
settingsHelper = SettingsHelper(this)
setupCrashReporter()
setupCloseGuard()
setupFresco()
Utils.cacheDir = cacheDir.absolutePath
Utils.clipboardManager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
LocaleUtils.setLocale(baseContext)
val pattern = if (settingsHelper.getBoolean(CUSTOM_DATE_TIME_FORMAT_ENABLED)) {
settingsHelper.getString(CUSTOM_DATE_TIME_FORMAT)
} else {
settingsHelper.getString(DATE_TIME_FORMAT)
}
TextUtils.setFormatter(DateTimeFormatter.ofPattern(pattern, currentLocale))
if (TextUtils.isEmpty(settingsHelper.getString(Constants.DEVICE_UUID))) {
settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
}
private fun setupCrashReporter() {
if (BuildConfig.DEBUG) return
CrashReporter.getInstance(this).start()
}
private fun setupCloseGuard() {
if (!BuildConfig.DEBUG) return
try {
Class.forName("dalvik.system.CloseGuard")
.getMethod("setEnabled", Boolean::class.javaPrimitiveType)
.invoke(null, true)
} catch (e: Exception) {
Log.e(TAG, "Error", e)
}
}
private fun setupFresco() {
// final Set<RequestListener> requestListeners = new HashSet<>();
// requestListeners.add(new RequestLoggingListener());
val imagePipelineConfig = ImagePipelineConfig
.newBuilder(this) // .setMainDiskCacheConfig(diskCacheConfig)
// .setRequestListeners(requestListeners)
.setDownsampleEnabled(true)
.build()
Fresco.initialize(this, imagePipelineConfig)
// FLog.setMinimumLoggingLevel(FLog.VERBOSE);
}
}

View File

@ -0,0 +1,18 @@
package awais.instagrabber.activities
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import awais.instagrabber.utils.LocaleUtils
import awais.instagrabber.utils.ThemeUtils
abstract class BaseLanguageActivity protected constructor() : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtils.changeTheme(this)
super.onCreate(savedInstanceState)
}
init {
@Suppress("LeakingThis")
LocaleUtils.updateConfig(this)
}
}

View File

@ -0,0 +1,240 @@
package awais.instagrabber.activities
import android.content.Intent
import android.content.res.Configuration
import android.hardware.display.DisplayManager
import android.hardware.display.DisplayManager.DisplayListener
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import awais.instagrabber.databinding.ActivityCameraBinding
import awais.instagrabber.utils.DownloadUtils
import awais.instagrabber.utils.PermissionUtils
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.TAG
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityCameraBinding
private lateinit var displayManager: DisplayManager
private lateinit var cameraExecutor: ExecutorService
private var outputDirectory: DocumentFile? = null
private var imageCapture: ImageCapture? = null
private var displayId = -1
private var cameraProvider: ProcessCameraProvider? = null
private var lensFacing = 0
private val cameraRequestCode = 100
private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
private val displayListener: DisplayListener = object : DisplayListener {
override fun onDisplayAdded(displayId: Int) {}
override fun onDisplayRemoved(displayId: Int) {}
override fun onDisplayChanged(displayId: Int) {
if (displayId == this@CameraActivity.displayId) {
imageCapture?.targetRotation = binding.root.display.rotation
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(LayoutInflater.from(baseContext))
setContentView(binding.root)
Utils.transparentStatusBar(this, true, false)
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
outputDirectory = DownloadUtils.cameraDir
cameraExecutor = Executors.newSingleThreadExecutor()
displayManager.registerDisplayListener(displayListener, null)
binding.viewFinder.post {
displayId = binding.viewFinder.display.displayId
updateUi()
checkPermissionsAndSetupCamera()
}
}
override fun onResume() {
super.onResume()
// Make sure that all permissions are still present, since the
// user could have removed them while the app was in paused state.
if (!PermissionUtils.hasCameraPerms(this)) {
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// Redraw the camera UI controls
updateUi()
// Enable or disable switching between cameras
updateCameraSwitchButton()
}
override fun onDestroy() {
super.onDestroy()
Utils.transparentStatusBar(this, false, false)
cameraExecutor.shutdown()
displayManager.unregisterDisplayListener(displayListener)
}
private fun updateUi() {
binding.cameraCaptureButton.setOnClickListener { takePhoto() }
// Disable the button until the camera is set up
binding.switchCamera.isEnabled = false
// Listener for button used to switch cameras. Only called if the button is enabled
binding.switchCamera.setOnClickListener {
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) CameraSelector.LENS_FACING_BACK else CameraSelector.LENS_FACING_FRONT
// Re-bind use cases to update selected camera
bindCameraUseCases()
}
binding.close.setOnClickListener {
setResult(RESULT_CANCELED)
finish()
}
}
private fun checkPermissionsAndSetupCamera() {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
return
}
PermissionUtils.requestCameraPerms(this, cameraRequestCode)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == cameraRequestCode) {
if (PermissionUtils.hasCameraPerms(this)) {
setupCamera()
}
}
}
private fun setupCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
try {
cameraProvider = cameraProviderFuture.get()
// Select lensFacing depending on the available cameras
lensFacing = -1
if (hasBackCamera()) {
lensFacing = CameraSelector.LENS_FACING_BACK
} else if (hasFrontCamera()) {
lensFacing = CameraSelector.LENS_FACING_FRONT
}
check(lensFacing != -1) { "Back and front camera are unavailable" }
// Enable or disable switching between cameras
updateCameraSwitchButton()
// Build and bind the camera use cases
bindCameraUseCases()
} catch (e: ExecutionException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: InterruptedException) {
Log.e(TAG, "setupCamera: ", e)
} catch (e: CameraInfoUnavailableException) {
Log.e(TAG, "setupCamera: ", e)
}
}, ContextCompat.getMainExecutor(this))
}
private fun bindCameraUseCases() {
val rotation = binding.viewFinder.display.rotation
// CameraSelector
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(lensFacing)
.build()
// Preview
val preview = Preview.Builder() // Set initial target rotation
.setTargetRotation(rotation)
.build()
// ImageCapture
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // Set initial target rotation, we will have to call this again if rotation changes
// during the lifecycle of this use case
.setTargetRotation(rotation)
.build()
cameraProvider?.unbindAll()
cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture)
preview.setSurfaceProvider(binding.viewFinder.surfaceProvider)
}
private fun takePhoto() {
if (imageCapture == null) return
val fileName = simpleDateFormat.format(System.currentTimeMillis()) + ".jpg"
val mimeType = "image/jpg"
val photoFile = outputDirectory?.createFile(mimeType, fileName)?.let { it } ?: return
val outputStream = contentResolver.openOutputStream(photoFile.uri)?.let { it } ?: return
val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputStream).build()
imageCapture?.takePicture(
outputFileOptions,
cameraExecutor,
object : ImageCapture.OnImageSavedCallback {
@Suppress("UnstableApiUsage")
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
try { outputStream.close() } catch (ignored: IOException) {}
val intent = Intent()
intent.data = photoFile.uri
setResult(RESULT_OK, intent)
finish()
Log.d(TAG, "onImageSaved: " + photoFile.uri)
}
override fun onError(exception: ImageCaptureException) {
Log.e(TAG, "onError: ", exception)
try { outputStream.close() } catch (ignored: IOException) {}
}
}
)
// We can only change the foreground Drawable using API level 23+ API
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// // Display flash animation to indicate that photo was captured
// final ConstraintLayout container = binding.getRoot();
// container.postDelayed(() -> {
// container.setForeground(new ColorDrawable(Color.WHITE));
// container.postDelayed(() -> container.setForeground(null), 50);
// }, 100);
// }
}
/**
* Enabled or disabled a button to switch cameras depending on the available cameras
*/
private fun updateCameraSwitchButton() {
try {
binding.switchCamera.isEnabled = hasBackCamera() && hasFrontCamera()
} catch (e: CameraInfoUnavailableException) {
binding.switchCamera.isEnabled = false
}
}
/**
* Returns true if the device has an available back camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasBackCamera(): Boolean {
return if (cameraProvider == null) false else cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
}
/**
* Returns true if the device has an available front camera. False otherwise
*/
@Throws(CameraInfoUnavailableException::class)
private fun hasFrontCamera(): Boolean {
return if (cameraProvider == null) {
false
} else cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
}
}

View File

@ -0,0 +1,129 @@
package awais.instagrabber.activities;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.util.Log;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ActivityDirectorySelectBinding;
import awais.instagrabber.dialogs.ConfirmDialogFragment;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.viewmodels.DirectorySelectActivityViewModel;
public class DirectorySelectActivity extends BaseLanguageActivity {
private static final String TAG = DirectorySelectActivity.class.getSimpleName();
public static final int SELECT_DIR_REQUEST_CODE = 0x01;
private static final int ERROR_REQUEST_CODE = 0x02;
private Uri initialUri;
private ActivityDirectorySelectBinding binding;
private DirectorySelectActivityViewModel viewModel;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityDirectorySelectBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
viewModel = new ViewModelProvider(this).get(DirectorySelectActivityViewModel.class);
setupObservers();
binding.selectDir.setOnClickListener(v -> openDirectoryChooser());
AppExecutors.INSTANCE.getMainThread().execute(() -> viewModel.setInitialUri(getIntent()));
}
private void setupObservers() {
viewModel.getMessage().observe(this, message -> binding.message.setText(message));
viewModel.getPrevUri().observe(this, prevUri -> {
if (prevUri == null) {
binding.prevUri.setVisibility(View.GONE);
binding.message2.setVisibility(View.GONE);
return;
}
binding.prevUri.setText(prevUri);
binding.prevUri.setVisibility(View.VISIBLE);
binding.message2.setVisibility(View.VISIBLE);
});
viewModel.getDirSuccess().observe(this, success -> binding.selectDir.setVisibility(success ? View.GONE : View.VISIBLE));
viewModel.isLoading().observe(this, loading -> {
binding.message.setVisibility(loading ? View.GONE : View.VISIBLE);
binding.loadingIndicator.setVisibility(loading ? View.VISIBLE : View.GONE);
});
}
private void openDirectoryChooser() {
final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) {
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri);
}
try {
startActivityForResult(intent, SELECT_DIR_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Log.e(TAG, "openDirectoryChooser: ", e);
showErrorDialog(getString(R.string.no_directory_picker_activity));
} catch (Exception e) {
Log.e(TAG, "openDirectoryChooser: ", e);
}
}
@SuppressLint("StringFormatInvalid")
@Override
protected void onActivityResult(final int requestCode, final int resultCode, @Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode != SELECT_DIR_REQUEST_CODE) return;
if (resultCode != RESULT_OK) {
showErrorDialog(getString(R.string.select_a_folder));
return;
}
if (data == null || data.getData() == null) {
showErrorDialog(getString(R.string.select_a_folder));
return;
}
if (!"com.android.externalstorage.documents".equals(data.getData().getAuthority())) {
showErrorDialog(getString(R.string.dir_select_no_download_folder, data.getData().getAuthority()));
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> {
try {
viewModel.setupSelectedDir(data);
final Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
} catch (Exception e) {
// Should not come to this point.
// If it does, we have to show this error to the user so that they can report it.
try (final StringWriter sw = new StringWriter();
final PrintWriter pw = new PrintWriter(sw)) {
e.printStackTrace(pw);
showErrorDialog("Please report this error to the developers:\n\n" + sw.toString());
} catch (IOException ioException) {
Log.e(TAG, "onActivityResult: ", ioException);
}
}
}, 500);
}
private void showErrorDialog(@NonNull final String message) {
final ConfirmDialogFragment dialogFragment = ConfirmDialogFragment.newInstance(
ERROR_REQUEST_CODE,
R.string.error,
message,
R.string.ok,
0,
0
);
dialogFragment.show(getSupportFragmentManager(), ConfirmDialogFragment.class.getSimpleName());
}
}

View File

@ -0,0 +1,119 @@
package awais.instagrabber.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Bitmap
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.webkit.*
import android.widget.Toast
import awais.instagrabber.R
import awais.instagrabber.databinding.ActivityLoginBinding
import awais.instagrabber.utils.Constants
import awais.instagrabber.utils.getCookie
class Login : BaseLanguageActivity(), View.OnClickListener {
private var webViewUrl: String? = null
private var ready = false
private lateinit var loginBinding: ActivityLoginBinding
private val webChromeClient = WebChromeClient()
private val webViewClient: WebViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
webViewUrl = url
}
override fun onPageFinished(view: WebView, url: String) {
webViewUrl = url
val mainCookie = getCookie(url)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
ready = true
return
}
if (mainCookie.contains("; ds_user_id=") && ready) {
returnCookieResult(mainCookie)
}
}
}
private fun returnCookieResult(mainCookie: String?) {
val intent = Intent()
intent.putExtra("cookie", mainCookie)
setResult(Constants.LOGIN_RESULT_CODE, intent)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loginBinding = ActivityLoginBinding.inflate(LayoutInflater.from(applicationContext))
setContentView(loginBinding.root)
initWebView()
loginBinding.cookies.setOnClickListener(this)
loginBinding.refresh.setOnClickListener(this)
}
override fun onClick(v: View) {
if (v === loginBinding.refresh) {
loginBinding.webView.loadUrl("https://instagram.com/")
return
}
if (v === loginBinding.cookies) {
val mainCookie = getCookie(webViewUrl)
if (mainCookie.isNullOrBlank() || !mainCookie.contains("; ds_user_id=")) {
Toast.makeText(this, R.string.login_error_loading_cookies, Toast.LENGTH_SHORT).show()
return
}
returnCookieResult(mainCookie)
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
loginBinding.webView.webChromeClient = webChromeClient
loginBinding.webView.webViewClient = webViewClient
val webSettings = loginBinding.webView.settings
webSettings.userAgentString =
"Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36"
webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true
webSettings.setSupportZoom(true)
webSettings.builtInZoomControls = true
webSettings.displayZoomControls = false
webSettings.loadWithOverviewMode = true
webSettings.useWideViewPort = true
webSettings.allowFileAccessFromFileURLs = true
webSettings.allowUniversalAccessFromFileURLs = true
webSettings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
} else {
val cookieSyncMngr = CookieSyncManager.createInstance(applicationContext)
cookieSyncMngr.startSync()
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookie()
cookieManager.removeSessionCookie()
cookieSyncMngr.stopSync()
cookieSyncMngr.sync()
}
loginBinding.webView.loadUrl("https://instagram.com/")
}
override fun onPause() {
loginBinding.webView.onPause()
super.onPause()
}
override fun onResume() {
super.onResume()
loginBinding.webView.onResume()
}
override fun onDestroy() {
loginBinding.webView.destroy()
super.onDestroy()
}
}

View File

@ -0,0 +1,715 @@
package awais.instagrabber.activities
import android.animation.LayoutTransition
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.*
import android.provider.DocumentsContract.EXTRA_INITIAL_URI
import android.text.Editable
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.NotificationManagerCompat
import androidx.core.provider.FontRequest
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback
import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavGraph
import androidx.navigation.NavGraphNavigator
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.*
import awais.instagrabber.BuildConfig
import awais.instagrabber.R
import awais.instagrabber.customviews.emoji.EmojiVariantManager
import awais.instagrabber.customviews.helpers.RootViewDeferringInsetsCallback
import awais.instagrabber.customviews.helpers.TextWatcherAdapter
import awais.instagrabber.databinding.ActivityMainBinding
import awais.instagrabber.fragments.main.FeedFragment
import awais.instagrabber.fragments.settings.PreferenceKeys
import awais.instagrabber.models.IntentModel
import awais.instagrabber.models.Resource
import awais.instagrabber.models.Tab
import awais.instagrabber.models.enums.IntentModelType
import awais.instagrabber.services.ActivityCheckerService
import awais.instagrabber.services.DMSyncAlarmReceiver
import awais.instagrabber.utils.*
import awais.instagrabber.utils.AppExecutors.tasksThread
import awais.instagrabber.utils.DownloadUtils.ReselectDocumentTreeException
import awais.instagrabber.utils.TextUtils.isEmpty
import awais.instagrabber.utils.emoji.EmojiParser
import awais.instagrabber.viewmodels.AppStateViewModel
import awais.instagrabber.viewmodels.DirectInboxViewModel
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.ScrollingViewBehavior
import com.google.android.material.appbar.CollapsingToolbarLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.textfield.TextInputLayout
import com.google.common.collect.ImmutableList
import java.util.*
class MainActivity : BaseLanguageActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private lateinit var appBarConfiguration: AppBarConfiguration
private var searchMenuItem: MenuItem? = null
private var startNavRootId: Int = 0
private var lastSelectedNavMenuId = 0
private var isActivityCheckerServiceBound = false
private var isLoggedIn = false
private var deviceUuid: String? = null
private var csrfToken: String? = null
private var userId: Long = 0
private var toolbarOwner: Fragment? = null
private lateinit var toolbar: Toolbar
var currentTabs: List<Tab> = emptyList()
private set
private var showBottomViewDestinations: List<Int> = emptyList()
private val serviceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
// final ActivityCheckerService.LocalBinder binder = (ActivityCheckerService.LocalBinder) service;
// final ActivityCheckerService activityCheckerService = binder.getService();
isActivityCheckerServiceBound = true
}
override fun onServiceDisconnected(name: ComponentName) {
isActivityCheckerServiceBound = false
}
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
DownloadUtils.init(
this,
Utils.settingsHelper.getString(PreferenceKeys.PREF_BARINSTA_DIR_URI)
)
} catch (e: ReselectDocumentTreeException) {
super.onCreate(savedInstanceState)
val intent = Intent(this, DirectorySelectActivity::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
intent.putExtra(EXTRA_INITIAL_URI, e.initialUri)
}
startActivity(intent)
finish()
return
}
super.onCreate(savedInstanceState)
instance = this
binding = ActivityMainBinding.inflate(layoutInflater)
toolbar = binding.toolbar
setupCookie()
if (Utils.settingsHelper.getBoolean(PreferenceKeys.FLAG_SECURE)) {
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
}
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
setupInsetsCallback()
createNotificationChannels()
val navHostFragment = supportFragmentManager.findFragmentById(R.id.main_nav_host) as NavHostFragment
navController = navHostFragment.navController
if (savedInstanceState == null) {
setupNavigation(true)
}
if (!BuildConfig.isPre) {
val checkUpdates = Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_UPDATES)
if (checkUpdates) FlavorTown.updateCheck(this)
}
FlavorTown.changelogCheck(this)
ViewModelProvider(this).get(AppStateViewModel::class.java) // Just initiate the App state here
handleIntent(intent)
if (isLoggedIn && Utils.settingsHelper.getBoolean(PreferenceKeys.CHECK_ACTIVITY)) {
bindActivityCheckerService()
}
// Initialise the internal map
tasksThread.execute {
EmojiParser.getInstance(this)
EmojiVariantManager.getInstance()
}
initEmojiCompat()
// initDmService();
initDmUnreadCount()
initSearchInput()
}
private fun setupInsetsCallback() {
val deferringInsetsCallback = RootViewDeferringInsetsCallback(
WindowInsetsCompat.Type.systemBars(),
WindowInsetsCompat.Type.ime()
)
ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsCallback)
ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsCallback)
WindowCompat.setDecorFitsSystemWindows(window, false)
}
private fun setupCookie() {
val cookie = Utils.settingsHelper.getString(Constants.COOKIE)
userId = 0
csrfToken = null
if (cookie.isNotBlank()) {
userId = getUserIdFromCookie(cookie)
csrfToken = getCsrfTokenFromCookie(cookie)
}
if (cookie.isBlank() || userId == 0L || csrfToken.isNullOrBlank()) {
isLoggedIn = false
return
}
deviceUuid = Utils.settingsHelper.getString(Constants.DEVICE_UUID)
if (isEmpty(deviceUuid)) {
Utils.settingsHelper.putString(Constants.DEVICE_UUID, UUID.randomUUID().toString())
}
setupCookies(cookie)
isLoggedIn = true
}
@Suppress("unused")
private fun initDmService() {
if (!isLoggedIn) return
val enabled = Utils.settingsHelper.getBoolean(PreferenceKeys.PREF_ENABLE_DM_AUTO_REFRESH)
if (!enabled) return
DMSyncAlarmReceiver.setAlarm(this)
}
private fun initDmUnreadCount() {
if (!isLoggedIn) return
val directInboxViewModel = ViewModelProvider(this).get(DirectInboxViewModel::class.java)
directInboxViewModel.unseenCount.observe(this, { unseenCountResource: Resource<Int?>? ->
if (unseenCountResource == null) return@observe
val unseenCount = unseenCountResource.data
setNavBarDMUnreadCountBadge(unseenCount ?: 0)
})
}
private fun initSearchInput() {
binding.searchInputLayout.setEndIconOnClickListener {
val editText = binding.searchInputLayout.editText ?: return@setEndIconOnClickListener
editText.setText("")
}
binding.searchInputLayout.addOnEditTextAttachedListener { textInputLayout: TextInputLayout ->
textInputLayout.isEndIconVisible = false
val editText = textInputLayout.editText ?: return@addOnEditTextAttachedListener
editText.addTextChangedListener(object : TextWatcherAdapter() {
override fun afterTextChanged(s: Editable) {
binding.searchInputLayout.isEndIconVisible = !isEmpty(s)
}
})
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_menu, menu)
searchMenuItem = menu.findItem(R.id.search)
val currentDestination = navController.currentDestination
if (currentDestination != null) {
val backStack = navController.backQueue
setupMenu(backStack.size, currentDestination.id)
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.search) {
try {
navController.navigate(getSearchDeepLink())
return true
} catch (e: Exception) {
Log.e(TAG, "onOptionsItemSelected: ", e)
}
return false
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
// outState.putString(FIRST_FRAGMENT_GRAPH_INDEX_KEY, firstFragmentGraphIndex.toString())
outState.putString(LAST_SELECT_NAV_MENU_ID, binding.bottomNavView.selectedItemId.toString())
super.onSaveInstanceState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
val lastSelected = savedInstanceState[LAST_SELECT_NAV_MENU_ID] as String?
if (lastSelected != null) {
try {
lastSelectedNavMenuId = lastSelected.toInt()
} catch (ignored: NumberFormatException) {
}
}
setupNavigation(false)
}
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp(appBarConfiguration)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
override fun onDestroy() {
try {
super.onDestroy()
} catch (e: Exception) {
Log.e(TAG, "onDestroy: ", e)
}
unbindActivityCheckerService()
// try {
// RetrofitFactory.getInstance().destroy()
// } catch (e: Exception) {
// Log.e(TAG, "onDestroy: ", e)
// }
DownloadUtils.destroy()
instance = null
}
// override fun onBackPressed() {
// Log.d(TAG, "onBackPressed: ")
// navController.navigateUp()
// val backStack = navController.backQueue
// val currentNavControllerBackStack = backStack.size
// if (isTaskRoot && isBackStackEmpty && currentNavControllerBackStack == 2) {
// finishAfterTransition()
// return
// }
// if (!isFinishing) {
// try {
// super.onBackPressed()
// } catch (e: Exception) {
// Log.e(TAG, "onBackPressed: ", e)
// finish()
// }
// }
// }
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val notificationManager = NotificationManagerCompat.from(applicationContext)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DOWNLOAD_CHANNEL_ID,
Constants.DOWNLOAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.ACTIVITY_CHANNEL_ID,
Constants.ACTIVITY_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
notificationManager.createNotificationChannel(
NotificationChannel(
Constants.DM_UNREAD_CHANNEL_ID,
Constants.DM_UNREAD_CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
)
)
val silentNotificationChannel = NotificationChannel(
Constants.SILENT_NOTIFICATIONS_CHANNEL_ID,
Constants.SILENT_NOTIFICATIONS_CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
silentNotificationChannel.setSound(null, null)
notificationManager.createNotificationChannel(silentNotificationChannel)
}
private fun setupNavigation(setDefaultTabFromSettings: Boolean) {
currentTabs = if (isLoggedIn) setupMainBottomNav() else setupAnonBottomNav()
showBottomViewDestinations = currentTabs.asSequence().map {
it.startDestinationFragmentId
}.toMutableList().apply {
add(R.id.postViewFragment)
add(R.id.favorites_non_top)
add(R.id.notifications_viewer_non_top)
add(R.id.profile_non_top)
}
if (setDefaultTabFromSettings) {
setSelectedTab(currentTabs)
} else {
binding.bottomNavView.selectedItemId = lastSelectedNavMenuId
}
val navigatorProvider = navController.navigatorProvider
val navigator = navigatorProvider.getNavigator<NavGraphNavigator>("navigation")
val rootNavGraph = NavGraph(navigator)
val navInflater = navController.navInflater
val topLevelDestinations = currentTabs.map { navInflater.inflate(it.navigationResId) }
rootNavGraph.id = R.id.root_nav_graph
rootNavGraph.label = "root_nav_graph"
rootNavGraph.addDestinations(topLevelDestinations)
rootNavGraph.setStartDestination(if (startNavRootId != 0) startNavRootId else R.id.profile_nav_graph)
navController.graph = rootNavGraph
binding.bottomNavView.setupWithNavController(navController)
appBarConfiguration = AppBarConfiguration(currentTabs.map { it.startDestinationFragmentId }.toSet())
setupActionBarWithNavController(navController, appBarConfiguration)
navController.addOnDestinationChangedListener { _: NavController?, destination: NavDestination, arguments: Bundle? ->
if (destination.id == R.id.directMessagesThreadFragment && arguments != null) {
// Set the thread title earlier for better ux
val title = arguments.getString("title")
if (!title.isNullOrBlank()) {
supportActionBar?.title = title
}
}
if (destination.id == R.id.profileFragment && arguments != null) {
// Set the title to username
val username = arguments.getString("username")
if (!username.isNullOrBlank()) {
supportActionBar?.title = username.substringAfter("@")
}
}
// below is a hack to check if we are at the end of the current stack, to setup the search view
binding.appBarLayout.setExpanded(true, true)
val destinationId = destination.id
val backStack = navController.backQueue
setupMenu(backStack.size, destinationId)
val contains = showBottomViewDestinations.contains(destinationId)
binding.root.post {
binding.bottomNavView.visibility = if (contains) View.VISIBLE else View.GONE
// if (contains) {
// behavior?.slideUp(binding.bottomNavView)
// }
}
// explicitly hide keyboard when we navigate
val view = currentFocus
Utils.hideKeyboard(view)
}
setupReselection()
}
private fun setupReselection() {
binding.bottomNavView.setOnItemReselectedListener {
val navHostFragment = (supportFragmentManager.primaryNavigationFragment ?: return@setOnItemReselectedListener) as NavHostFragment
val currentFragment = navHostFragment.childFragmentManager.fragments.firstOrNull() ?: return@setOnItemReselectedListener
if (currentFragment is FeedFragment) {
currentFragment.scrollToTop()
return@setOnItemReselectedListener
}
val currentDestination = navController.currentDestination ?: return@setOnItemReselectedListener
val currentTabStartDestId = (navController.getBackStackEntry(it.itemId).destination as NavGraph).startDestinationId
if (currentDestination.id == currentTabStartDestId) return@setOnItemReselectedListener
navController.popBackStack(currentTabStartDestId, false)
}
}
private fun setSelectedTab(tabs: List<Tab>) {
val defaultTabResNameString = Utils.settingsHelper.getString(Constants.DEFAULT_TAB)
try {
var navId = 0
if (defaultTabResNameString.isNotBlank()) {
navId = resources.getIdentifier(defaultTabResNameString, "id", packageName)
}
val startFragmentNavResId = if (navId <= 0) R.id.profile_nav_graph else navId
val tab = tabs.firstOrNull { it.navigationRootId == startFragmentNavResId }
// if (index < 0 || index >= tabs.size) index = 0
val firstTab = tab ?: tabs[0]
startNavRootId = firstTab.navigationRootId
binding.bottomNavView.selectedItemId = firstTab.navigationRootId
} catch (e: Exception) {
Log.e(TAG, "Error parsing id", e)
}
}
private fun setupAnonBottomNav(): List<Tab> {
val selectedItemId = binding.bottomNavView.selectedItemId
val anonNavTabs = getAnonNavTabs(this)
val menu = binding.bottomNavView.menu
menu.clear()
for (tab in anonNavTabs) {
menu.add(0, tab.navigationRootId, 0, tab.title).setIcon(tab.iconResId)
}
if (selectedItemId != R.id.profile_nav_graph && selectedItemId != R.id.more_nav_graph && selectedItemId != R.id.favorites_nav_graph) {
binding.bottomNavView.selectedItemId = R.id.profile_nav_graph
}
return anonNavTabs
}
private fun setupMainBottomNav(): List<Tab> {
val menu = binding.bottomNavView.menu
menu.clear()
val navTabList = getLoggedInNavTabs(this).first
for (tab in navTabList) {
menu.add(0, tab.navigationRootId, 0, tab.title).setIcon(tab.iconResId)
}
return navTabList
}
private fun setupMenu(backStackSize: Int, destinationId: Int) {
val searchMenuItem = searchMenuItem ?: return
if (backStackSize >= 2 && SEARCH_VISIBLE_DESTINATIONS.contains(destinationId)) {
searchMenuItem.isVisible = true
return
}
searchMenuItem.isVisible = false
}
private fun setScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = ScrollingViewBehavior()
binding.mainNavHost.requestLayout()
}
private fun removeScrollingBehaviour() {
val layoutParams = binding.mainNavHost.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.behavior = null
binding.mainNavHost.requestLayout()
}
private fun handleIntent(intent: Intent?) {
if (intent == null) return
val action = intent.action
val type = intent.type
// Log.d(TAG, action + " " + type);
if (Intent.ACTION_MAIN == action) return
if (Constants.ACTION_SHOW_ACTIVITY == action) {
showActivityView()
return
}
if (Constants.ACTION_SHOW_DM_THREAD == action) {
showThread(intent)
return
}
if (Intent.ACTION_SEND == action && type != null) {
if (type == "text/plain") {
handleUrl(intent.getStringExtra(Intent.EXTRA_TEXT))
}
return
}
if (Intent.ACTION_VIEW == action) {
val data = intent.data ?: return
handleUrl(data.toString())
}
}
private fun showThread(intent: Intent) {
val threadId = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_ID)
val threadTitle = intent.getStringExtra(Constants.DM_THREAD_ACTION_EXTRA_THREAD_TITLE)
navigateToThread(threadId, threadTitle)
}
fun navigateToThread(threadId: String?, threadTitle: String?) {
if (threadId == null || threadTitle == null) return
try {
navController.navigate(getDirectThreadDeepLink(threadId, threadTitle))
} catch (e: Exception) {
Log.e(TAG, "navigateToThread: ", e)
}
}
private fun handleUrl(url: String?) {
if (url == null) return
// Log.d(TAG, url);
val intentModel = IntentUtils.parseUrl(url) ?: return
showView(intentModel)
}
private fun showView(intentModel: IntentModel) {
when (intentModel.type) {
IntentModelType.USERNAME -> showProfileView(intentModel)
IntentModelType.POST -> showPostView(intentModel)
IntentModelType.LOCATION -> showLocationView(intentModel)
IntentModelType.HASHTAG -> showHashtagView(intentModel)
IntentModelType.UNKNOWN -> Log.w(TAG, "Unknown model type received!")
// else -> Log.w(TAG, "Unknown model type received!")
}
}
private fun showProfileView(intentModel: IntentModel) {
try {
val username = intentModel.text
navController.navigate(getProfileDeepLink(username))
} catch (e: Exception) {
Log.e(TAG, "showProfileView: ", e)
}
}
private fun showPostView(intentModel: IntentModel) {
val shortCode = intentModel.text
// Log.d(TAG, "shortCode: " + shortCode);
try {
navController.navigate(getPostDeepLink(shortCode))
} catch (e: Exception) {
Log.e(TAG, "showPostView: ", e)
}
}
private fun showLocationView(intentModel: IntentModel) {
val locationId = intentModel.text
// Log.d(TAG, "locationId: " + locationId);
try {
navController.navigate(getLocationDeepLink(locationId))
} catch (e: Exception) {
Log.e(TAG, "showLocationView: ", e)
}
}
private fun showHashtagView(intentModel: IntentModel) {
val hashtag = intentModel.text
// Log.d(TAG, "hashtag: " + hashtag);
try {
navController.navigate(getHashtagDeepLink(hashtag))
} catch (e: Exception) {
Log.e(TAG, "showHashtagView: ", e)
}
}
private fun showActivityView() {
try {
navController.navigate(getNotificationsDeepLink("notif"))
} catch (e: Exception) {
Log.e(TAG, "showActivityView: ", e)
}
}
private fun bindActivityCheckerService() {
bindService(Intent(this, ActivityCheckerService::class.java), serviceConnection, BIND_AUTO_CREATE)
isActivityCheckerServiceBound = true
}
private fun unbindActivityCheckerService() {
if (!isActivityCheckerServiceBound) return
unbindService(serviceConnection)
isActivityCheckerServiceBound = false
}
val bottomNavView: BottomNavigationView
get() = binding.bottomNavView
// fun setCollapsingView(view: View) {
// try {
// binding.collapsingToolbarLayout.addView(view, 0)
// } catch (e: Exception) {
// Log.e(TAG, "setCollapsingView: ", e)
// }
// }
//
// fun removeCollapsingView(view: View) {
// try {
// binding.collapsingToolbarLayout.removeView(view)
// } catch (e: Exception) {
// Log.e(TAG, "removeCollapsingView: ", e)
// }
// }
val collapsingToolbarView: CollapsingToolbarLayout
get() = binding.collapsingToolbarLayout
val appbarLayout: AppBarLayout
get() = binding.appBarLayout
fun removeLayoutTransition() {
binding.root.layoutTransition = null
}
fun setLayoutTransition() {
binding.root.layoutTransition = LayoutTransition()
}
private fun initEmojiCompat() {
// Use a downloadable font for EmojiCompat
val fontRequest = FontRequest(
"com.google.android.gms.fonts",
"com.google.android.gms",
"Noto Color Emoji Compat",
R.array.com_google_android_gms_fonts_certs
)
val config: EmojiCompat.Config = FontRequestEmojiCompatConfig(applicationContext, fontRequest)
config.setReplaceAll(true) // .setUseEmojiAsDefaultStyle(true)
.registerInitCallback(object : InitCallback() {
override fun onInitialized() {
Log.i(TAG, "EmojiCompat initialized")
}
override fun onFailed(throwable: Throwable?) {
Log.e(TAG, "EmojiCompat initialization failed", throwable)
}
})
EmojiCompat.init(config)
}
val rootView: View
get() = binding.root
private val toolbarLock = Any()
fun getToolbar() = synchronized(toolbarLock) { this.toolbar }
fun setToolbar(toolbar: Toolbar, owner: Fragment) = synchronized(toolbarLock) {
supportActionBar?.subtitle = null
toolbarOwner = owner
binding.appBarLayout.visibility = View.GONE
removeScrollingBehaviour()
setSupportActionBar(toolbar)
this.toolbar = toolbar
NavigationUI.setupWithNavController(toolbar, navController, appBarConfiguration)
}
fun resetToolbar(owner: Fragment) = synchronized(toolbarLock) {
if (owner != toolbarOwner) return
this.toolbar = binding.toolbar
setSupportActionBar(binding.toolbar)
binding.appBarLayout.visibility = View.VISIBLE
setScrollingBehaviour()
setupActionBarWithNavController(navController, appBarConfiguration)
toolbarOwner = null
}
private fun setNavBarDMUnreadCountBadge(unseenCount: Int) {
val badge = binding.bottomNavView.getOrCreateBadge(R.id.direct_messages_nav_graph)
if (unseenCount == 0) {
badge.isVisible = false
badge.clearNumber()
return
}
if (badge.verticalOffset != 10) {
badge.verticalOffset = 10
}
badge.number = unseenCount
badge.isVisible = true
}
fun showSearchView(): TextInputLayout {
binding.searchInputLayout.visibility = View.VISIBLE
return binding.searchInputLayout
}
fun hideSearchView() {
binding.searchInputLayout.visibility = View.GONE
}
companion object {
private const val TAG = "MainActivity"
private const val LAST_SELECT_NAV_MENU_ID = "lastSelectedNavMenuId"
private val SEARCH_VISIBLE_DESTINATIONS: List<Int> = ImmutableList.of(
R.id.feedFragment,
R.id.profileFragment,
R.id.directMessagesInboxFragment,
R.id.discoverFragment,
R.id.favoritesFragment,
R.id.hashTagFragment,
R.id.locationFragment
)
@JvmStatic
var instance: MainActivity? = null
private set
}
}

View File

@ -0,0 +1,112 @@
package awais.instagrabber.adapters;
import android.annotation.SuppressLint;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.databinding.PrefAccountSwitcherBinding;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.utils.Constants;
import static awais.instagrabber.utils.Utils.settingsHelper;
public class AccountSwitcherAdapter extends ListAdapter<Account, AccountSwitcherAdapter.ViewHolder> {
private static final String TAG = "AccountSwitcherAdapter";
private static final DiffUtil.ItemCallback<Account> DIFF_CALLBACK = new DiffUtil.ItemCallback<Account>() {
@Override
public boolean areItemsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
@Override
public boolean areContentsTheSame(@NonNull final Account oldItem, @NonNull final Account newItem) {
return oldItem.getUid().equals(newItem.getUid());
}
};
private final OnAccountClickListener clickListener;
private final OnAccountLongClickListener longClickListener;
public AccountSwitcherAdapter(final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
super(DIFF_CALLBACK);
this.clickListener = clickListener;
this.longClickListener = longClickListener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final PrefAccountSwitcherBinding binding = PrefAccountSwitcherBinding.inflate(layoutInflater, parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final Account model = getItem(position);
if (model == null) return;
final String cookie = settingsHelper.getString(Constants.COOKIE);
final boolean isCurrent = model.getCookie().equals(cookie);
holder.bind(model, isCurrent, clickListener, longClickListener);
}
public interface OnAccountClickListener {
void onAccountClick(final Account model, final boolean isCurrent);
}
public interface OnAccountLongClickListener {
boolean onAccountLongClick(final Account model, final boolean isCurrent);
}
public static class ViewHolder extends RecyclerView.ViewHolder {
private final PrefAccountSwitcherBinding binding;
public ViewHolder(final PrefAccountSwitcherBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.arrowDown.setImageResource(R.drawable.ic_check_24);
}
@SuppressLint("SetTextI18n")
public void bind(final Account model,
final boolean isCurrent,
final OnAccountClickListener clickListener,
final OnAccountLongClickListener longClickListener) {
// Log.d(TAG, model.getFullName());
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onAccountClick(model, isCurrent);
});
itemView.setOnLongClickListener(v -> {
if (longClickListener == null) return false;
return longClickListener.onAccountLongClick(model, isCurrent);
});
binding.profilePic.setImageURI(model.getProfilePic());
binding.username.setText("@" + model.getUsername());
binding.fullName.setTypeface(null);
final String fullName = model.getFullName();
if (TextUtils.isEmpty(fullName)) {
binding.fullName.setVisibility(View.GONE);
} else {
binding.fullName.setVisibility(View.VISIBLE);
binding.fullName.setText(fullName);
}
if (!isCurrent) {
binding.arrowDown.setVisibility(View.GONE);
return;
}
binding.fullName.setTypeface(binding.fullName.getTypeface(), Typeface.BOLD);
binding.arrowDown.setVisibility(View.VISIBLE);
}
}
}

View File

@ -0,0 +1,77 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.CommentViewHolder;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.Comment;
public final class CommentsAdapter extends ListAdapter<Comment, CommentViewHolder> {
private static final DiffUtil.ItemCallback<Comment> DIFF_CALLBACK = new DiffUtil.ItemCallback<Comment>() {
@Override
public boolean areItemsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Comment oldItem, @NonNull final Comment newItem) {
return Objects.equals(oldItem, newItem);
}
};
private final boolean showingReplies;
private final CommentCallback commentCallback;
private final long currentUserId;
public CommentsAdapter(final long currentUserId,
final boolean showingReplies,
final CommentCallback commentCallback) {
super(DIFF_CALLBACK);
this.showingReplies = showingReplies;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
}
@NonNull
@Override
public CommentViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemCommentBinding binding = ItemCommentBinding.inflate(layoutInflater, parent, false);
return new CommentViewHolder(binding, currentUserId, commentCallback);
}
@Override
public void onBindViewHolder(@NonNull final CommentViewHolder holder, final int position) {
final Comment comment = getItem(position);
holder.bind(comment, showingReplies && position == 0, showingReplies && position != 0);
}
public interface CommentCallback {
void onClick(final Comment comment);
void onHashtagClick(final String hashtag);
void onMentionClick(final String mention);
void onURLClick(final String url);
void onEmailClick(final String emailAddress);
void onLikeClick(final Comment comment, boolean liked, final boolean isReply);
void onRepliesClick(final Comment comment);
void onViewLikes(Comment comment);
void onTranslate(Comment comment);
void onDelete(Comment comment, boolean isReply);
}
}

View File

@ -0,0 +1,424 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemActionLogViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemAnimatedMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemDefaultViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemLikeViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemLinkViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemMediaShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemPlaceholderViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemProfileViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemRavenMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemReelShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemStoryShareViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemTextViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVideoCallEventViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemVoiceMediaViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.DirectItemXmaViewHolder;
import awais.instagrabber.customviews.emoji.Emoji;
import awais.instagrabber.databinding.LayoutDmActionLogBinding;
import awais.instagrabber.databinding.LayoutDmAnimatedMediaBinding;
import awais.instagrabber.databinding.LayoutDmBaseBinding;
import awais.instagrabber.databinding.LayoutDmHeaderBinding;
import awais.instagrabber.databinding.LayoutDmLikeBinding;
import awais.instagrabber.databinding.LayoutDmLinkBinding;
import awais.instagrabber.databinding.LayoutDmMediaBinding;
import awais.instagrabber.databinding.LayoutDmMediaShareBinding;
import awais.instagrabber.databinding.LayoutDmProfileBinding;
import awais.instagrabber.databinding.LayoutDmRavenMediaBinding;
import awais.instagrabber.databinding.LayoutDmReelShareBinding;
import awais.instagrabber.databinding.LayoutDmStoryShareBinding;
import awais.instagrabber.databinding.LayoutDmTextBinding;
import awais.instagrabber.databinding.LayoutDmVoiceMediaBinding;
import awais.instagrabber.models.enums.DirectItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public final class DirectItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = DirectItemsAdapter.class.getSimpleName();
private List<DirectItem> items;
private DirectThread thread;
private DirectItemViewHolder selectedViewHolder;
private final User currentUser;
private final DirectItemCallback callback;
private final AsyncListDiffer<DirectItemOrHeader> differ;
private final DirectItemInternalLongClickListener longClickListener;
private static final DiffUtil.ItemCallback<DirectItemOrHeader> diffCallback = new DiffUtil.ItemCallback<DirectItemOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final DirectItemOrHeader oldItem, @NonNull final DirectItemOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.date.equals(newItem.date);
}
if (oldItem.item != null && newItem.item != null) {
String oldClientContext = oldItem.item.getClientContext();
if (oldClientContext == null) {
oldClientContext = oldItem.item.getItemId();
}
String newClientContext = newItem.item.getClientContext();
if (newClientContext == null) {
newClientContext = newItem.item.getItemId();
}
return oldClientContext.equals(newClientContext);
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final DirectItemOrHeader oldItem, @NonNull final DirectItemOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.date.equals(newItem.date);
}
final boolean timestampEqual = oldItem.item.getTimestamp() == newItem.item.getTimestamp();
final boolean bothPending = oldItem.item.isPending() == newItem.item.isPending();
final boolean reactionSame = Objects.equals(oldItem.item.getReactions(), newItem.item.getReactions());
return timestampEqual && bothPending && reactionSame;
}
};
public DirectItemsAdapter(@NonNull final User currentUser,
@NonNull final DirectThread thread,
@NonNull final DirectItemCallback callback,
@NonNull final DirectItemLongClickListener itemLongClickListener) {
this.currentUser = currentUser;
this.thread = thread;
this.callback = callback;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
longClickListener = (position, viewHolder) -> {
if (selectedViewHolder != null) {
selectedViewHolder.setSelected(false);
}
selectedViewHolder = viewHolder;
viewHolder.setSelected(true);
itemLongClickListener.onLongClick(position);
};
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (type == -1) {
// header
return new HeaderViewHolder(LayoutDmHeaderBinding.inflate(layoutInflater, parent, false));
}
final LayoutDmBaseBinding baseBinding = LayoutDmBaseBinding.inflate(layoutInflater, parent, false);
final DirectItemType directItemType = DirectItemType.Companion.getTypeFromId(type);
final DirectItemViewHolder itemViewHolder = getItemViewHolder(layoutInflater, baseBinding, directItemType);
itemViewHolder.setLongClickListener(longClickListener);
return itemViewHolder;
}
@NonNull
private DirectItemViewHolder getItemViewHolder(final LayoutInflater layoutInflater,
final LayoutDmBaseBinding baseBinding,
@NonNull final DirectItemType directItemType) {
switch (directItemType) {
case TEXT: {
final LayoutDmTextBinding binding = LayoutDmTextBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemTextViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LIKE: {
final LayoutDmLikeBinding binding = LayoutDmLikeBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemLikeViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LINK: {
final LayoutDmLinkBinding binding = LayoutDmLinkBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemLinkViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case ACTION_LOG: {
final LayoutDmActionLogBinding binding = LayoutDmActionLogBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemActionLogViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case VIDEO_CALL_EVENT: {
final LayoutDmActionLogBinding binding = LayoutDmActionLogBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemVideoCallEventViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case PLACEHOLDER: {
final LayoutDmStoryShareBinding binding = LayoutDmStoryShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemPlaceholderViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case ANIMATED_MEDIA: {
final LayoutDmAnimatedMediaBinding binding = LayoutDmAnimatedMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemAnimatedMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case VOICE_MEDIA: {
final LayoutDmVoiceMediaBinding binding = LayoutDmVoiceMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemVoiceMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case LOCATION:
case PROFILE: {
final LayoutDmProfileBinding binding = LayoutDmProfileBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemProfileViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case MEDIA: {
final LayoutDmMediaBinding binding = LayoutDmMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case CLIP:
case FELIX_SHARE:
case MEDIA_SHARE: {
final LayoutDmMediaShareBinding binding = LayoutDmMediaShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemMediaShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case STORY_SHARE: {
final LayoutDmStoryShareBinding binding = LayoutDmStoryShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemStoryShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case REEL_SHARE: {
final LayoutDmReelShareBinding binding = LayoutDmReelShareBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemReelShareViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case RAVEN_MEDIA: {
final LayoutDmRavenMediaBinding binding = LayoutDmRavenMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemRavenMediaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case XMA: {
final LayoutDmAnimatedMediaBinding binding = LayoutDmAnimatedMediaBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemXmaViewHolder(baseBinding, binding, currentUser, thread, callback);
}
case UNKNOWN:
default: {
final LayoutDmTextBinding binding = LayoutDmTextBinding.inflate(layoutInflater, baseBinding.message, false);
return new DirectItemDefaultViewHolder(baseBinding, binding, currentUser, thread, callback);
}
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
((HeaderViewHolder) holder).bind(itemOrHeader.date);
return;
}
if (thread == null) return;
((DirectItemViewHolder) holder).bind(position, itemOrHeader.item);
}
protected DirectItemOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
return -1;
}
final DirectItemType itemType = itemOrHeader.item.getItemType();
if (itemType == null) {
return 0;
}
return itemType.getId();
}
@Override
public long getItemId(final int position) {
final DirectItemOrHeader itemOrHeader = getItem(position);
if (itemOrHeader.isHeader()) {
return itemOrHeader.date.hashCode();
}
if (itemOrHeader.item.getClientContext() == null) {
return itemOrHeader.item.getItemId().hashCode();
}
return itemOrHeader.item.getClientContext().hashCode();
}
public void setThread(final DirectThread thread) {
if (thread == null) return;
this.thread = thread;
// notifyDataSetChanged();
}
public void submitList(@Nullable final List<DirectItem> list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
this.items = list;
}
public void submitList(@Nullable final List<DirectItem> list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
this.items = list;
}
private List<DirectItemOrHeader> sectionAndSort(final List<DirectItem> list) {
final List<DirectItemOrHeader> itemOrHeaders = new ArrayList<>();
LocalDate prevSectionDate = null;
for (int i = 0; i < list.size(); i++) {
final DirectItem item = list.get(i);
if (item == null || item.getDate() == null) continue;
final DirectItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
if (prev != null
&& prev.item != null
&& prev.item.getDate() != null
&& prev.item.getDate().toLocalDate().isEqual(item.getDate().toLocalDate())) {
// just add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
itemOrHeaders.add(itemOrHeader);
if (i == list.size() - 1) {
// add header
final DirectItemOrHeader itemOrHeader2 = new DirectItemOrHeader();
itemOrHeader2.date = prevSectionDate;
itemOrHeaders.add(itemOrHeader2);
}
continue;
}
if (prevSectionDate != null) {
// add header
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.date = prevSectionDate;
itemOrHeaders.add(itemOrHeader);
}
// Add item
final DirectItemOrHeader itemOrHeader = new DirectItemOrHeader();
itemOrHeader.item = item;
itemOrHeaders.add(itemOrHeader);
prevSectionDate = item.getDate().toLocalDate();
}
return itemOrHeaders;
}
public List<DirectItemOrHeader> getList() {
return differ.getCurrentList();
}
public List<DirectItem> getItems() {
return items;
}
@Override
public void onViewRecycled(@NonNull final RecyclerView.ViewHolder holder) {
if (holder instanceof DirectItemViewHolder) {
((DirectItemViewHolder) holder).cleanup();
}
}
@Override
public void onViewDetachedFromWindow(@NonNull final RecyclerView.ViewHolder holder) {
if (holder instanceof DirectItemViewHolder) {
((DirectItemViewHolder) holder).cleanup();
}
}
public DirectThread getThread() {
return thread;
}
public static class DirectItemOrHeader {
LocalDate date;
public DirectItem item;
public boolean isHeader() {
return date != null;
}
@NonNull
@Override
public String toString() {
return "DirectItemOrHeader{" +
"date=" + date +
", item=" + (item != null ? item.getItemType() : null) +
'}';
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final LayoutDmHeaderBinding binding;
public HeaderViewHolder(@NonNull final LayoutDmHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final LocalDate date) {
if (date == null) {
binding.header.setText("");
return;
}
final DateTimeFormatter dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT);
binding.header.setText(dateFormatter.format(date));
}
}
public interface DirectItemCallback {
void onHashtagClick(String hashtag);
void onMentionClick(String mention);
void onLocationClick(long locationId);
void onURLClick(String url);
void onEmailClick(String email);
void onMediaClick(Media media, int index);
void onStoryClick(DirectItemStoryShare storyShare);
void onReaction(DirectItem item, Emoji emoji);
void onReactionClick(DirectItem item, int position);
void onOptionSelect(DirectItem item, @IdRes int itemId, final Function<DirectItem, Void> callback);
void onAddReactionListener(DirectItem item);
}
public interface DirectItemInternalLongClickListener {
void onLongClick(int position, DirectItemViewHolder viewHolder);
}
public interface DirectItemLongClickListener {
void onLongClick(int position);
}
}

View File

@ -0,0 +1,75 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.directmessages.DirectInboxItemViewHolder;
import awais.instagrabber.databinding.LayoutDmInboxItemBinding;
import awais.instagrabber.repositories.responses.directmessages.DirectItem;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
public final class DirectMessageInboxAdapter extends ListAdapter<DirectThread, DirectInboxItemViewHolder> {
private final OnItemClickListener onClickListener;
private static final DiffUtil.ItemCallback<DirectThread> diffCallback = new DiffUtil.ItemCallback<DirectThread>() {
@Override
public boolean areItemsTheSame(@NonNull final DirectThread oldItem, @NonNull final DirectThread newItem) {
return oldItem.getThreadId().equals(newItem.getThreadId());
}
@Override
public boolean areContentsTheSame(@NonNull final DirectThread oldThread,
@NonNull final DirectThread newThread) {
final boolean titleEqual = oldThread.getThreadTitle().equals(newThread.getThreadTitle());
if (!titleEqual) return false;
final boolean lastSeenAtEqual = Objects.equals(oldThread.getLastSeenAt(), newThread.getLastSeenAt());
if (!lastSeenAtEqual) return false;
final List<DirectItem> oldItems = oldThread.getItems();
final List<DirectItem> newItems = newThread.getItems();
if (oldItems == null || newItems == null) return false;
if (oldItems.size() != newItems.size()) return false;
final DirectItem oldItemFirst = oldThread.getFirstDirectItem();
final DirectItem newItemFirst = newThread.getFirstDirectItem();
if (oldItemFirst == null || newItemFirst == null) return false;
final boolean idsEqual = oldItemFirst.getItemId().equals(newItemFirst.getItemId());
if (!idsEqual) return false;
return oldItemFirst.getTimestamp() == newItemFirst.getTimestamp();
}
};
public DirectMessageInboxAdapter(final OnItemClickListener onClickListener) {
super(new AsyncDifferConfig.Builder<>(diffCallback).build());
this.onClickListener = onClickListener;
}
@NonNull
@Override
public DirectInboxItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmInboxItemBinding binding = LayoutDmInboxItemBinding.inflate(layoutInflater, parent, false);
return new DirectInboxItemViewHolder(binding, onClickListener);
}
@Override
public void onBindViewHolder(@NonNull final DirectInboxItemViewHolder holder, final int position) {
final DirectThread thread = getItem(position);
holder.bind(thread);
}
@Override
public long getItemId(final int position) {
return getItem(position).getThreadId().hashCode();
}
public interface OnItemClickListener {
void onItemClick(final DirectThread thread);
}
}

View File

@ -0,0 +1,117 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.directmessages.DirectPendingUserViewHolder;
import awais.instagrabber.databinding.LayoutDmPendingUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse;
public final class DirectPendingUsersAdapter extends ListAdapter<DirectPendingUsersAdapter.PendingUser, DirectPendingUserViewHolder> {
private static final DiffUtil.ItemCallback<PendingUser> DIFF_CALLBACK = new DiffUtil.ItemCallback<PendingUser>() {
@Override
public boolean areItemsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) {
return oldItem.user.getPk() == newItem.user.getPk();
}
@Override
public boolean areContentsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) {
return Objects.equals(oldItem.user.getUsername(), newItem.user.getUsername()) &&
Objects.equals(oldItem.user.getFullName(), newItem.user.getFullName()) &&
Objects.equals(oldItem.requester, newItem.requester);
}
};
private final PendingUserCallback callback;
public DirectPendingUsersAdapter(final PendingUserCallback callback) {
super(DIFF_CALLBACK);
this.callback = callback;
setHasStableIds(true);
}
public void submitPendingRequests(final DirectThreadParticipantRequestsResponse requests) {
if (requests == null || requests.getUsers() == null) {
submitList(Collections.emptyList());
return;
}
submitList(parse(requests));
}
private List<PendingUser> parse(final DirectThreadParticipantRequestsResponse requests) {
final List<User> users = requests.getUsers();
final Map<Long, String> requesterUsernames = requests.getRequesterUsernames();
return users.stream()
.map(user -> new PendingUser(user, requesterUsernames.get(user.getPk())))
.collect(Collectors.toList());
}
@NonNull
@Override
public DirectPendingUserViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmPendingUserItemBinding binding = LayoutDmPendingUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectPendingUserViewHolder(binding, callback);
}
@Override
public void onBindViewHolder(@NonNull final DirectPendingUserViewHolder holder, final int position) {
final PendingUser pendingUser = getItem(position);
holder.bind(position, pendingUser);
}
@Override
public long getItemId(final int position) {
final PendingUser item = getItem(position);
return item.user.getPk();
}
public static class PendingUser {
private final User user;
private final String requester;
private boolean inProgress;
public PendingUser(final User user, final String requester) {
this.user = user;
this.requester = requester;
}
public User getUser() {
return user;
}
public String getRequester() {
return requester;
}
public boolean isInProgress() {
return inProgress;
}
public PendingUser setInProgress(final boolean inProgress) {
this.inProgress = inProgress;
return this;
}
}
public interface PendingUserCallback {
void onClick(int position, PendingUser pendingUser);
void onApprove(int position, PendingUser pendingUser);
void onDeny(int position, PendingUser pendingUser);
}
}

View File

@ -0,0 +1,81 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import awais.instagrabber.adapters.viewholder.directmessages.DirectReactionViewHolder;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction;
public final class DirectReactionsAdapter extends ListAdapter<DirectItemEmojiReaction, DirectReactionViewHolder> {
private static final DiffUtil.ItemCallback<DirectItemEmojiReaction> DIFF_CALLBACK = new DiffUtil.ItemCallback<DirectItemEmojiReaction>() {
@Override
public boolean areItemsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) {
return oldItem.getSenderId() == newItem.getSenderId();
}
@Override
public boolean areContentsTheSame(@NonNull final DirectItemEmojiReaction oldItem, @NonNull final DirectItemEmojiReaction newItem) {
return oldItem.getEmoji().equals(newItem.getEmoji());
}
};
private final long viewerId;
private final List<User> users;
private final String itemId;
private final OnReactionClickListener onReactionClickListener;
public DirectReactionsAdapter(final long viewerId,
final List<User> users,
final String itemId,
final OnReactionClickListener onReactionClickListener) {
super(DIFF_CALLBACK);
this.viewerId = viewerId;
this.users = users;
this.itemId = itemId;
this.onReactionClickListener = onReactionClickListener;
setHasStableIds(true);
}
@NonNull
@Override
public DirectReactionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectReactionViewHolder(binding, viewerId, itemId, onReactionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final DirectReactionViewHolder holder, final int position) {
final DirectItemEmojiReaction reaction = getItem(position);
if (reaction == null) return;
holder.bind(reaction, getUser(reaction.getSenderId()));
}
@Override
public long getItemId(final int position) {
return getItem(position).getSenderId();
}
@Nullable
private User getUser(final long pk) {
return users.stream()
.filter(user -> user.getPk() == pk)
.findFirst()
.orElse(null);
}
public interface OnReactionClickListener {
void onReactionClick(String itemId, DirectItemEmojiReaction reaction);
}
}

View File

@ -0,0 +1,183 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.User;
public final class DirectUsersAdapter extends ListAdapter<DirectUsersAdapter.DirectUserOrHeader, RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_USER = 1;
private static final DiffUtil.ItemCallback<DirectUserOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<DirectUserOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.headerTitle == newItem.headerTitle;
}
if (oldItem.user != null && newItem.user != null) {
return oldItem.user.getPk() == newItem.user.getPk();
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) {
final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader();
final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader();
boolean areSameType = bothHeaders || bothItems;
if (!areSameType) return false;
if (bothHeaders) {
return oldItem.headerTitle == newItem.headerTitle;
}
if (oldItem.user != null && newItem.user != null) {
return oldItem.user.getUsername().equals(newItem.user.getUsername()) &&
oldItem.user.getFullName().equals(newItem.user.getFullName());
}
return false;
}
};
private final long inviterId;
private final OnDirectUserClickListener onClickListener;
private final OnDirectUserLongClickListener onLongClickListener;
private List<Long> adminUserIds;
public DirectUsersAdapter(final long inviterId,
final OnDirectUserClickListener onClickListener,
final OnDirectUserLongClickListener onLongClickListener) {
super(DIFF_CALLBACK);
this.inviterId = inviterId;
this.onClickListener = onClickListener;
this.onLongClickListener = onLongClickListener;
setHasStableIds(true);
}
public void submitUsers(final List<User> users, final List<User> leftUsers) {
if (users == null && leftUsers == null) return;
final List<DirectUserOrHeader> userOrHeaders = combineLists(users, leftUsers);
submitList(userOrHeaders);
}
private List<DirectUserOrHeader> combineLists(final List<User> users, final List<User> leftUsers) {
final ImmutableList.Builder<DirectUserOrHeader> listBuilder = ImmutableList.builder();
if (users != null && !users.isEmpty()) {
listBuilder.add(new DirectUserOrHeader(R.string.members));
users.stream()
.map(DirectUserOrHeader::new)
.forEach(listBuilder::add);
}
if (leftUsers != null && !leftUsers.isEmpty()) {
listBuilder.add(new DirectUserOrHeader(R.string.dms_left_users));
leftUsers.stream()
.map(DirectUserOrHeader::new)
.forEach(listBuilder::add);
}
return listBuilder.build();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
switch (viewType) {
case VIEW_TYPE_USER:
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
return new DirectUserViewHolder(binding, onClickListener, onLongClickListener);
case VIEW_TYPE_HEADER:
default:
final ItemFavSectionHeaderBinding headerBinding = ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false);
return new HeaderViewHolder(headerBinding);
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof HeaderViewHolder) {
((HeaderViewHolder) holder).bind(getItem(position).headerTitle);
return;
}
if (holder instanceof DirectUserViewHolder) {
final User user = getItem(position).user;
((DirectUserViewHolder) holder).bind(position,
user,
user != null && adminUserIds != null && adminUserIds.contains(user.getPk()),
user != null && user.getPk() == inviterId,
false,
false);
}
}
@Override
public int getItemViewType(final int position) {
final DirectUserOrHeader item = getItem(position);
return item.isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_USER;
}
@Override
public long getItemId(final int position) {
final DirectUserOrHeader item = getItem(position);
return item.isHeader() ? item.headerTitle : item.user.getPk();
}
public void setAdminUserIds(final List<Long> adminUserIds) {
this.adminUserIds = adminUserIds;
notifyDataSetChanged();
}
public static class DirectUserOrHeader {
int headerTitle;
User user;
public DirectUserOrHeader(final int headerTitle) {
this.headerTitle = headerTitle;
}
public DirectUserOrHeader(final User user) {
this.user = user;
}
boolean isHeader() {
return headerTitle > 0;
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(@StringRes final int headerTitle) {
binding.getRoot().setText(headerTitle);
}
}
public interface OnDirectUserClickListener {
void onClick(int position, User user, boolean selected);
}
public interface OnDirectUserLongClickListener {
boolean onLongClick(int position, User user);
}
}

View File

@ -0,0 +1,75 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.io.File;
import awais.instagrabber.R;
import awais.instagrabber.databinding.ItemDirListBinding;
public final class DirectoryFilesAdapter extends ListAdapter<File, DirectoryFilesAdapter.ViewHolder> {
private final OnFileClickListener onFileClickListener;
private static final DiffUtil.ItemCallback<File> DIFF_CALLBACK = new DiffUtil.ItemCallback<File>() {
@Override
public boolean areItemsTheSame(@NonNull final File oldItem, @NonNull final File newItem) {
return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath());
}
@Override
public boolean areContentsTheSame(@NonNull final File oldItem, @NonNull final File newItem) {
return oldItem.getAbsolutePath().equals(newItem.getAbsolutePath());
}
};
public DirectoryFilesAdapter(final OnFileClickListener onFileClickListener) {
super(DIFF_CALLBACK);
this.onFileClickListener = onFileClickListener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final ItemDirListBinding binding = ItemDirListBinding.inflate(inflater, parent, false);
return new ViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) {
final File file = getItem(position);
holder.bind(file, onFileClickListener);
}
public interface OnFileClickListener {
void onFileClick(File file);
}
static final class ViewHolder extends RecyclerView.ViewHolder {
private final ItemDirListBinding binding;
private ViewHolder(final ItemDirListBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final File file, final OnFileClickListener onFileClickListener) {
if (file == null) return;
if (onFileClickListener != null) {
itemView.setOnClickListener(v -> onFileClickListener.onFileClick(file));
}
binding.text.setText(file.getName());
if (file.isDirectory()) {
binding.icon.setImageResource(R.drawable.ic_folder_24);
return;
}
binding.icon.setImageResource(R.drawable.ic_file_24);
}
}
}

View File

@ -0,0 +1,58 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.discover.TopicCluster;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.utils.ResponseBodyUtils;
public class DiscoverTopicsAdapter extends ListAdapter<TopicCluster, TopicClusterViewHolder> {
private static final DiffUtil.ItemCallback<TopicCluster> DIFF_CALLBACK = new DiffUtil.ItemCallback<TopicCluster>() {
@Override
public boolean areItemsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final TopicCluster oldItem, @NonNull final TopicCluster newItem) {
final String oldThumbUrl = ResponseBodyUtils.getThumbUrl(oldItem.getCoverMedia());
return oldThumbUrl != null && oldThumbUrl.equals(ResponseBodyUtils.getThumbUrl(newItem.getCoverMedia()))
&& oldItem.getTitle().equals(newItem.getTitle());
}
};
private final OnTopicClickListener onTopicClickListener;
public DiscoverTopicsAdapter(final OnTopicClickListener onTopicClickListener) {
super(DIFF_CALLBACK);
this.onTopicClickListener = onTopicClickListener;
}
@NonNull
@Override
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, onTopicClickListener, null);
}
@Override
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) {
final TopicCluster topicCluster = getItem(position);
holder.bind(topicCluster);
}
public interface OnTopicClickListener {
void onTopicClick(TopicCluster topicCluster, View cover, int titleColor, int backgroundColor);
void onTopicLongClick(Media coverMedia);
}
}

View File

@ -0,0 +1,202 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.ObjectsCompat;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FavoriteViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
public class FavoritesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final OnFavoriteClickListener clickListener;
private final OnFavoriteLongClickListener longClickListener;
private final AsyncListDiffer<FavoriteModelOrHeader> differ;
private static final DiffUtil.ItemCallback<FavoriteModelOrHeader> diffCallback = new DiffUtil.ItemCallback<FavoriteModelOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) {
boolean areSame = oldItem.isHeader() && newItem.isHeader();
if (!areSame) {
return false;
}
if (oldItem.isHeader()) {
return ObjectsCompat.equals(oldItem.header, newItem.header);
}
if (oldItem.model != null && newItem.model != null) {
return oldItem.model.getId() == newItem.model.getId();
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final FavoriteModelOrHeader oldItem, @NonNull final FavoriteModelOrHeader newItem) {
boolean areSame = oldItem.isHeader() && newItem.isHeader();
if (!areSame) {
return false;
}
if (oldItem.isHeader()) {
return ObjectsCompat.equals(oldItem.header, newItem.header);
}
return ObjectsCompat.equals(oldItem.model, newItem.model);
}
};
public FavoritesAdapter(final OnFavoriteClickListener clickListener, final OnFavoriteLongClickListener longClickListener) {
this.clickListener = clickListener;
this.longClickListener = longClickListener;
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(diffCallback).build());
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
if (viewType == 0) {
// header
return new FavSectionViewHolder(ItemFavSectionHeaderBinding.inflate(inflater, parent, false));
}
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(inflater, parent, false);
return new FavoriteViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (getItemViewType(position) == 0) {
final FavoriteModelOrHeader modelOrHeader = getItem(position);
if (!modelOrHeader.isHeader()) return;
((FavSectionViewHolder) holder).bind(modelOrHeader.header);
return;
}
((FavoriteViewHolder) holder).bind(getItem(position).model, clickListener, longClickListener);
}
protected FavoriteModelOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? 0 : 1;
}
public void submitList(@Nullable final List<Favorite> list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
}
public void submitList(@Nullable final List<Favorite> list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
}
@NonNull
private List<FavoriteModelOrHeader> sectionAndSort(@NonNull final List<Favorite> list) {
final List<Favorite> listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
if (o1.getType() == o2.getType()) return 0;
// keep users at top
if (o1.getType() == FavoriteType.USER) return -1;
if (o2.getType() == FavoriteType.USER) return 1;
// keep locations at bottom
if (o1.getType() == FavoriteType.LOCATION) return 1;
if (o2.getType() == FavoriteType.LOCATION) return -1;
return 0;
});
final List<FavoriteModelOrHeader> modelOrHeaders = new ArrayList<>();
for (int i = 0; i < listCopy.size(); i++) {
final Favorite model = listCopy.get(i);
final FavoriteModelOrHeader prev = modelOrHeaders.isEmpty() ? null : modelOrHeaders.get(modelOrHeaders.size() - 1);
boolean prevWasSameType = prev != null && prev.model.getType() == model.getType();
if (prevWasSameType) {
// just add model
final FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.model = model;
modelOrHeaders.add(modelOrHeader);
continue;
}
// add header and model
FavoriteModelOrHeader modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.header = model.getType();
modelOrHeaders.add(modelOrHeader);
modelOrHeader = new FavoriteModelOrHeader();
modelOrHeader.model = model;
modelOrHeaders.add(modelOrHeader);
}
return modelOrHeaders;
}
private static class FavoriteModelOrHeader {
FavoriteType header;
Favorite model;
boolean isHeader() {
return header != null;
}
}
public interface OnFavoriteClickListener {
void onClick(final Favorite model);
}
public interface OnFavoriteLongClickListener {
boolean onLongClick(final Favorite model);
}
public static class FavSectionViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public FavSectionViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final FavoriteType header) {
if (header == null) return;
final int headerText;
switch (header) {
case USER:
headerText = R.string.accounts;
break;
case HASHTAG:
headerText = R.string.hashtags;
break;
case LOCATION:
headerText = R.string.locations;
break;
default:
headerText = R.string.unknown;
break;
}
binding.getRoot().setText(headerText);
}
}
}

View File

@ -0,0 +1,247 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import awais.instagrabber.adapters.viewholder.FeedGridItemViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedItemViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedSliderViewHolder;
import awais.instagrabber.adapters.viewholder.feed.FeedVideoViewHolder;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.databinding.ItemFeedPhotoBinding;
import awais.instagrabber.databinding.ItemFeedSliderBinding;
import awais.instagrabber.databinding.ItemFeedVideoBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Caption;
import awais.instagrabber.repositories.responses.Media;
public final class FeedAdapterV2 extends ListAdapter<Media, RecyclerView.ViewHolder> {
private static final String TAG = "FeedAdapterV2";
private final FeedItemCallback feedItemCallback;
private final SelectionModeCallback selectionModeCallback;
private final Set<Integer> selectedPositions = new HashSet<>();
private final Set<Media> selectedFeedModels = new HashSet<>();
private PostsLayoutPreferences layoutPreferences;
private boolean selectionModeActive = false;
private static final DiffUtil.ItemCallback<Media> DIFF_CALLBACK = new DiffUtil.ItemCallback<Media>() {
@Override
public boolean areItemsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return Objects.equals(oldItem.getPk(), newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
final Caption oldItemCaption = oldItem.getCaption();
final Caption newItemCaption = newItem.getCaption();
return Objects.equals(oldItem.getPk(), newItem.getPk())
&& Objects.equals(getCaptionText(oldItemCaption), getCaptionText(newItemCaption));
}
private String getCaptionText(final Caption caption) {
if (caption == null) return null;
return caption.getText();
}
};
private final AdapterSelectionCallback adapterSelectionCallback = new AdapterSelectionCallback() {
@Override
public boolean onPostLongClick(final int position, final Media feedModel) {
if (!selectionModeActive) {
selectionModeActive = true;
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionStart();
}
}
selectedPositions.add(position);
selectedFeedModels.add(feedModel);
notifyItemChanged(position);
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionChange(selectedFeedModels);
}
return true;
}
@Override
public void onPostClick(final int position, final Media feedModel) {
if (!selectionModeActive) return;
if (selectedPositions.contains(position)) {
selectedPositions.remove(position);
selectedFeedModels.remove(feedModel);
} else {
selectedPositions.add(position);
selectedFeedModels.add(feedModel);
}
notifyItemChanged(position);
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionChange(selectedFeedModels);
}
if (selectedPositions.isEmpty()) {
selectionModeActive = false;
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionEnd();
}
}
}
};
public FeedAdapterV2(@NonNull final PostsLayoutPreferences layoutPreferences,
final FeedItemCallback feedItemCallback,
final SelectionModeCallback selectionModeCallback) {
super(DIFF_CALLBACK);
this.layoutPreferences = layoutPreferences;
this.feedItemCallback = feedItemCallback;
this.selectionModeCallback = selectionModeCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final Context context = parent.getContext();
final LayoutInflater layoutInflater = LayoutInflater.from(context);
switch (layoutPreferences.getType()) {
case LINEAR:
return getLinearViewHolder(parent, layoutInflater, viewType);
case GRID:
case STAGGERED_GRID:
default:
final ItemFeedGridBinding binding = ItemFeedGridBinding.inflate(layoutInflater, parent, false);
return new FeedGridItemViewHolder(binding);
}
}
@NonNull
private RecyclerView.ViewHolder getLinearViewHolder(@NonNull final ViewGroup parent,
final LayoutInflater layoutInflater,
final int viewType) {
switch (MediaItemType.valueOf(viewType)) {
case MEDIA_TYPE_VIDEO: {
final ItemFeedVideoBinding binding = ItemFeedVideoBinding.inflate(layoutInflater, parent, false);
return new FeedVideoViewHolder(binding, feedItemCallback);
}
case MEDIA_TYPE_SLIDER: {
final ItemFeedSliderBinding binding = ItemFeedSliderBinding.inflate(layoutInflater, parent, false);
return new FeedSliderViewHolder(binding, feedItemCallback);
}
case MEDIA_TYPE_IMAGE:
default: {
final ItemFeedPhotoBinding binding = ItemFeedPhotoBinding.inflate(layoutInflater, parent, false);
return new FeedPhotoViewHolder(binding, feedItemCallback);
}
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int position) {
final Media feedModel = getItem(position);
if (feedModel == null) return;
switch (layoutPreferences.getType()) {
case LINEAR:
((FeedItemViewHolder) viewHolder).bind(feedModel);
break;
case GRID:
case STAGGERED_GRID:
default:
((FeedGridItemViewHolder) viewHolder).bind(position,
feedModel,
layoutPreferences,
feedItemCallback,
adapterSelectionCallback,
selectionModeActive,
selectedPositions.contains(position));
}
}
@Override
public int getItemViewType(final int position) {
return getItem(position).getType().getId();
}
public void setLayoutPreferences(@NonNull final PostsLayoutPreferences layoutPreferences) {
this.layoutPreferences = layoutPreferences;
}
public void endSelection() {
if (!selectionModeActive) return;
selectionModeActive = false;
selectedPositions.clear();
selectedFeedModels.clear();
notifyDataSetChanged();
if (selectionModeCallback != null) {
selectionModeCallback.onSelectionEnd();
}
}
// @Override
// public void onViewAttachedToWindow(@NonNull final FeedItemViewHolder holder) {
// super.onViewAttachedToWindow(holder);
// // Log.d(TAG, "attached holder: " + holder);
// if (!(holder instanceof FeedSliderViewHolder)) return;
// final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder;
// feedSliderViewHolder.startPlayingVideo();
// }
//
// @Override
// public void onViewDetachedFromWindow(@NonNull final FeedItemViewHolder holder) {
// super.onViewDetachedFromWindow(holder);
// // Log.d(TAG, "detached holder: " + holder);
// if (!(holder instanceof FeedSliderViewHolder)) return;
// final FeedSliderViewHolder feedSliderViewHolder = (FeedSliderViewHolder) holder;
// feedSliderViewHolder.stopPlayingVideo();
// }
public interface FeedItemCallback {
void onPostClick(final Media feedModel);
void onProfilePicClick(final Media feedModel);
void onNameClick(final Media feedModel);
void onLocationClick(final Media feedModel);
void onMentionClick(final String mention);
void onHashtagClick(final String hashtag);
void onCommentsClick(final Media feedModel);
void onDownloadClick(final Media feedModel, final int childPosition, final View popupLocation);
void onEmailClick(final String emailId);
void onURLClick(final String url);
void onSliderClick(Media feedModel, int position);
}
public interface AdapterSelectionCallback {
boolean onPostLongClick(final int position, Media feedModel);
void onPostClick(final int position, Media feedModel);
}
public interface SelectionModeCallback {
void onSelectionStart();
void onSelectionChange(final Set<Media> selectedFeedModels);
void onSelectionEnd();
}
}

View File

@ -0,0 +1,41 @@
package awais.instagrabber.adapters;
import android.view.View;
import awais.instagrabber.repositories.responses.Media;
public class FeedItemCallbackAdapter implements FeedAdapterV2.FeedItemCallback {
@Override
public void onPostClick(final Media media) {}
@Override
public void onProfilePicClick(final Media media) {}
@Override
public void onNameClick(final Media media) {}
@Override
public void onLocationClick(final Media media) {}
@Override
public void onMentionClick(final String mention) {}
@Override
public void onHashtagClick(final String hashtag) {}
@Override
public void onCommentsClick(final Media media) {}
@Override
public void onDownloadClick(final Media media, final int childPosition, final View popupLocation) {}
@Override
public void onEmailClick(final String emailId) {}
@Override
public void onURLClick(final String url) {}
@Override
public void onSliderClick(final Media media, final int position) {}
}

View File

@ -0,0 +1,53 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.FeedStoryViewHolder;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class FeedStoriesAdapter extends ListAdapter<Story, FeedStoryViewHolder> {
private final OnFeedStoryClickListener listener;
private static final DiffUtil.ItemCallback<Story> diffCallback = new DiffUtil.ItemCallback<Story>() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId()) && oldItem.getSeen() == newItem.getSeen();
}
};
public FeedStoriesAdapter(final OnFeedStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@NonNull
@Override
public FeedStoryViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemHighlightBinding binding = ItemHighlightBinding.inflate(layoutInflater, parent, false);
return new FeedStoryViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final FeedStoryViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, position, listener);
}
public interface OnFeedStoryClickListener {
void onFeedStoryClick(Story model, int position);
void onFeedStoryLongClick(Story model, int position);
}
}

View File

@ -0,0 +1,105 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.StoryListViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.repositories.responses.stories.Story;
import awais.instagrabber.utils.TextUtils;
public final class FeedStoriesListAdapter extends ListAdapter<Story, StoryListViewHolder> implements Filterable {
private final OnFeedStoryClickListener listener;
private List<Story> list;
private final Filter filter = new Filter() {
@NonNull
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final String query = TextUtils.isEmpty(filter) ? null : filter.toString().toLowerCase();
List<Story> filteredList = list;
if (list != null && query != null) {
filteredList = list.stream()
.filter(feedStoryModel -> feedStoryModel.getUser()
.getUsername()
.toLowerCase()
.contains(query))
.collect(Collectors.toList());
}
final FilterResults filterResults = new FilterResults();
filterResults.count = filteredList != null ? filteredList.size() : 0;
filterResults.values = filteredList;
return filterResults;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
//noinspection unchecked
submitList((List<Story>) results.values, true);
}
};
private static final DiffUtil.ItemCallback<Story> diffCallback = new DiffUtil.ItemCallback<Story>() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId()) && oldItem.getSeen() == newItem.getSeen();
}
};
public FeedStoriesListAdapter(final OnFeedStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@Override
public Filter getFilter() {
return filter;
}
private void submitList(@Nullable final List<Story> list, final boolean isFiltered) {
if (!isFiltered) {
this.list = list;
}
super.submitList(list);
}
@Override
public void submitList(final List<Story> list) {
submitList(list, false);
}
@NonNull
@Override
public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new StoryListViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, listener);
}
public interface OnFeedStoryClickListener {
void onFeedStoryClick(final Story model);
void onProfileClick(final String username);
}
}

View File

@ -0,0 +1,94 @@
package awais.instagrabber.adapters;
import android.graphics.Bitmap;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Collection;
import java.util.List;
import awais.instagrabber.adapters.viewholder.FilterViewHolder;
import awais.instagrabber.databinding.ItemFilterBinding;
import awais.instagrabber.fragments.imageedit.filters.filters.Filter;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
public class FiltersAdapter extends ListAdapter<Filter<?>, FilterViewHolder> {
private static final DiffUtil.ItemCallback<Filter<?>> DIFF_CALLBACK = new DiffUtil.ItemCallback<Filter<?>>() {
@Override
public boolean areItemsTheSame(@NonNull final Filter<?> oldItem, @NonNull final Filter<?> newItem) {
return oldItem.getType().equals(newItem.getType());
}
@Override
public boolean areContentsTheSame(@NonNull final Filter<?> oldItem, @NonNull final Filter<?> newItem) {
return oldItem.getType().equals(newItem.getType());
}
};
private final Bitmap bitmap;
private final OnFilterClickListener onFilterClickListener;
private final Collection<GPUImageFilter> filters;
private final String originalKey;
private int selectedPosition = 0;
public FiltersAdapter(final Collection<GPUImageFilter> filters,
final String originalKey,
final Bitmap bitmap,
final OnFilterClickListener onFilterClickListener) {
super(DIFF_CALLBACK);
this.filters = filters;
this.originalKey = originalKey;
this.bitmap = bitmap;
this.onFilterClickListener = onFilterClickListener;
setHasStableIds(true);
}
@NonNull
@Override
public FilterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemFilterBinding binding = ItemFilterBinding.inflate(layoutInflater, parent, false);
return new FilterViewHolder(binding, filters, onFilterClickListener);
}
@Override
public void onBindViewHolder(@NonNull final FilterViewHolder holder, final int position) {
holder.bind(position, originalKey, bitmap, getItem(position), selectedPosition == position);
}
@Override
public long getItemId(final int position) {
return getItem(position).getLabel();
}
public void setSelected(final int position) {
final int prev = this.selectedPosition;
this.selectedPosition = position;
notifyItemChanged(position);
notifyItemChanged(prev);
}
public void setSelectedFilter(final GPUImageFilter instance) {
final List<Filter<?>> currentList = getCurrentList();
int index = -1;
for (int i = 0; i < currentList.size(); i++) {
final Filter<?> filter = currentList.get(i);
final GPUImageFilter filterInstance = filter.getInstance();
if (filterInstance.getClass() == instance.getClass()) {
index = i;
break;
}
}
if (index < 0) return;
setSelected(index);
}
public interface OnFilterClickListener {
void onClick(int position, Filter<?> filter);
}
}

View File

@ -0,0 +1,152 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.interfaces.OnGroupClickListener;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.TextUtils;
import thoughtbot.expandableadapter.ExpandableGroup;
import thoughtbot.expandableadapter.ExpandableList;
import thoughtbot.expandableadapter.ExpandableListPosition;
import thoughtbot.expandableadapter.GroupViewHolder;
// thanks to ThoughtBot's ExpandableRecyclerViewAdapter
// https://github.com/thoughtbot/expandable-recycler-view
public final class FollowAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements OnGroupClickListener, Filterable {
private final View.OnClickListener onClickListener;
private final ExpandableList expandableListOriginal;
private final boolean hasManyGroups;
private ExpandableList expandableList;
private final Filter filter = new Filter() {
@Nullable
@Override
protected FilterResults performFiltering(final CharSequence filter) {
final List<User> filteredItems = new ArrayList<User>();
if (expandableListOriginal.groups == null || TextUtils.isEmpty(filter)) return null;
final String query = filter.toString().toLowerCase();
final ArrayList<ExpandableGroup> groups = new ArrayList<ExpandableGroup>();
for (int x = 0; x < expandableListOriginal.groups.size(); ++x) {
final ExpandableGroup expandableGroup = expandableListOriginal.groups.get(x);
final String title = expandableGroup.getTitle();
final List<User> items = expandableGroup.getItems();
if (items != null) {
final List<User> toReturn = items.stream()
.filter(u -> hasKey(query, u.getUsername(), u.getFullName()))
.collect(Collectors.toList());
groups.add(new ExpandableGroup(title, toReturn));
}
}
final FilterResults filterResults = new FilterResults();
filterResults.values = new ExpandableList(groups, expandableList.expandedGroupIndexes);
return filterResults;
}
private boolean hasKey(final String key, final String username, final String name) {
if (TextUtils.isEmpty(key)) return true;
final boolean hasUserName = username != null && username.toLowerCase().contains(key);
if (!hasUserName && name != null) return name.toLowerCase().contains(key);
return true;
}
@Override
protected void publishResults(final CharSequence constraint, final FilterResults results) {
if (results == null) {
expandableList = expandableListOriginal;
}
else {
final ExpandableList filteredList = (ExpandableList) results.values;
expandableList = filteredList;
}
notifyDataSetChanged();
}
};
public FollowAdapter(final View.OnClickListener onClickListener, @NonNull final ArrayList<ExpandableGroup> groups) {
this.expandableListOriginal = new ExpandableList(groups);
expandableList = this.expandableListOriginal;
this.onClickListener = onClickListener;
this.hasManyGroups = groups.size() > 1;
}
@Override
public Filter getFilter() {
return filter;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final boolean isGroup = hasManyGroups && viewType == ExpandableListPosition.GROUP;
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View view;
if (isGroup) {
view = layoutInflater.inflate(R.layout.header_follow, parent, false);
return new GroupViewHolder(view, this);
} else {
final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false);
return new FollowsViewHolder(binding);
}
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final ExpandableListPosition listPos = expandableList.getUnflattenedPosition(position);
final ExpandableGroup group = expandableList.getExpandableGroup(listPos);
if (hasManyGroups && listPos.type == ExpandableListPosition.GROUP) {
final GroupViewHolder gvh = (GroupViewHolder) holder;
gvh.setTitle(group.getTitle());
gvh.toggle(isGroupExpanded(group));
return;
}
final User model = group.getItems().get(hasManyGroups ? listPos.childPos : position);
((FollowsViewHolder) holder).bind(model, onClickListener);
}
@Override
public int getItemCount() {
return expandableList.getVisibleItemCount() - (hasManyGroups ? 0 : 1);
}
@Override
public int getItemViewType(final int position) {
return !hasManyGroups ? 0 : expandableList.getUnflattenedPosition(position).type;
}
@Override
public void toggleGroup(final int flatPos) {
final ExpandableListPosition listPosition = expandableList.getUnflattenedPosition(flatPos);
final int groupPos = listPosition.groupPos;
final int positionStart = expandableList.getFlattenedGroupIndex(listPosition) + 1;
final int positionEnd = expandableList.groups.get(groupPos).getItemCount();
final boolean isExpanded = expandableList.expandedGroupIndexes[groupPos];
expandableList.expandedGroupIndexes[groupPos] = !isExpanded;
notifyItemChanged(positionStart - 1);
if (positionEnd > 0) {
if (isExpanded) notifyItemRangeRemoved(positionStart, positionEnd);
else notifyItemRangeInserted(positionStart, positionEnd);
}
}
public boolean isGroupExpanded(final ExpandableGroup group) {
return expandableList.expandedGroupIndexes[expandableList.groups.indexOf(group)];
}
}

View File

@ -0,0 +1,107 @@
package awais.instagrabber.adapters;
import android.net.Uri;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.Objects;
import awais.instagrabber.databinding.ItemMediaBinding;
import awais.instagrabber.repositories.responses.giphy.GiphyGif;
import awais.instagrabber.utils.Utils;
public class GifItemsAdapter extends ListAdapter<GiphyGif, GifItemsAdapter.GifViewHolder> {
private static final DiffUtil.ItemCallback<GiphyGif> diffCallback = new DiffUtil.ItemCallback<GiphyGif>() {
@Override
public boolean areItemsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) {
return Objects.equals(oldItem.getId(), newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) {
return Objects.equals(oldItem.getId(), newItem.getId());
}
};
private final OnItemClickListener onItemClickListener;
public GifItemsAdapter(final OnItemClickListener onItemClickListener) {
super(diffCallback);
this.onItemClickListener = onItemClickListener;
}
@NonNull
@Override
public GifViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemMediaBinding binding = ItemMediaBinding.inflate(layoutInflater, parent, false);
return new GifViewHolder(binding, onItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull final GifViewHolder holder, final int position) {
holder.bind(getItem(position));
}
public static class GifViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = GifViewHolder.class.getSimpleName();
private static final int size = Utils.displayMetrics.widthPixels / 3;
private final ItemMediaBinding binding;
private final OnItemClickListener onItemClickListener;
public GifViewHolder(@NonNull final ItemMediaBinding binding,
final OnItemClickListener onItemClickListener) {
super(binding.getRoot());
this.binding = binding;
this.onItemClickListener = onItemClickListener;
binding.duration.setVisibility(View.GONE);
final GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(itemView.getResources());
builder.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER);
binding.item.setHierarchy(builder.build());
}
public void bind(final GiphyGif item) {
if (onItemClickListener != null) {
itemView.setOnClickListener(v -> onItemClickListener.onItemClick(item));
}
final BaseControllerListener<ImageInfo> controllerListener = new BaseControllerListener<ImageInfo>() {
@Override
public void onFailure(final String id, final Throwable throwable) {
Log.e(TAG, "onFailure: ", throwable);
}
};
final ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(item.getImages().getFixedHeight().getWebp()))
.setResizeOptions(ResizeOptions.forDimensions(size, size))
.build();
final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
.setImageRequest(request)
.setAutoPlayAnimations(true)
.setControllerListener(controllerListener);
binding.item.setController(builder.build());
}
}
public interface OnItemClickListener {
void onItemClick(GiphyGif giphyGif);
}
}

View File

@ -0,0 +1,53 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.StoryListViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightStoriesListAdapter extends ListAdapter<Story, StoryListViewHolder> {
private final OnHighlightStoryClickListener listener;
private static final DiffUtil.ItemCallback<Story> diffCallback = new DiffUtil.ItemCallback<Story>() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public HighlightStoriesListAdapter(final OnHighlightStoryClickListener listener) {
super(diffCallback);
this.listener = listener;
}
@NonNull
@Override
public StoryListViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new StoryListViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryListViewHolder holder, final int position) {
final Story model = getItem(position);
holder.bind(model, position, listener);
}
public interface OnHighlightStoryClickListener {
void onHighlightClick(final Story model, final int position);
void onProfileClick(final String username);
}
}

View File

@ -0,0 +1,55 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import awais.instagrabber.adapters.viewholder.HighlightViewHolder;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightsAdapter extends ListAdapter<Story, HighlightViewHolder> {
private final OnHighlightClickListener clickListener;
private static final DiffUtil.ItemCallback<Story> diffCallback = new DiffUtil.ItemCallback<Story>() {
@Override
public boolean areItemsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final Story oldItem, @NonNull final Story newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public HighlightsAdapter(final OnHighlightClickListener clickListener) {
super(diffCallback);
this.clickListener = clickListener;
}
@NonNull
@Override
public HighlightViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemHighlightBinding binding = ItemHighlightBinding.inflate(layoutInflater, parent, false);
return new HighlightViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final HighlightViewHolder holder, final int position) {
final Story highlightModel = getItem(position);
if (clickListener != null) {
holder.itemView.setOnClickListener(v -> clickListener.onHighlightClick(highlightModel, position));
}
holder.bind(highlightModel);
}
public interface OnHighlightClickListener {
void onHighlightClick(final Story model, final int position);
}
}

View File

@ -0,0 +1,42 @@
package awais.instagrabber.adapters;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.dialogs.KeywordsFilterDialogViewHolder;
public class KeywordsFilterAdapter extends RecyclerView.Adapter<KeywordsFilterDialogViewHolder> {
private final Context context;
private final ArrayList<String> items;
public KeywordsFilterAdapter(Context context, ArrayList<String> items){
this.context = context;
this.items = items;
}
@NonNull
@Override
public KeywordsFilterDialogViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
final View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_keyword, parent, false);
return new KeywordsFilterDialogViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull KeywordsFilterDialogViewHolder holder, int position) {
holder.bind(items, position, context, this);
}
@Override
public int getItemCount() {
return items.size();
}
}

View File

@ -0,0 +1,44 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import awais.instagrabber.adapters.viewholder.FollowsViewHolder;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.repositories.responses.User;
public final class LikesAdapter extends RecyclerView.Adapter<FollowsViewHolder> {
private final List<User> profileModels;
private final View.OnClickListener onClickListener;
public LikesAdapter(final List<User> profileModels,
final View.OnClickListener onClickListener) {
this.profileModels = profileModels;
this.onClickListener = onClickListener;
}
@NonNull
@Override
public FollowsViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false);
return new FollowsViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) {
final User model = profileModels.get(position);
holder.bind(model, onClickListener);
}
@Override
public int getItemCount() {
return profileModels.size();
}
}

View File

@ -0,0 +1,97 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import awais.instagrabber.adapters.viewholder.NotificationViewHolder;
import awais.instagrabber.databinding.ItemNotificationBinding;
import awais.instagrabber.models.enums.NotificationType;
import awais.instagrabber.repositories.responses.notification.Notification;
public final class NotificationsAdapter extends ListAdapter<Notification, NotificationViewHolder> {
private final OnNotificationClickListener notificationClickListener;
private static final DiffUtil.ItemCallback<Notification> DIFF_CALLBACK = new DiffUtil.ItemCallback<Notification>() {
@Override
public boolean areItemsTheSame(final Notification oldItem, final Notification newItem) {
return oldItem.getPk().equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Notification oldItem, @NonNull final Notification newItem) {
return oldItem.getPk().equals(newItem.getPk()) && oldItem.getType() == newItem.getType();
}
};
public NotificationsAdapter(final OnNotificationClickListener notificationClickListener) {
super(DIFF_CALLBACK);
this.notificationClickListener = notificationClickListener;
}
@NonNull
@Override
public NotificationViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int type) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemNotificationBinding binding = ItemNotificationBinding.inflate(layoutInflater, parent, false);
return new NotificationViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final NotificationViewHolder holder, final int position) {
final Notification Notification = getItem(position);
holder.bind(Notification, notificationClickListener);
}
@Override
public void submitList(@Nullable final List<Notification> list, @Nullable final Runnable commitCallback) {
if (list == null) {
super.submitList(null, commitCallback);
return;
}
super.submitList(sort(list), commitCallback);
}
@Override
public void submitList(@Nullable final List<Notification> list) {
if (list == null) {
super.submitList(null);
return;
}
super.submitList(sort(list));
}
private List<Notification> sort(final List<Notification> list) {
final List<Notification> listCopy = new ArrayList<>(list).stream()
.filter(i -> i.getType() != null)
.collect(Collectors.toList());
Collections.sort(listCopy, (o1, o2) -> {
// keep requests at top
if (o1.getType() == o2.getType()
&& o1.getType() == NotificationType.REQUEST
&& o2.getType() == NotificationType.REQUEST) return 0;
else if (o1.getType() == NotificationType.REQUEST) return -1;
else if (o2.getType() == NotificationType.REQUEST) return 1;
// timestamp
return Double.compare(o2.getArgs().getTimestamp(), o1.getArgs().getTimestamp());
});
return listCopy;
}
public interface OnNotificationClickListener {
void onNotificationClick(final Notification model);
void onProfileClick(final String username);
void onPreviewClick(final Notification model);
}
}

View File

@ -0,0 +1,61 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import java.util.Objects;
import awais.instagrabber.adapters.viewholder.TopicClusterViewHolder;
import awais.instagrabber.databinding.ItemDiscoverTopicBinding;
import awais.instagrabber.repositories.responses.saved.SavedCollection;
public class SavedCollectionsAdapter extends ListAdapter<SavedCollection, TopicClusterViewHolder> {
private static final DiffUtil.ItemCallback<SavedCollection> DIFF_CALLBACK = new DiffUtil.ItemCallback<SavedCollection>() {
@Override
public boolean areItemsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
return oldItem.getCollectionId().equals(newItem.getCollectionId());
}
@Override
public boolean areContentsTheSame(@NonNull final SavedCollection oldItem, @NonNull final SavedCollection newItem) {
if (oldItem.getCoverMediaList() != null && newItem.getCoverMediaList() != null
&& oldItem.getCoverMediaList().size() == newItem.getCoverMediaList().size()) {
return Objects.equals(oldItem.getCoverMediaList().get(0).getId(), newItem.getCoverMediaList().get(0).getId());
}
else if (oldItem.getCoverMedia() != null && newItem.getCoverMedia() != null) {
return Objects.equals(oldItem.getCoverMedia().getId(), newItem.getCoverMedia().getId());
}
return false;
}
};
private final OnCollectionClickListener onCollectionClickListener;
public SavedCollectionsAdapter(final OnCollectionClickListener onCollectionClickListener) {
super(DIFF_CALLBACK);
this.onCollectionClickListener = onCollectionClickListener;
}
@NonNull
@Override
public TopicClusterViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemDiscoverTopicBinding binding = ItemDiscoverTopicBinding.inflate(layoutInflater, parent, false);
return new TopicClusterViewHolder(binding, null, onCollectionClickListener);
}
@Override
public void onBindViewHolder(@NonNull final TopicClusterViewHolder holder, final int position) {
final SavedCollection topicCluster = getItem(position);
holder.bind(topicCluster);
}
public interface OnCollectionClickListener {
void onCollectionClick(SavedCollection savedCollection, View root, View cover, View title, int titleColor, int backgroundColor);
}
}

View File

@ -0,0 +1,33 @@
package awais.instagrabber.adapters;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import java.util.List;
import awais.instagrabber.fragments.search.SearchCategoryFragment;
import awais.instagrabber.models.enums.FavoriteType;
public class SearchCategoryAdapter extends FragmentStateAdapter {
private final List<FavoriteType> categories;
public SearchCategoryAdapter(@NonNull final Fragment fragment,
@NonNull final List<FavoriteType> categories) {
super(fragment);
this.categories = categories;
}
@NonNull
@Override
public Fragment createFragment(final int position) {
return SearchCategoryFragment.newInstance(categories.get(position));
}
@Override
public int getItemCount() {
return categories.size();
}
}

View File

@ -0,0 +1,215 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AdapterListUpdateCallback;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.SearchItemViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.fragments.search.SearchCategoryFragment.OnSearchItemClickListener;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.repositories.responses.search.SearchItem;
public final class SearchItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final String TAG = SearchItemsAdapter.class.getSimpleName();
private static final DiffUtil.ItemCallback<SearchItemOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<SearchItemOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
@Override
public boolean areContentsTheSame(@NonNull final SearchItemOrHeader oldItem, @NonNull final SearchItemOrHeader newItem) {
return Objects.equals(oldItem, newItem);
}
};
private static final String RECENT = "recent";
private static final String FAVORITE = "favorite";
private static final int VIEW_TYPE_HEADER = 0;
private static final int VIEW_TYPE_ITEM = 1;
private final OnSearchItemClickListener onSearchItemClickListener;
private final AsyncListDiffer<SearchItemOrHeader> differ;
public SearchItemsAdapter(final OnSearchItemClickListener onSearchItemClickListener) {
differ = new AsyncListDiffer<>(new AdapterListUpdateCallback(this),
new AsyncDifferConfig.Builder<>(DIFF_CALLBACK).build());
this.onSearchItemClickListener = onSearchItemClickListener;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == VIEW_TYPE_HEADER) {
return new HeaderViewHolder(ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false));
}
final ItemSearchResultBinding binding = ItemSearchResultBinding.inflate(layoutInflater, parent, false);
return new SearchItemViewHolder(binding, onSearchItemClickListener);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (getItemViewType(position) == VIEW_TYPE_HEADER) {
final SearchItemOrHeader searchItemOrHeader = getItem(position);
if (!searchItemOrHeader.isHeader()) return;
((HeaderViewHolder) holder).bind(searchItemOrHeader.header);
return;
}
((SearchItemViewHolder) holder).bind(getItem(position).searchItem);
}
protected SearchItemOrHeader getItem(int position) {
return differ.getCurrentList().get(position);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
}
public void submitList(@Nullable final List<SearchItem> list) {
if (list == null) {
differ.submitList(null);
return;
}
differ.submitList(sectionAndSort(list));
}
public void submitList(@Nullable final List<SearchItem> list, @Nullable final Runnable commitCallback) {
if (list == null) {
differ.submitList(null, commitCallback);
return;
}
differ.submitList(sectionAndSort(list), commitCallback);
}
@NonNull
private List<SearchItemOrHeader> sectionAndSort(@NonNull final List<SearchItem> list) {
final boolean containsRecentOrFavorite = list.stream().anyMatch(searchItem -> searchItem.isRecent() || searchItem.isFavorite());
// Don't do anything if not showing recent results
if (!containsRecentOrFavorite) {
return list.stream()
.map(SearchItemOrHeader::new)
.collect(Collectors.toList());
}
final List<SearchItem> listCopy = new ArrayList<>(list);
Collections.sort(listCopy, (o1, o2) -> {
final boolean bothRecent = o1.isRecent() && o2.isRecent();
if (bothRecent) {
// Don't sort
return 0;
}
final boolean bothFavorite = o1.isFavorite() && o2.isFavorite();
if (bothFavorite) {
if (o1.getType() == o2.getType()) return 0;
// keep users at top
if (o1.getType() == FavoriteType.USER) return -1;
if (o2.getType() == FavoriteType.USER) return 1;
// keep locations at bottom
if (o1.getType() == FavoriteType.LOCATION) return 1;
if (o2.getType() == FavoriteType.LOCATION) return -1;
}
// keep recents at top
if (o1.isRecent()) return -1;
if (o2.isRecent()) return 1;
return 0;
});
final List<SearchItemOrHeader> itemOrHeaders = new ArrayList<>();
for (int i = 0; i < listCopy.size(); i++) {
final SearchItem searchItem = listCopy.get(i);
final SearchItemOrHeader prev = itemOrHeaders.isEmpty() ? null : itemOrHeaders.get(itemOrHeaders.size() - 1);
boolean prevWasSameType = prev != null && ((prev.searchItem.isRecent() && searchItem.isRecent())
|| (prev.searchItem.isFavorite() && searchItem.isFavorite()));
if (prevWasSameType) {
// just add the item
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
continue;
}
// add header and item
// add header only if search item is recent or favorite
if (searchItem.isRecent() || searchItem.isFavorite()) {
itemOrHeaders.add(new SearchItemOrHeader(searchItem.isRecent() ? RECENT : FAVORITE));
}
itemOrHeaders.add(new SearchItemOrHeader(searchItem));
}
return itemOrHeaders;
}
private static class SearchItemOrHeader {
String header;
SearchItem searchItem;
public SearchItemOrHeader(final SearchItem searchItem) {
this.searchItem = searchItem;
}
public SearchItemOrHeader(final String header) {
this.header = header;
}
boolean isHeader() {
return header != null;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final SearchItemOrHeader that = (SearchItemOrHeader) o;
return Objects.equals(header, that.header) &&
Objects.equals(searchItem, that.searchItem);
}
@Override
public int hashCode() {
return Objects.hash(header, searchItem);
}
}
public static class HeaderViewHolder extends RecyclerView.ViewHolder {
private final ItemFavSectionHeaderBinding binding;
public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final String header) {
if (header == null) return;
final int headerText;
switch (header) {
case RECENT:
headerText = R.string.recent;
break;
case FAVORITE:
headerText = R.string.title_favorites;
break;
default:
headerText = R.string.unknown;
break;
}
binding.getRoot().setText(headerText);
}
}
}

View File

@ -0,0 +1,32 @@
package awais.instagrabber.adapters;
import android.view.View;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.repositories.responses.Media;
public class SliderCallbackAdapter implements SliderItemsAdapter.SliderCallback {
@Override
public void onThumbnailLoaded(final int position) {}
@Override
public void onItemClicked(final int position, final Media media, final View view) {}
@Override
public void onPlayerPlay(final int position) {}
@Override
public void onPlayerPause(final int position) {}
@Override
public void onPlayerRelease(final int position) {}
@Override
public void onFullScreenModeChanged(final boolean isFullScreen, final StyledPlayerView playerView) {}
@Override
public boolean isInFullScreen() {
return false;
}
}

View File

@ -0,0 +1,153 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import com.google.android.exoplayer2.ui.StyledPlayerView;
import awais.instagrabber.adapters.viewholder.SliderItemViewHolder;
import awais.instagrabber.adapters.viewholder.SliderPhotoViewHolder;
import awais.instagrabber.adapters.viewholder.SliderVideoViewHolder;
import awais.instagrabber.databinding.ItemSliderPhotoBinding;
import awais.instagrabber.databinding.LayoutVideoPlayerWithThumbnailBinding;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
public final class SliderItemsAdapter extends ListAdapter<Media, SliderItemViewHolder> {
private final boolean loadVideoOnItemClick;
private final SliderCallback sliderCallback;
private static final DiffUtil.ItemCallback<Media> DIFF_CALLBACK = new DiffUtil.ItemCallback<Media>() {
@Override
public boolean areItemsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return oldItem.getPk().equals(newItem.getPk());
}
@Override
public boolean areContentsTheSame(@NonNull final Media oldItem, @NonNull final Media newItem) {
return oldItem.getPk().equals(newItem.getPk());
}
};
public SliderItemsAdapter(final boolean loadVideoOnItemClick,
final SliderCallback sliderCallback) {
super(DIFF_CALLBACK);
this.loadVideoOnItemClick = loadVideoOnItemClick;
this.sliderCallback = sliderCallback;
}
@NonNull
@Override
public SliderItemViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final MediaItemType mediaItemType = MediaItemType.valueOf(viewType);
switch (mediaItemType) {
case MEDIA_TYPE_VIDEO: {
final LayoutVideoPlayerWithThumbnailBinding binding = LayoutVideoPlayerWithThumbnailBinding.inflate(inflater, parent, false);
return new SliderVideoViewHolder(binding, loadVideoOnItemClick);
}
case MEDIA_TYPE_IMAGE:
default:
final ItemSliderPhotoBinding binding = ItemSliderPhotoBinding.inflate(inflater, parent, false);
return new SliderPhotoViewHolder(binding);
}
}
@Override
public void onBindViewHolder(@NonNull final SliderItemViewHolder holder, final int position) {
final Media media = getItem(position);
holder.bind(media, position, sliderCallback);
}
@Override
public int getItemViewType(final int position) {
final Media media = getItem(position);
return media.getType().getId();
}
// @NonNull
// @Override
// public Object instantiateItem(@NonNull final ViewGroup container, final int position) {
// final Context context = container.getContext();
// final ViewerPostModel sliderItem = sliderItems.get(position);
//
// if (sliderItem.getItemType() == MediaItemType.MEDIA_TYPE_VIDEO) {
// final ViewSwitcher viewSwitcher = createViewSwitcher(context, position, sliderItem.getThumbnailUrl(), sliderItem.getDisplayUrl());
// container.addView(viewSwitcher);
// return viewSwitcher;
// }
// final GenericDraweeHierarchy hierarchy = GenericDraweeHierarchyBuilder.newInstance(container.getResources())
// .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
// .build();
// final SimpleDraweeView photoView = new SimpleDraweeView(context, hierarchy);
// photoView.setLayoutParams(layoutParams);
// final ImageRequest imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(sliderItem.getDisplayUrl()))
// .setLocalThumbnailPreviewsEnabled(true)
// .setProgressiveRenderingEnabled(true)
// .build();
// photoView.setImageRequest(imageRequest);
// container.addView(photoView);
// return photoView;
// }
// @NonNull
// private ViewSwitcher createViewSwitcher(final Context context,
// final int position,
// final String thumbnailUrl,
// final String displayUrl) {
//
// final ViewSwitcher viewSwitcher = new ViewSwitcher(context);
// viewSwitcher.setLayoutParams(layoutParams);
//
// final FrameLayout frameLayout = new FrameLayout(context);
// frameLayout.setLayoutParams(layoutParams);
//
// final GenericDraweeHierarchy hierarchy = new GenericDraweeHierarchyBuilder(context.getResources())
// .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
// .build();
// final SimpleDraweeView simpleDraweeView = new SimpleDraweeView(context, hierarchy);
// simpleDraweeView.setLayoutParams(layoutParams);
// simpleDraweeView.setImageURI(thumbnailUrl);
// frameLayout.addView(simpleDraweeView);
//
// final AppCompatImageView imageView = new AppCompatImageView(context);
// final int px = Utils.convertDpToPx(50);
// final FrameLayout.LayoutParams playButtonLayoutParams = new FrameLayout.LayoutParams(px, px);
// playButtonLayoutParams.gravity = Gravity.CENTER;
// imageView.setLayoutParams(playButtonLayoutParams);
// imageView.setImageResource(R.drawable.exo_icon_play);
// frameLayout.addView(imageView);
//
// viewSwitcher.addView(frameLayout);
//
// final PlayerView playerView = new PlayerView(context);
// viewSwitcher.addView(playerView);
// if (shouldAutoPlay && position == 0) {
// loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener);
// } else
// frameLayout.setOnClickListener(v -> loadPlayer(context, position, displayUrl, viewSwitcher, factory, playerChangeListener));
// return viewSwitcher;
// }
public interface SliderCallback {
void onThumbnailLoaded(int position);
void onItemClicked(int position, final Media media, final View view);
void onPlayerPlay(int position);
void onPlayerPause(int position);
void onPlayerRelease(int position);
void onFullScreenModeChanged(boolean isFullScreen, final StyledPlayerView playerView);
boolean isInFullScreen();
}
}

View File

@ -0,0 +1,90 @@
package awais.instagrabber.adapters;
import java.util.List;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemStoryBinding;
import awais.instagrabber.repositories.responses.stories.StoryMedia;
import awais.instagrabber.utils.ResponseBodyUtils;
public final class StoriesAdapter extends ListAdapter<StoryMedia, StoriesAdapter.StoryViewHolder> {
private final OnItemClickListener onItemClickListener;
private static final DiffUtil.ItemCallback<StoryMedia> diffCallback = new DiffUtil.ItemCallback<StoryMedia>() {
@Override
public boolean areItemsTheSame(@NonNull final StoryMedia oldItem, @NonNull final StoryMedia newItem) {
return oldItem.getId().equals(newItem.getId());
}
@Override
public boolean areContentsTheSame(@NonNull final StoryMedia oldItem, @NonNull final StoryMedia newItem) {
return oldItem.getId().equals(newItem.getId());
}
};
public StoriesAdapter(final OnItemClickListener onItemClickListener) {
super(diffCallback);
this.onItemClickListener = onItemClickListener;
}
@NonNull
@Override
public StoryViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final ItemStoryBinding binding = ItemStoryBinding.inflate(layoutInflater, parent, false);
return new StoryViewHolder(binding);
}
@Override
public void onBindViewHolder(@NonNull final StoryViewHolder holder, final int position) {
final StoryMedia storyMedia = getItem(position);
holder.bind(storyMedia, position, onItemClickListener);
}
public final static class StoryViewHolder extends RecyclerView.ViewHolder {
private final ItemStoryBinding binding;
public StoryViewHolder(final ItemStoryBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final StoryMedia model,
final int position,
final OnItemClickListener clickListener) {
if (model == null) return;
model.setPosition(position);
itemView.setTag(model);
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onItemClick(model, position);
});
binding.selectedView.setVisibility(model.isCurrentSlide() ? View.VISIBLE : View.GONE);
binding.icon.setImageURI(ResponseBodyUtils.getThumbUrl(model));
}
}
public void paginate(final int newIndex) {
final List<StoryMedia> list = getCurrentList();
for (int i = 0; i < list.size(); i++) {
final StoryMedia item = list.get(i);
if (!item.isCurrentSlide() && i != newIndex) continue;
item.setCurrentSlide(i == newIndex);
notifyItemChanged(i, item);
}
}
public interface OnItemClickListener {
void onItemClick(StoryMedia storyModel, int position);
}
}

View File

@ -0,0 +1,156 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import awais.instagrabber.R;
import awais.instagrabber.adapters.viewholder.TabViewHolder;
import awais.instagrabber.databinding.ItemFavSectionHeaderBinding;
import awais.instagrabber.databinding.ItemTabOrderPrefBinding;
import awais.instagrabber.models.Tab;
import awais.instagrabber.utils.Utils;
public class TabsAdapter extends ListAdapter<TabsAdapter.TabOrHeader, RecyclerView.ViewHolder> {
private static final DiffUtil.ItemCallback<TabOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<TabOrHeader>() {
@Override
public boolean areItemsTheSame(@NonNull final TabOrHeader oldItem, @NonNull final TabOrHeader newItem) {
if (oldItem.isHeader() && newItem.isHeader()) {
return oldItem.header == newItem.header;
}
if (!oldItem.isHeader() && !newItem.isHeader()) {
final Tab oldTab = oldItem.tab;
final Tab newTab = newItem.tab;
return oldTab.getIconResId() == newTab.getIconResId()
&& Objects.equals(oldTab.getTitle(), newTab.getTitle());
}
return false;
}
@Override
public boolean areContentsTheSame(@NonNull final TabOrHeader oldItem, @NonNull final TabOrHeader newItem) {
if (oldItem.isHeader() && newItem.isHeader()) {
return oldItem.header == newItem.header;
}
if (!oldItem.isHeader() && !newItem.isHeader()) {
final Tab oldTab = oldItem.tab;
final Tab newTab = newItem.tab;
return oldTab.getIconResId() == newTab.getIconResId()
&& Objects.equals(oldTab.getTitle(), newTab.getTitle());
}
return false;
}
};
private final TabAdapterCallback tabAdapterCallback;
private List<Tab> current = new ArrayList<>();
private List<Tab> others = new ArrayList<>();
public TabsAdapter(@NonNull final TabAdapterCallback tabAdapterCallback) {
super(DIFF_CALLBACK);
this.tabAdapterCallback = tabAdapterCallback;
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if (viewType == 1) {
final ItemTabOrderPrefBinding binding = ItemTabOrderPrefBinding.inflate(layoutInflater, parent, false);
return new TabViewHolder(binding, tabAdapterCallback);
}
final ItemFavSectionHeaderBinding headerBinding = ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false);
return new DirectUsersAdapter.HeaderViewHolder(headerBinding);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
if (holder instanceof DirectUsersAdapter.HeaderViewHolder) {
((DirectUsersAdapter.HeaderViewHolder) holder).bind(R.string.other_tabs);
return;
}
if (holder instanceof TabViewHolder) {
final Tab tab = getItem(position).tab;
((TabViewHolder) holder).bind(tab, others.contains(tab), current.size() == 5);
}
}
@Override
public int getItemViewType(final int position) {
return getItem(position).isHeader() ? 0 : 1;
}
public void submitList(final List<Tab> current, final List<Tab> others, final Runnable commitCallback) {
final ImmutableList.Builder<TabOrHeader> builder = ImmutableList.builder();
if (current != null) {
builder.addAll(current.stream()
.map(TabOrHeader::new)
.collect(Collectors.toList()));
}
builder.add(new TabOrHeader(R.string.other_tabs));
if (others != null) {
builder.addAll(others.stream()
.map(TabOrHeader::new)
.collect(Collectors.toList()));
}
// Mutable non-null copies
this.current = current != null ? new ArrayList<>(current) : new ArrayList<>();
this.others = others != null ? new ArrayList<>(others) : new ArrayList<>();
submitList(builder.build(), commitCallback);
}
public void submitList(final List<Tab> current, final List<Tab> others) {
submitList(current, others, null);
}
public void moveItem(final int from, final int to) {
final List<Tab> currentCopy = new ArrayList<>(current);
Utils.moveItem(from, to, currentCopy);
submitList(currentCopy, others);
tabAdapterCallback.onOrderChange(currentCopy);
}
public int getCurrentCount() {
return current.size();
}
public static class TabOrHeader {
Tab tab;
int header;
public TabOrHeader(final Tab tab) {
this.tab = tab;
}
public TabOrHeader(@StringRes final int header) {
this.header = header;
}
boolean isHeader() {
return header != 0;
}
}
public interface TabAdapterCallback {
void onStartDrag(TabViewHolder viewHolder);
void onOrderChange(List<Tab> newOrderTabs);
void onAdd(Tab tab);
void onRemove(Tab tab);
}
}

View File

@ -0,0 +1,151 @@
package awais.instagrabber.adapters;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener;
import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder;
import awais.instagrabber.adapters.viewholder.directmessages.RecipientThreadViewHolder;
import awais.instagrabber.databinding.LayoutDmUserItemBinding;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
public final class UserSearchResultsAdapter extends ListAdapter<RankedRecipient, RecyclerView.ViewHolder> {
private static final int VIEW_TYPE_USER = 0;
private static final int VIEW_TYPE_THREAD = 1;
private static final DiffUtil.ItemCallback<RankedRecipient> DIFF_CALLBACK = new DiffUtil.ItemCallback<RankedRecipient>() {
@Override
public boolean areItemsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) {
final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null;
if (!bothUsers) return false;
final boolean bothThreads = oldItem.getThread() != null && newItem.getThread() != null;
if (!bothThreads) return false;
if (bothUsers) {
return oldItem.getUser().getPk() == newItem.getUser().getPk();
}
return Objects.equals(oldItem.getThread().getThreadId(), newItem.getThread().getThreadId());
}
@Override
public boolean areContentsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) {
final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null;
if (bothUsers) {
return Objects.equals(oldItem.getUser().getUsername(), newItem.getUser().getUsername()) &&
Objects.equals(oldItem.getUser().getFullName(), newItem.getUser().getFullName());
}
return Objects.equals(oldItem.getThread().getThreadTitle(), newItem.getThread().getThreadTitle());
}
};
private final boolean showSelection;
private final Set<RankedRecipient> selectedRecipients;
private final OnDirectUserClickListener onUserClickListener;
private final OnRecipientClickListener onRecipientClickListener;
public UserSearchResultsAdapter(final boolean showSelection,
final OnRecipientClickListener onRecipientClickListener) {
super(DIFF_CALLBACK);
this.showSelection = showSelection;
selectedRecipients = showSelection ? new HashSet<>() : null;
this.onRecipientClickListener = onRecipientClickListener;
this.onUserClickListener = (position, user, selected) -> {
if (onRecipientClickListener != null) {
onRecipientClickListener.onClick(position, RankedRecipient.of(user), selected);
}
};
setHasStableIds(true);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false);
if (viewType == VIEW_TYPE_USER) {
return new DirectUserViewHolder(binding, onUserClickListener, null);
}
return new RecipientThreadViewHolder(binding, onRecipientClickListener);
}
@Override
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) {
final RankedRecipient recipient = getItem(position);
final int itemViewType = getItemViewType(position);
if (itemViewType == VIEW_TYPE_USER) {
boolean isSelected = false;
if (selectedRecipients != null) {
isSelected = selectedRecipients.stream()
.anyMatch(rankedRecipient -> rankedRecipient.getUser() != null
&& rankedRecipient.getUser().getPk() == recipient.getUser().getPk());
}
((DirectUserViewHolder) holder).bind(position, recipient.getUser(), false, false, showSelection, isSelected);
return;
}
boolean isSelected = false;
if (selectedRecipients != null) {
isSelected = selectedRecipients.stream()
.anyMatch(rankedRecipient -> rankedRecipient.getThread() != null
&& Objects.equals(rankedRecipient.getThread().getThreadId(), recipient.getThread().getThreadId()));
}
((RecipientThreadViewHolder) holder).bind(position, recipient.getThread(), showSelection, isSelected);
}
@Override
public long getItemId(final int position) {
final RankedRecipient recipient = getItem(position);
if (recipient.getUser() != null) {
return recipient.getUser().getPk();
}
if (recipient.getThread() != null) {
return recipient.getThread().getThreadTitle().hashCode();
}
return 0;
}
@Override
public int getItemViewType(final int position) {
final RankedRecipient recipient = getItem(position);
return recipient.getUser() != null ? VIEW_TYPE_USER : VIEW_TYPE_THREAD;
}
public void setSelectedRecipient(final RankedRecipient recipient, final boolean selected) {
if (selectedRecipients == null || recipient == null || (recipient.getUser() == null && recipient.getThread() == null)) return;
final boolean isUser = recipient.getUser() != null;
int position = -1;
final List<RankedRecipient> currentList = getCurrentList();
for (int i = 0; i < currentList.size(); i++) {
final RankedRecipient temp = currentList.get(i);
if (isUser) {
if (temp.getUser() != null && temp.getUser().getPk() == recipient.getUser().getPk()) {
position = i;
break;
}
continue;
}
if (temp.getThread() != null && Objects.equals(temp.getThread().getThreadId(), recipient.getThread().getThreadId())) {
position = i;
break;
}
}
if (position < 0) return;
if (selected) {
selectedRecipients.add(recipient);
} else {
selectedRecipients.remove(recipient);
}
notifyItemChanged(position);
}
public interface OnRecipientClickListener {
void onClick(int position, RankedRecipient recipient, final boolean isSelected);
}
}

View File

@ -0,0 +1,207 @@
package awais.instagrabber.adapters.viewholder;
import android.content.Context;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.Menu;
import android.view.View;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.appcompat.widget.PopupMenu;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.R;
import awais.instagrabber.adapters.CommentsAdapter.CommentCallback;
import awais.instagrabber.customviews.ProfilePicView;
import awais.instagrabber.databinding.ItemCommentBinding;
import awais.instagrabber.models.Comment;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.Utils;
public final class CommentViewHolder extends RecyclerView.ViewHolder {
private final ItemCommentBinding binding;
private final long currentUserId;
private final CommentCallback commentCallback;
@ColorInt
private int parentCommentHighlightColor;
private PopupMenu optionsPopup;
public CommentViewHolder(@NonNull final ItemCommentBinding binding,
final long currentUserId,
final CommentCallback commentCallback) {
super(binding.getRoot());
this.binding = binding;
this.currentUserId = currentUserId;
this.commentCallback = commentCallback;
final Context context = itemView.getContext();
if (context == null) return;
final Resources.Theme theme = context.getTheme();
if (theme == null) return;
final TypedValue typedValue = new TypedValue();
final boolean resolved = theme.resolveAttribute(R.attr.parentCommentHighlightColor, typedValue, true);
if (resolved) {
parentCommentHighlightColor = typedValue.data;
}
}
public void bind(final Comment comment, final boolean isReplyParent, final boolean isReply) {
if (comment == null) return;
itemView.setOnClickListener(v -> {
if (commentCallback != null) {
commentCallback.onClick(comment);
}
});
if (isReplyParent && parentCommentHighlightColor != 0) {
itemView.setBackgroundColor(parentCommentHighlightColor);
} else {
itemView.setBackgroundColor(itemView.getResources().getColor(android.R.color.transparent));
}
setupCommentText(comment, isReply);
binding.date.setText(comment.getDateTime());
setLikes(comment, isReply);
setReplies(comment, isReply);
setUser(comment, isReply);
setupOptions(comment, isReply);
}
private void setupCommentText(@NonNull final Comment comment, final boolean isReply) {
binding.comment.clearOnURLClickListeners();
binding.comment.clearOnHashtagClickListeners();
binding.comment.clearOnMentionClickListeners();
binding.comment.clearOnEmailClickListeners();
binding.comment.setText(comment.getText());
binding.comment.setTextSize(TypedValue.COMPLEX_UNIT_SP, isReply ? 12 : 14);
binding.comment.addOnHashtagListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onHashtagClick(originalText);
});
binding.comment.addOnMentionClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onMentionClick(originalText);
});
binding.comment.addOnEmailClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onEmailClick(originalText);
});
binding.comment.addOnURLClickListener(autoLinkItem -> {
final String originalText = autoLinkItem.getOriginalText();
if (commentCallback == null) return;
commentCallback.onURLClick(originalText);
});
binding.comment.setOnLongClickListener(v -> {
Utils.copyText(itemView.getContext(), comment.getText());
return true;
});
binding.comment.setOnClickListener(v -> commentCallback.onClick(comment));
}
private void setUser(@NonNull final Comment comment, final boolean isReply) {
final User user = comment.getUser();
if (user == null) return;
binding.username.setUsername(user.getUsername(), user.isVerified());
binding.username.setTextAppearance(itemView.getContext(), isReply ? R.style.TextAppearance_MaterialComponents_Subtitle2
: R.style.TextAppearance_MaterialComponents_Subtitle1);
binding.username.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
binding.profilePic.setImageURI(user.getProfilePicUrl());
binding.profilePic.setSize(isReply ? ProfilePicView.Size.SMALLER : ProfilePicView.Size.SMALL);
binding.profilePic.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onMentionClick("@" + user.getUsername());
});
}
private void setLikes(@NonNull final Comment comment, final boolean isReply) {
binding.likes.setText(String.valueOf(comment.getCommentLikeCount()));
binding.likes.setOnLongClickListener(v -> {
if (commentCallback == null) return false;
commentCallback.onViewLikes(comment);
return true;
});
if (currentUserId == 0) { // not logged in
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onViewLikes(comment);
});
return;
}
final boolean liked = comment.getLiked();
final int resId = liked ? R.drawable.ic_like : R.drawable.ic_not_liked;
binding.likes.setCompoundDrawablesRelativeWithSize(ContextCompat.getDrawable(itemView.getContext(), resId), null, null, null);
binding.likes.setOnClickListener(v -> {
if (commentCallback == null) return;
// toggle like
commentCallback.onLikeClick(comment, !liked, isReply);
});
}
private void setReplies(@NonNull final Comment comment, final boolean isReply) {
final int replies = comment.getChildCommentCount();
binding.replies.setVisibility(View.VISIBLE);
final String text = isReply ? "" : String.valueOf(replies);
binding.replies.setText(text);
binding.replies.setOnClickListener(v -> {
if (commentCallback == null) return;
commentCallback.onRepliesClick(comment);
});
}
private void setupOptions(final Comment comment, final boolean isReply) {
binding.options.setOnClickListener(v -> {
if (optionsPopup == null) {
createOptionsPopupMenu(comment, isReply);
}
if (optionsPopup == null) return;
optionsPopup.show();
});
}
private void createOptionsPopupMenu(final Comment comment, final boolean isReply) {
if (optionsPopup == null) {
final ContextThemeWrapper themeWrapper = new ContextThemeWrapper(itemView.getContext(), R.style.popupMenuStyle);
optionsPopup = new PopupMenu(themeWrapper, binding.options);
} else {
optionsPopup.getMenu().clear();
}
optionsPopup.getMenuInflater().inflate(R.menu.comment_options_menu, optionsPopup.getMenu());
final User user = comment.getUser();
if (currentUserId == 0 || user == null || user.getPk() != currentUserId) {
final Menu menu = optionsPopup.getMenu();
menu.removeItem(R.id.delete);
}
optionsPopup.setOnMenuItemClickListener(item -> {
if (commentCallback == null) return false;
int itemId = item.getItemId();
if (itemId == R.id.translate) {
commentCallback.onTranslate(comment);
return true;
}
if (itemId == R.id.delete) {
commentCallback.onDelete(comment, isReply);
}
return true;
});
}
// private void setupReply(final Comment comment) {
// if (!isLoggedIn) {
// binding.reply.setVisibility(View.GONE);
// return;
// }
// binding.reply.setOnClickListener(v -> {
// if (commentCallback == null) return;
// // toggle like
// commentCallback.onReplyClick(comment);
// });
// }
}

View File

@ -0,0 +1,26 @@
//package awais.instagrabber.adapters.viewholder;
//
//import android.view.View;
//import android.widget.ImageView;
//
//import androidx.annotation.NonNull;
//import androidx.recyclerview.widget.RecyclerView;
//
//import com.facebook.drawee.view.SimpleDraweeView;
//
//import awais.instagrabber.R;
//
//public final class DiscoverViewHolder extends RecyclerView.ViewHolder {
// public final SimpleDraweeView postImage;
// public final ImageView typeIcon;
// public final View selectedView;
// // public final View progressView;
//
// public DiscoverViewHolder(@NonNull final View itemView) {
// super(itemView);
// typeIcon = itemView.findViewById(R.id.typeIcon);
// postImage = itemView.findViewById(R.id.postImage);
// selectedView = itemView.findViewById(R.id.selectedView);
// // progressView = itemView.findViewById(R.id.progressView);
// }
//}

View File

@ -0,0 +1,61 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FavoritesAdapter;
import awais.instagrabber.databinding.ItemSearchResultBinding;
import awais.instagrabber.db.entities.Favorite;
import awais.instagrabber.models.enums.FavoriteType;
import awais.instagrabber.utils.Constants;
public class FavoriteViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = "FavoriteViewHolder";
private final ItemSearchResultBinding binding;
public FavoriteViewHolder(@NonNull final ItemSearchResultBinding binding) {
super(binding.getRoot());
this.binding = binding;
binding.verified.setVisibility(View.GONE);
}
public void bind(final Favorite model,
final FavoritesAdapter.OnFavoriteClickListener clickListener,
final FavoritesAdapter.OnFavoriteLongClickListener longClickListener) {
// Log.d(TAG, "bind: " + model);
if (model == null) return;
itemView.setOnClickListener(v -> {
if (clickListener == null) return;
clickListener.onClick(model);
});
itemView.setOnLongClickListener(v -> {
if (clickListener == null) return false;
return longClickListener.onLongClick(model);
});
if (model.getType() == FavoriteType.HASHTAG) {
binding.profilePic.setImageURI(Constants.DEFAULT_HASH_TAG_PIC);
} else {
binding.profilePic.setImageURI(model.getPicUrl());
}
binding.title.setVisibility(View.VISIBLE);
binding.subtitle.setText(model.getDisplayName());
String query = model.getQuery();
switch (model.getType()) {
case HASHTAG:
query = "#" + query;
break;
case USER:
query = "@" + query;
break;
case LOCATION:
binding.title.setVisibility(View.GONE);
break;
default:
// do nothing
}
binding.title.setText(query);
}
}

View File

@ -0,0 +1,212 @@
package awais.instagrabber.adapters.viewholder;
import android.content.Context;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder;
import com.facebook.imagepipeline.common.ResizeOptions;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import java.util.List;
import awais.instagrabber.R;
import awais.instagrabber.adapters.FeedAdapterV2;
import awais.instagrabber.databinding.ItemFeedGridBinding;
import awais.instagrabber.models.PostsLayoutPreferences;
import awais.instagrabber.models.enums.MediaItemType;
import awais.instagrabber.repositories.responses.Media;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils;
import static awais.instagrabber.models.PostsLayoutPreferences.PostsLayoutType.STAGGERED_GRID;
public class FeedGridItemViewHolder extends RecyclerView.ViewHolder {
private final ItemFeedGridBinding binding;
public FeedGridItemViewHolder(@NonNull final ItemFeedGridBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final int position,
@NonNull final Media media,
@NonNull final PostsLayoutPreferences layoutPreferences,
final FeedAdapterV2.FeedItemCallback feedItemCallback,
final FeedAdapterV2.AdapterSelectionCallback adapterSelectionCallback,
final boolean selectionModeActive,
final boolean selected) {
itemView.setOnClickListener(v -> {
if (!selectionModeActive && feedItemCallback != null) {
feedItemCallback.onPostClick(media);
return;
}
if (selectionModeActive && adapterSelectionCallback != null) {
adapterSelectionCallback.onPostClick(position, media);
}
});
if (adapterSelectionCallback != null) {
itemView.setOnLongClickListener(v -> adapterSelectionCallback.onPostLongClick(position, media));
}
binding.selectedView.setVisibility(selected ? View.VISIBLE : View.GONE);
// for rounded borders (clip view to background shape)
itemView.setClipToOutline(layoutPreferences.getHasRoundedCorners());
if (layoutPreferences.getType() == STAGGERED_GRID) {
final float aspectRatio = (float) media.getOriginalWidth() / media.getOriginalHeight();
binding.postImage.setAspectRatio(aspectRatio);
} else {
binding.postImage.setAspectRatio(1);
}
setUserDetails(media, layoutPreferences);
String thumbnailUrl = null;
final int typeIconRes;
final MediaItemType mediaType = media.getType();
if (mediaType == null) return;
switch (mediaType) {
case MEDIA_TYPE_IMAGE:
typeIconRes = -1;
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
break;
case MEDIA_TYPE_VIDEO:
thumbnailUrl = ResponseBodyUtils.getThumbUrl(media);
typeIconRes = R.drawable.exo_icon_play;
break;
case MEDIA_TYPE_SLIDER:
final List<Media> sliderItems = media.getCarouselMedia();
if (sliderItems != null) {
final Media child = sliderItems.get(0);
if (child != null) {
thumbnailUrl = ResponseBodyUtils.getThumbUrl(child);
if (layoutPreferences.getType() == STAGGERED_GRID) {
final float childAspectRatio = (float) child.getOriginalWidth() / child.getOriginalHeight();
binding.postImage.setAspectRatio(childAspectRatio);
}
}
}
typeIconRes = R.drawable.ic_checkbox_multiple_blank_stroke;
break;
default:
typeIconRes = -1;
thumbnailUrl = null;
}
setThumbImage(thumbnailUrl);
if (typeIconRes <= 0) {
binding.typeIcon.setVisibility(View.GONE);
} else {
binding.typeIcon.setVisibility(View.VISIBLE);
binding.typeIcon.setImageResource(typeIconRes);
}
binding.downloaded.setVisibility(View.GONE);
final Context context = itemView.getContext();
if (context == null) {
return;
}
AppExecutors.INSTANCE.getTasksThread().execute(() -> {
final List<Boolean> checkList = DownloadUtils.checkDownloaded(media, context);
if (checkList.isEmpty()) {
return;
}
AppExecutors.INSTANCE.getMainThread().execute(() -> {
switch (media.getType()) {
case MEDIA_TYPE_IMAGE:
case MEDIA_TYPE_VIDEO:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(R.color.green_A400)));
break;
case MEDIA_TYPE_SLIDER:
binding.downloaded.setVisibility(checkList.get(0) ? View.VISIBLE : View.GONE);
final List<Media> carouselMedia = media.getCarouselMedia();
boolean allDownloaded = checkList.size() == (carouselMedia == null ? 0 : carouselMedia.size());
if (allDownloaded) {
allDownloaded = checkList.stream().allMatch(downloaded -> downloaded);
}
binding.downloaded.setImageTintList(ColorStateList.valueOf(itemView.getResources().getColor(
allDownloaded ? R.color.green_A400 : R.color.yellow_400)));
break;
default:
}
});
});
}
private void setThumbImage(final String thumbnailUrl) {
if (TextUtils.isEmpty(thumbnailUrl)) {
binding.postImage.setController(null);
return;
}
final ImageRequest requestBuilder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(thumbnailUrl))
.setResizeOptions(ResizeOptions.forDimensions(binding.postImage.getWidth(),
binding.postImage.getHeight()))
.setLocalThumbnailPreviewsEnabled(true)
.setProgressiveRenderingEnabled(true)
.build();
final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder()
.setImageRequest(requestBuilder)
.setOldController(binding.postImage.getController());
binding.postImage.setController(builder.build());
}
private void setUserDetails(@NonNull final Media media,
@NonNull final PostsLayoutPreferences layoutPreferences) {
final User user = media.getUser();
if (layoutPreferences.isAvatarVisible()) {
if (user == null) {
binding.profilePic.setVisibility(View.GONE);
} else {
final String profilePicUrl = user.getProfilePicUrl();
if (TextUtils.isEmpty(profilePicUrl)) {
binding.profilePic.setVisibility(View.GONE);
} else {
binding.profilePic.setVisibility(View.VISIBLE);
binding.profilePic.setImageURI(profilePicUrl);
}
}
final ViewGroup.LayoutParams layoutParams = binding.profilePic.getLayoutParams();
@DimenRes final int dimenRes;
switch (layoutPreferences.getProfilePicSize()) {
case SMALL:
dimenRes = R.dimen.profile_pic_size_small;
break;
case TINY:
dimenRes = R.dimen.profile_pic_size_tiny;
break;
default:
case REGULAR:
dimenRes = R.dimen.profile_pic_size_regular;
break;
}
final int dimensionPixelSize = itemView.getResources().getDimensionPixelSize(dimenRes);
layoutParams.width = dimensionPixelSize;
layoutParams.height = dimensionPixelSize;
binding.profilePic.requestLayout();
} else {
binding.profilePic.setVisibility(View.GONE);
}
if (layoutPreferences.isNameVisible()) {
if (user == null) {
binding.name.setVisibility(View.GONE);
} else {
final String username = user.getUsername();
if (username == null) {
binding.name.setVisibility(View.GONE);
} else {
binding.name.setVisibility(View.VISIBLE);
binding.name.setText(username);
}
}
} else {
binding.name.setVisibility(View.GONE);
}
}
}

View File

@ -0,0 +1,44 @@
package awais.instagrabber.adapters.viewholder;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.adapters.FeedStoriesAdapter;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.User;
import awais.instagrabber.repositories.responses.stories.Story;
public final class FeedStoryViewHolder extends RecyclerView.ViewHolder {
private final ItemHighlightBinding binding;
public FeedStoryViewHolder(final ItemHighlightBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Story model,
final int position,
final FeedStoriesAdapter.OnFeedStoryClickListener listener) {
if (model == null) return;
binding.getRoot().setOnClickListener(v -> {
if (listener == null) return;
listener.onFeedStoryClick(model, position);
});
binding.getRoot().setOnLongClickListener(v -> {
if (listener != null) listener.onFeedStoryLongClick(model, position);
return true;
});
final User profileModel = model.getUser();
binding.title.setText(profileModel.getUsername());
final boolean isFullyRead =
model.getSeen() != null &&
model.getSeen().equals(model.getLatestReelMedia());
binding.title.setAlpha(isFullyRead ? 0.5F : 1.0F);
binding.icon.setImageURI(profileModel.getProfilePicUrl());
binding.icon.setAlpha(isFullyRead ? 0.5F : 1.0F);
if (model.getBroadcast() != null) binding.icon.setStoriesBorder(2);
else if (model.getHasBestiesMedia()) binding.icon.setStoriesBorder(1);
else binding.icon.setStoriesBorder(0);
}
}

View File

@ -0,0 +1,72 @@
package awais.instagrabber.adapters.viewholder;
import android.graphics.Bitmap;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import awais.instagrabber.adapters.FiltersAdapter;
import awais.instagrabber.databinding.ItemFilterBinding;
import awais.instagrabber.fragments.imageedit.filters.filters.Filter;
import awais.instagrabber.utils.AppExecutors;
import awais.instagrabber.utils.BitmapUtils;
import jp.co.cyberagent.android.gpuimage.GPUImage;
import jp.co.cyberagent.android.gpuimage.filter.GPUImageFilter;
public class FilterViewHolder extends RecyclerView.ViewHolder {
private static final String TAG = FilterViewHolder.class.getSimpleName();
private final ItemFilterBinding binding;
private final Collection<GPUImageFilter> tuneFilters;
private final FiltersAdapter.OnFilterClickListener onFilterClickListener;
private final AppExecutors appExecutors;
public FilterViewHolder(@NonNull final ItemFilterBinding binding,
final Collection<GPUImageFilter> tuneFilters,
final FiltersAdapter.OnFilterClickListener onFilterClickListener) {
super(binding.getRoot());
this.binding = binding;
this.tuneFilters = tuneFilters;
this.onFilterClickListener = onFilterClickListener;
appExecutors = AppExecutors.INSTANCE;
}
public void bind(final int position, final String originalKey, final Bitmap originalBitmap, final Filter<?> item, final boolean isSelected) {
if (originalBitmap == null || item == null) return;
if (onFilterClickListener != null) {
itemView.setOnClickListener(v -> onFilterClickListener.onClick(position, item));
}
if (item.getLabel() != -1) {
binding.name.setVisibility(View.VISIBLE);
binding.name.setText(item.getLabel());
binding.name.setSelected(isSelected);
} else {
binding.name.setVisibility(View.GONE);
}
final String filterKey = item.getLabel() + "_" + originalKey;
// avoid resetting the bitmap
if (binding.preview.getTag() != null && binding.preview.getTag().equals(filterKey)) return;
binding.preview.setTag(filterKey);
final Bitmap bitmap = BitmapUtils.getBitmapFromMemCache(filterKey);
if (bitmap == null) {
final GPUImageFilter filter = item.getInstance();
appExecutors.getTasksThread().submit(() -> {
GPUImage.getBitmapForMultipleFilters(
originalBitmap,
ImmutableList.<GPUImageFilter>builder().add(filter).addAll(tuneFilters).build(),
filteredBitmap -> {
BitmapUtils.addBitmapToMemoryCache(filterKey, filteredBitmap, true);
appExecutors.getMainThread().execute(() -> binding.getRoot().post(() -> binding.preview.setImageBitmap(filteredBitmap)));
}
);
});
return;
}
binding.getRoot().post(() -> binding.preview.setImageBitmap(bitmap));
}
}

View File

@ -0,0 +1,29 @@
package awais.instagrabber.adapters.viewholder;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemFollowBinding;
import awais.instagrabber.repositories.responses.User;
public final class FollowsViewHolder extends RecyclerView.ViewHolder {
private final ItemFollowBinding binding;
public FollowsViewHolder(@NonNull final ItemFollowBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final User model,
final View.OnClickListener onClickListener) {
if (model == null) return;
itemView.setTag(model);
itemView.setOnClickListener(onClickListener);
binding.username.setUsername("@" + model.getUsername(), model.isVerified());
binding.fullName.setText(model.getFullName());
binding.profilePic.setImageURI(model.getProfilePicUrl());
}
}

View File

@ -0,0 +1,31 @@
package awais.instagrabber.adapters.viewholder;
import androidx.recyclerview.widget.RecyclerView;
import awais.instagrabber.databinding.ItemHighlightBinding;
import awais.instagrabber.repositories.responses.stories.Story;
public final class HighlightViewHolder extends RecyclerView.ViewHolder {
private final ItemHighlightBinding binding;
public HighlightViewHolder(final ItemHighlightBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
public void bind(final Story model) {
if (model == null) return;
binding.title.setText(model.getTitle());
binding.icon.setImageURI(model.getCoverMedia().getCroppedImageVersion().getUrl());
// binding.getRoot().setOnClickListener(v -> {
// if (listener == null) return;
// listener.onFeedStoryClick(model, position);
// });
// final ProfileModel profileModel = model.getProfileModel();
// binding.title.setText(profileModel.getUsername());
// binding.title.setAlpha(model.getFullyRead() ? 0.5F : 1.0F);
// binding.icon.setImageURI(profileModel.getSdProfilePic());
// binding.icon.setAlpha(model.getFullyRead() ? 0.5F : 1.0F);
}
}

Some files were not shown because too many files have changed in this diff Show More