diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..25a652f --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Tiktok Video Downloader (Without Watermark) \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..ae388c2 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..83915d7 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/.idea/.gitignore b/app/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/app/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/app/.idea/.name b/app/.idea/.name new file mode 100644 index 0000000..25a652f --- /dev/null +++ b/app/.idea/.name @@ -0,0 +1 @@ +Tiktok Video Downloader (Without Watermark) \ No newline at end of file diff --git a/app/.idea/compiler.xml b/app/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/app/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/gradle.xml b/app/.idea/gradle.xml new file mode 100644 index 0000000..93fb31b --- /dev/null +++ b/app/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/app/.idea/kotlinc.xml b/app/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/app/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/.idea/misc.xml b/app/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/app/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/.idea/navEditor.xml b/app/.idea/navEditor.xml new file mode 100644 index 0000000..b6524c1 --- /dev/null +++ b/app/.idea/navEditor.xml @@ -0,0 +1,106 @@ + + + + + + \ No newline at end of file diff --git a/app/.idea/vcs.xml b/app/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/app/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..b498419 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,83 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") +} + +android { + namespace = "com.example.tiktokvideodownloaderwithoutwatermark" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.tiktokvideodownloaderwithoutwatermark" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + viewBinding = true + } + + tasks.register("wrapper") { + gradleVersion = "7.2" + } + + tasks.register("prepareKotlinBuildScriptModel"){} + +} + +dependencies { + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + //SDP TO SHOW SAME VIEW GROUP SIZES ON ALL SCREENS + implementation ("com.intuit.sdp:sdp-android:1.1.0") + + //NAVIGATION TO NAVIGATION FROM ONE DESTINATION TO OTHER + implementation ("androidx.navigation:navigation-fragment-ktx:2.7.5") + implementation ("androidx.navigation:navigation-ui-ktx:2.7.5") + + //SSP FOR TO SET SAME TEXT SIZE ON ALL SCREEN SIZES + implementation ("com.intuit.ssp:ssp-android:1.1.0") + + //RETROFIT FOR NETWORK CALLS + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + + //GLIDE LIBRARY FOR IMAGE CACHING + implementation ("com.github.bumptech.glide:glide:4.16.0") + annotationProcessor ("com.github.bumptech.glide:compiler:4.15.1") + + //LOTTIE FILES TO SHOW ANIMATION + implementation ("com.airbnb.android:lottie:6.2.0") + + +} \ No newline at end of file diff --git a/app/gradle/wrapper/gradle-wrapper.jar b/app/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/app/gradle/wrapper/gradle-wrapper.jar differ diff --git a/app/gradle/wrapper/gradle-wrapper.properties b/app/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..722117b --- /dev/null +++ b/app/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/app/gradlew b/app/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/app/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/app/gradlew.bat b/app/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/app/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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 \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..051ab4d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/DownloadAdapters.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/DownloadAdapters.kt new file mode 100644 index 0000000..35be319 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/DownloadAdapters.kt @@ -0,0 +1,96 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.adapters + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.VideoItemsBinding +import com.example.tiktokvideodownloaderwithoutwatermark.models.MediaModel +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils.showAudio +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils.showVideo +import okhttp3.internal.Util +import java.io.File + +class DownloadAdapters : RecyclerView.Adapter() { + private var downloadList: ArrayList = ArrayList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadViewHolder { + val binding = + VideoItemsBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return DownloadViewHolder(binding) + } + + override fun getItemCount(): Int { + return downloadList.size + } + + override fun onBindViewHolder(holder: DownloadViewHolder, position: Int) { + holder.bind(downloadList[position]) + } + + class DownloadViewHolder(private val binding: VideoItemsBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(item: MediaModel) { + Glide.with(binding.root.context).load(item.uri) + .placeholder(R.drawable.music).into(binding.tvImgMedia) + binding.tvVideoName.text = item.name + binding.tvVideoName.isSelected = true + (item.size + " - " + item.duration).also { binding.tvVideoDetails.text = it } + + binding.imgMore.setOnClickListener { + showPopupMenu(binding.imgMore, binding.root.context, item.uri) + } + + } + + private fun showPopupMenu(view: View, context: Context, uri: Uri) { + val popupMenu = PopupMenu(context, view, Gravity.START) + popupMenu.menuInflater.inflate(R.menu.more_menu, popupMenu.menu) + val isVideo = Utils.isVideoFile(context, uri) + + popupMenu.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.action_share -> { + if (isVideo) { + Utils.shareVideo(context, uri) + } else { + Utils.shareAudio(context, uri) + } + true + } + + R.id.action_view -> { + if (isVideo) { + showVideo(uri, context) + } else { + showAudio(uri, context) + } + true + } + + else -> false + } + } + + popupMenu.show() + } + } + + fun submitList(list: ArrayList) { + this.downloadList = list + notifyDataSetChanged() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/FolderAdapter.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/FolderAdapter.kt new file mode 100644 index 0000000..7579f98 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/adapters/FolderAdapter.kt @@ -0,0 +1,53 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.ListFoldersBinding +import com.example.tiktokvideodownloaderwithoutwatermark.models.folderModel + +class FolderAdapter : RecyclerView.Adapter() { + + private var downloadList: ArrayList = ArrayList() + private var listener: OnClickListener? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { + val binding = ListFoldersBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return FolderViewHolder(binding) + } + + override fun getItemCount(): Int { + return downloadList.size + } + + override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { + holder.bind(downloadList[position]) + holder.cdMain.setOnClickListener { + listener?.onItemClick(downloadList[position]) + } + + } + + class FolderViewHolder(private val binding: ListFoldersBinding) : + RecyclerView.ViewHolder(binding.root) { + val cdMain = binding.cdMain + fun bind(item: folderModel) { + binding.tvFolderName.text = item.name + binding.tvNoOfItems.text = "(${item.noOfItems})" + } + } + + fun submitList(list: ArrayList) { + this.downloadList = list + notifyDataSetChanged() + } + + interface OnClickListener { + fun onItemClick(item: folderModel) + } + + fun setOnClickListener(listener: OnClickListener) { + this.listener = listener + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/app/application.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/app/application.kt new file mode 100644 index 0000000..4b4c6a5 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/app/application.kt @@ -0,0 +1,9 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.app + +import android.app.Application + +class application: Application() { + override fun onCreate() { + super.onCreate() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/repository/TiktokRepo.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/repository/TiktokRepo.kt new file mode 100644 index 0000000..cf48359 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/repository/TiktokRepo.kt @@ -0,0 +1,34 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.data.remote.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.SocketTimeoutException + +class TiktokRepo { + private val expandedUrlLiveData = MutableLiveData() + fun getExpandedUrlLiveData(): LiveData { + return expandedUrlLiveData + } + + suspend fun expandShortenedUrl(shortenedUrl: String) = withContext(Dispatchers.IO) { + try { + val client = OkHttpClient() + val request = Request.Builder() + .url(shortenedUrl) + .build() + + val response = client.newCall(request).execute() + val expandedUrl = response.request().url().toString() + expandedUrlLiveData.postValue(expandedUrl) + } catch (e: SocketTimeoutException) { + expandedUrlLiveData.postValue("Timeout occurred: ${e.message}") + } catch (e: Exception) { + expandedUrlLiveData.postValue("Error: ${e.message}") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/viewModel/TiktokViewModel.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/viewModel/TiktokViewModel.kt new file mode 100644 index 0000000..928e550 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/data/remote/viewModel/TiktokViewModel.kt @@ -0,0 +1,230 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.data.remote.viewModel + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.app.DownloadManager +import android.content.ContentUris +import android.content.ContentValues.TAG +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment +import android.provider.MediaStore +import android.provider.Settings +import android.util.Log +import android.widget.Toast +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.tiktokvideodownloaderwithoutwatermark.data.remote.repository.TiktokRepo +import com.example.tiktokvideodownloaderwithoutwatermark.models.MediaModel +import com.example.tiktokvideodownloaderwithoutwatermark.models.folderModel +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils.AUDIOS +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils.VIDEOS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File + + +class TiktokViewModel : ViewModel() { + private val tiktokRepo: TiktokRepo = TiktokRepo() + + private val expandedUrlLiveData = tiktokRepo.getExpandedUrlLiveData() + fun getExpandedUrlLiveData(): LiveData { + return expandedUrlLiveData + } + + fun expandShortenedUrl(shortenedUrl: String) { + viewModelScope.launch { + tiktokRepo.expandShortenedUrl(shortenedUrl) + } + } + + fun extractVideoIdFromUrl(url: String): String? { + val pattern = Regex("/video/(\\d+)") + val matchResult = pattern.find(url) + return matchResult?.groupValues?.getOrNull(1) + } + + fun downloadVideo(filename: String, downloadUrlOfVideo: String, context: Context) { + try { + if (!VIDEOS.exists()) { + VIDEOS.mkdirs() + } + + val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadUri = Uri.parse(downloadUrlOfVideo) + val request = DownloadManager.Request(downloadUri) + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + .setAllowedOverRoaming(false) + .setTitle(filename) + .setMimeType("video/*") + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setDestinationUri(Uri.fromFile(File(VIDEOS, "$filename"))) + + dm.enqueue(request) + + Toast.makeText(context, "Download started!", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Download failed! ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + + fun downloadAudio(filename: String, downloadUrlOfVideo: String, context: Context) { + try { + if (!AUDIOS.exists()) { + AUDIOS.mkdirs() + } + + val dm = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadUri = Uri.parse(downloadUrlOfVideo) + val request = DownloadManager.Request(downloadUri) + request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + .setAllowedOverRoaming(false) + .setTitle(filename) + .setMimeType("audio/*") + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) + .setDestinationUri(Uri.fromFile(File(AUDIOS, "$filename"))) + + dm.enqueue(request) + + Toast.makeText(context, "Download started!", Toast.LENGTH_SHORT).show() + } catch (e: Exception) { + Toast.makeText(context, "Download failed! ${e.message}", Toast.LENGTH_SHORT).show() + } + } + + + fun showAlertDialog(heading: String, subHeading: String, context: Context) { + AlertDialog.Builder(context) + .setTitle(heading) + .setMessage(subHeading) + .setPositiveButton("OK") { dialog, _ -> + dialog.dismiss() + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + context.startActivity(intent) + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .show() + } + + + suspend fun getVideosFromFolder(context: Context, folderPath: String): ArrayList = withContext(Dispatchers.IO) { + val videoList: ArrayList = ArrayList() + val projection = arrayOf( + MediaStore.Video.Media._ID, + MediaStore.Video.Media.DISPLAY_NAME, + MediaStore.Video.Media.DURATION, + MediaStore.Video.Media.SIZE + ) + + val selection = "${MediaStore.Video.Media.DATA} like ?" + val selectionArgs = arrayOf("$folderPath%") + + val sortOrder = "${MediaStore.Video.Media.DATE_MODIFIED} DESC" + + val queryUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + + context.contentResolver.query(queryUri, projection, selection, selectionArgs, sortOrder) + ?.use { cursor -> + while (cursor.moveToNext()) { + val videoId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)) + val displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)) + val duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)) + val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)) + + val contentUri: Uri = ContentUris.withAppendedId( + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + videoId + ) + videoList.add(MediaModel(displayName, contentUri, formatDuration(duration), formatSize(size))) + } + } + return@withContext videoList + } + + suspend fun getAudiosFromFolder(context: Context, folderPath: String): ArrayList = withContext(Dispatchers.IO) { + val audioList: ArrayList = ArrayList() + val projection = arrayOf( + MediaStore.Audio.Media._ID, + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.SIZE + ) + + val selection = "${MediaStore.Audio.Media.DATA} like ?" + val selectionArgs = arrayOf("$folderPath%") + + val sortOrder = "${MediaStore.Audio.Media.DATE_MODIFIED} DESC" + + val queryUri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + + context.contentResolver.query(queryUri, projection, selection, selectionArgs, sortOrder) + ?.use { cursor -> + while (cursor.moveToNext()) { + val videoId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)) + val displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)) + val duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)) + val size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)) + + val contentUri: Uri = ContentUris.withAppendedId( + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + videoId + ) + audioList.add(MediaModel(displayName, contentUri, formatDuration(duration), formatSize(size))) + } + } + return@withContext audioList + } + + + + private fun formatSize(size: Long): String { + val fileSizeInKB = size / 1024 + val fileSizeInMB = fileSizeInKB / 1024 + + return if (fileSizeInMB > 0) { + String.format("%d MB", fileSizeInMB) + } else { + String.format("%d KB", fileSizeInKB) + } + } + + private fun formatDuration(duration: Long): String { + val seconds = duration / 1000 + val minutes = seconds / 60 + val remainingSeconds = seconds % 60 + + return String.format("%02d:%02d", minutes, remainingSeconds) + } + + + suspend fun getFolderInfo(): ArrayList = withContext(Dispatchers.IO) { + val folderList: ArrayList = ArrayList() + if (Utils.TIKTOK_DOWNLOAD_PATH.exists() && Utils.TIKTOK_DOWNLOAD_PATH.isDirectory) { + val videosFolder = Utils.TIKTOK_DOWNLOAD_PATH.resolve("VIDEOS") + val videosCount = getMediaCount(videosFolder) + folderList.add(folderModel(videosFolder.name, videosCount)) + + val audiosFolder = Utils.TIKTOK_DOWNLOAD_PATH.resolve("AUDIOS") + val audiosCount = getMediaCount(audiosFolder) + folderList.add(folderModel(audiosFolder.name, audiosCount)) + } else { + Log.e("NotFound", "TikTok folder not found.") + } + return@withContext folderList + } + + private fun getMediaCount(folder: File): Int { + return folder.listFiles { file -> file.isFile }?.size ?: 0 + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/MediaModel.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/MediaModel.kt new file mode 100644 index 0000000..a9889b2 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/MediaModel.kt @@ -0,0 +1,5 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.models + +import android.net.Uri + +data class MediaModel(val name: String, val uri: Uri, val duration: String, val size: String) diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/folderModel.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/folderModel.kt new file mode 100644 index 0000000..c57294d --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/models/folderModel.kt @@ -0,0 +1,3 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.models + +data class folderModel(val name: String, val noOfItems: Int) diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/activities/MainActivity.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/activities/MainActivity.kt new file mode 100644 index 0000000..1b5d107 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/activities/MainActivity.kt @@ -0,0 +1,18 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.activities + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.navigation.findNavController +import androidx.navigation.ui.setupWithNavController +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.google.android.material.bottomnavigation.BottomNavigationView + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + val navController = this.findNavController(R.id.nav_host_fragment) + val navView: BottomNavigationView = findViewById(R.id.bottom_nav_view) + navView.setupWithNavController(navController) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/AppSettings.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/AppSettings.kt new file mode 100644 index 0000000..2d57422 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/AppSettings.kt @@ -0,0 +1,85 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.fragments + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.example.tiktokvideodownloaderwithoutwatermark.data.remote.viewModel.TiktokViewModel +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.FragmentAppSettingsBinding +import com.example.tiktokvideodownloaderwithoutwatermark.models.folderModel +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils + + +class AppSettings : Fragment(){ + private var _binding: FragmentAppSettingsBinding? = null + private val binding get() = _binding!! + private val viewModel: TiktokViewModel by activityViewModels() + private val list: ArrayList = ArrayList() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAppSettingsBinding.inflate(inflater, container, false) + + binding.storagePath.text = Utils.TIKTOK_DOWNLOAD_PATH.absolutePath + binding.appVersion.text = "1.0" + binding.storagePath.isSelected = true + + binding.cdPrivcyPolicy.setOnClickListener { + try { + findNavController().navigate(R.id.action_action_settings_to_privacyPolicy) + } catch (ex: Exception) { + ex.printStackTrace() + } + } + + binding.cdShare.setOnClickListener { shareAppLink() } + + binding.cdRateUs.setOnClickListener { openPlayStore() } + + return binding.root + } + + + private fun shareAppLink() { + val appLink = + "https://play.google.com/store/apps/details?id=${requireContext().packageName}" + val intent = Intent(Intent.ACTION_SEND) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TEXT, appLink) + startActivity(Intent.createChooser(intent, "Share App Link")) + } + + private fun openPlayStore() { + val appPackageName = requireActivity().packageName + try { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("market://details?id=$appPackageName") + ) + ) + } catch (e: android.content.ActivityNotFoundException) { + startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName") + ) + ) + } + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Download.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Download.kt new file mode 100644 index 0000000..57141b1 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Download.kt @@ -0,0 +1,79 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.example.tiktokvideodownloaderwithoutwatermark.adapters.FolderAdapter +import com.example.tiktokvideodownloaderwithoutwatermark.data.remote.viewModel.TiktokViewModel +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.FragmentDownloadBinding +import com.example.tiktokvideodownloaderwithoutwatermark.models.folderModel +import kotlinx.coroutines.launch + +class Download : Fragment() { + private var _binding: FragmentDownloadBinding? = null + private val binding get() = _binding!! + private val viewModel: TiktokViewModel by activityViewModels() + private lateinit var adapter: FolderAdapter + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentDownloadBinding.inflate(inflater, container, false) + + adapter = FolderAdapter() + binding.recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + + lifecycleScope.launch { + val list = viewModel.getFolderInfo() + if (list.isNotEmpty()) { + binding.textView2.visibility = View.GONE + adapter.submitList(list) + } + } + + adapter.setOnClickListener(listener = object : FolderAdapter.OnClickListener { + override fun onItemClick(item: folderModel) { + try { + val bundle = Bundle() + when (item.name) { + "AUDIOS" -> { + bundle.putString("MEDIA", "audio") + findNavController().navigate( + R.id.action_action_download_to_mediaItems, + bundle + ) + } + + "VIDEOS" -> { + bundle.putString("MEDIA", "video") + findNavController().navigate( + R.id.action_action_download_to_mediaItems, + bundle + ) + } + } + } catch (ex: Exception) { + ex.printStackTrace() + } + } + + }) + + return binding.root + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Home.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Home.kt new file mode 100644 index 0000000..051d409 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/Home.kt @@ -0,0 +1,211 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.fragments + +import android.Manifest +import android.app.AlertDialog +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.Window +import android.view.inputmethod.InputMethodManager +import androidx.activity.result.contract.ActivityResultContracts +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.example.tiktokvideodownloaderwithoutwatermark.data.remote.viewModel.TiktokViewModel +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils.TIKTOK_DOWNLOAD_PATH +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.FragmentHomeBinding +import com.example.tiktokvideodownloaderwithoutwatermark.utils.showToast +import kotlinx.coroutines.launch + +class Home : Fragment() { + private var _binding: FragmentHomeBinding? = null + private val binding get() = _binding!! + private val viewModel: TiktokViewModel by activityViewModels() + private var videoId: String? = null + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentHomeBinding.inflate(inflater, container, false) + + binding.btnDownload.setOnClickListener { + hideKeyboard(binding.root) + startReadExternalStoragePermission() + } + + + + binding.clearText.setOnClickListener { + binding.edUrl.setText("") + } + + + binding.btnPaste.setOnClickListener { + val copyBoardText = Utils.pasteFromClipboard(requireContext()) + if (copyBoardText.isNotEmpty()) { + binding.edUrl.setText(copyBoardText) + } else { + binding.edUrl.setText("") + } + } + + + binding.cdAudio.setOnClickListener { + viewModel.downloadAudio( + "Video-$videoId.mp3", + "${Utils.DOWNLOAD_AUDIO_LINK}$videoId.mp3", + requireContext() + ) + } + + binding.cdOriginalVideo.setOnClickListener { + viewModel.downloadVideo( + "Video-Original-$videoId.mp4", + "${Utils.DOWNLOAD_ORIGINAL_VIDEO_LINK}$videoId.mp4", + requireContext() + ) + } + + binding.cdDownloadWithoutWaterMark.setOnClickListener { + viewModel.downloadVideo( + "Video-WM-$videoId.mp4", + "${Utils.DOWNLOAD_VIDEO_WITHOUT_WATERMARK}$videoId.mp4", + requireContext() + ) + } + + binding.cdDownloadWithoutWaterMarkHD.setOnClickListener { + viewModel.downloadVideo( + "Video-WMHD-$videoId.mp4", + "${Utils.DOWNLOAD_VIDEO_WITHOUT_WATERMARK_HD}$videoId.mp4", + requireContext() + ) + } + + + binding.downloadAnother.setOnClickListener { + binding.cdDownloadOptions.visibility = View.GONE + } + + + + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + + private fun startReadExternalStoragePermission() { + this.requestReadExternalPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + + private val requestReadExternalPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + startWriteExternalStoragePermission() + } else { + viewModel.showAlertDialog( + getString(R.string.media_permission), + getString(R.string.please_allow_media_permission_to_continue), + requireContext() + ) + } + } + + private fun startWriteExternalStoragePermission() { + this.requestWriteExternalPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + + private val requestWriteExternalPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + if (binding.edUrl.text.toString().isNotEmpty()) { + lifecycleScope.launch { + if (Utils.isValidUrl(binding.edUrl.text.toString())) { + + val dialogBuilder = AlertDialog.Builder(context) + val inflater = requireActivity().layoutInflater + val dialogView: View = + inflater.inflate(R.layout.fetching_dialog, null, false) + dialogBuilder.setView(dialogView) + val alertDialog = dialogBuilder.create() + alertDialog.setCancelable(false) + + + + alertDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + alertDialog.window!!.attributes.windowAnimations = R.style.DialogAnimation + + + + alertDialog.show() + val window: Window? = alertDialog.window + window!!.setLayout( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + + + + viewModel.expandShortenedUrl(binding.edUrl.text.toString()) + viewModel.getExpandedUrlLiveData() + .observe(viewLifecycleOwner) { expandedUrl -> + alertDialog.dismiss() + if (expandedUrl == "Error:" || expandedUrl == "Timeout occurred:") { + showToast(expandedUrl.toString()) + return@observe + } else { + videoId = viewModel.extractVideoIdFromUrl(expandedUrl) + if (!TIKTOK_DOWNLOAD_PATH.exists()) { + TIKTOK_DOWNLOAD_PATH.mkdir() + } + Glide.with(requireContext()) + .load("${Utils.SOURCE_IMAGEVIEW}$videoId.webp") + .into(binding.srcImage) + + binding.edUrl.setText("") + binding.cdDownloadOptions.visibility = View.VISIBLE + } + } + + } else { + showToast("Invalid url, please check again that pasted url is of tiktok video.") + } + } + } else { + showToast("url is not valid!") + } + } else { + + viewModel.showAlertDialog( + getString(R.string.media_permission), + getString(R.string.please_allow_media_permission_to_continue), + requireContext() + + ) + } + } + + + private fun hideKeyboard(view: View) { + val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/MediaItems.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/MediaItems.kt new file mode 100644 index 0000000..563a920 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/MediaItems.kt @@ -0,0 +1,63 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.example.tiktokvideodownloaderwithoutwatermark.R +import com.example.tiktokvideodownloaderwithoutwatermark.adapters.DownloadAdapters +import com.example.tiktokvideodownloaderwithoutwatermark.data.remote.viewModel.TiktokViewModel +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.FragmentMediaItemsBinding +import com.example.tiktokvideodownloaderwithoutwatermark.models.MediaModel +import com.example.tiktokvideodownloaderwithoutwatermark.utils.Utils +import kotlinx.coroutines.launch + +class MediaItems : Fragment() { + private var _binding: FragmentMediaItemsBinding? = null + private val binding get() = _binding!! + private val viewModel: TiktokViewModel by activityViewModels() + private lateinit var adapter: DownloadAdapters + private var list: ArrayList = ArrayList() + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentMediaItemsBinding.inflate(inflater, container, false) + + val arguments: String? = requireArguments().getString("MEDIA") + + adapter = DownloadAdapters() + binding.recyclerView.adapter = adapter + binding.recyclerView.setHasFixedSize(true) + + lifecycleScope.launch { + when (arguments) { + "audio" -> { + list = + viewModel.getAudiosFromFolder(requireContext(), Utils.AUDIOS.absolutePath) + adapter.submitList(list) + } + + "video" -> { + list = + viewModel.getVideosFromFolder(requireContext(), Utils.VIDEOS.absolutePath) + adapter.submitList(list) + } + } + } + + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/PrivacyPolicy.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/PrivacyPolicy.kt new file mode 100644 index 0000000..104babe --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/ui/fragments/PrivacyPolicy.kt @@ -0,0 +1,30 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.ui.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import com.example.tiktokvideodownloaderwithoutwatermark.databinding.FragmentPrivacyPolicyBinding + + +class PrivacyPolicy : Fragment() { + private var _binding: FragmentPrivacyPolicyBinding? = null + private val binding get() = _binding!! + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentPrivacyPolicyBinding.inflate(inflater, container, false) + binding.textView.setOnClickListener { + findNavController().popBackStack() + } + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/Utils.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/Utils.kt new file mode 100644 index 0000000..bd8f093 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/Utils.kt @@ -0,0 +1,129 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.utils + +import android.app.Activity +import android.content.ClipboardManager +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Environment +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import java.io.File +import java.net.MalformedURLException +import java.net.URL + +object Utils { + val DOWNLOAD_AUDIO_LINK = "https://www.tikwm.com/video/music/" + val DOWNLOAD_ORIGINAL_VIDEO_LINK = "https://www.tikwm.com/video/media/wmplay/" + val DOWNLOAD_VIDEO_WITHOUT_WATERMARK = "https://www.tikwm.com/video/media/play/" + val DOWNLOAD_VIDEO_WITHOUT_WATERMARK_HD = "https://www.tikwm.com/video/media/hdplay/" + val SOURCE_IMAGEVIEW = "https://www.tikwm.com/video/cover/" + + + val TIKTOK_DOWNLOAD_PATH = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "TIKTOK VIDEO DOWNLOADER" + ) + + val VIDEOS = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "TIKTOK VIDEO DOWNLOADER/VIDEOS" + ) + + val AUDIOS = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "TIKTOK VIDEO DOWNLOADER/AUDIOS" + ) + + fun isValidUrl(urlString: String): Boolean { + return try { + val url = URL(urlString) + url.protocol == "http" || url.protocol == "https" + true + } catch (e: MalformedURLException) { + e.printStackTrace() + false + } + } + + fun shareVideo(context: Context, videoUri: Uri) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "video/*" + shareIntent.putExtra(Intent.EXTRA_STREAM, videoUri) + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(Intent.createChooser(shareIntent, "Share Video")) + } + + + fun showVideo(uri: Uri, context: Context) { + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.setDataAndType(uri, "video/*") + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + Toast.makeText(context, "No app to handle video playback", Toast.LENGTH_SHORT) + .show() + } + } + + fun showAudio(uri: Uri, context: Context) { + val intent = Intent(Intent.ACTION_VIEW, uri) + intent.setDataAndType(uri, "audio/*") + + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + Toast.makeText(context, "No app to handle video playback", Toast.LENGTH_SHORT) + .show() + } + } + + fun shareAudio(context: Context, audioUri: Uri) { + val shareIntent = Intent(Intent.ACTION_SEND) + shareIntent.type = "audio/*" + shareIntent.putExtra(Intent.EXTRA_STREAM, audioUri) + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + context.startActivity(Intent.createChooser(shareIntent, "Share Audio")) + } + + fun pasteFromClipboard(context: Context): String { + var clipBoardText: String? = null + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + if (clipboard.hasPrimaryClip()) { + val clip = clipboard.primaryClip + val item = clip?.getItemAt(0) + clipBoardText = item?.text.toString() + } + return clipBoardText!! + } + + + fun isVideoFile(context: Context, uri: Uri): Boolean { + val mimeType = getMimeType(context, uri) + return mimeType?.startsWith("video/") == true + } + + fun isAudioFile(context: Context, uri: Uri): Boolean { + val mimeType = getMimeType(context, uri) + return mimeType?.startsWith("audio/") == true + } + + private fun getMimeType(context: Context, uri: Uri): String? { + val contentResolver: ContentResolver = context.contentResolver + return try { + contentResolver.getType(uri) + } catch (e: Exception) { + null + } + } + + + fun Context.hideKeyboard(view: View) { + val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/extension.kt b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/extension.kt new file mode 100644 index 0000000..8a267a2 --- /dev/null +++ b/app/src/main/java/com/example/tiktokvideodownloaderwithoutwatermark/utils/extension.kt @@ -0,0 +1,12 @@ +package com.example.tiktokvideodownloaderwithoutwatermark.utils + +import android.view.View +import android.widget.Toast +import androidx.fragment.app.Fragment + +fun Fragment.showToast(message: String) +{ + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() +} + + diff --git a/app/src/main/res/drawable/baseline_close_24.xml b/app/src/main/res/drawable/baseline_close_24.xml new file mode 100644 index 0000000..a038f30 --- /dev/null +++ b/app/src/main/res/drawable/baseline_close_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_downloading_24.xml b/app/src/main/res/drawable/baseline_downloading_24.xml new file mode 100644 index 0000000..95f1fc6 --- /dev/null +++ b/app/src/main/res/drawable/baseline_downloading_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 0000000..ad8e698 --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_keyboard_backspace_24.xml b/app/src/main/res/drawable/baseline_keyboard_backspace_24.xml new file mode 100644 index 0000000..a23629f --- /dev/null +++ b/app/src/main/res/drawable/baseline_keyboard_backspace_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_more_vert_24.xml b/app/src/main/res/drawable/baseline_more_vert_24.xml new file mode 100644 index 0000000..c73e4c0 --- /dev/null +++ b/app/src/main/res/drawable/baseline_more_vert_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..a779a56 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/bg_settings.xml b/app/src/main/res/drawable/bg_settings.xml new file mode 100644 index 0000000..eb620ab --- /dev/null +++ b/app/src/main/res/drawable/bg_settings.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bnv_tab_item_foreground.xml b/app/src/main/res/drawable/bnv_tab_item_foreground.xml new file mode 100644 index 0000000..79de3e3 --- /dev/null +++ b/app/src/main/res/drawable/bnv_tab_item_foreground.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/close_ripple_effect.xml b/app/src/main/res/drawable/close_ripple_effect.xml new file mode 100644 index 0000000..6d43aac --- /dev/null +++ b/app/src/main/res/drawable/close_ripple_effect.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/editext_bg.xml b/app/src/main/res/drawable/editext_bg.xml new file mode 100644 index 0000000..b660a1e --- /dev/null +++ b/app/src/main/res/drawable/editext_bg.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/folder.xml b/app/src/main/res/drawable/folder.xml new file mode 100644 index 0000000..c22df3f --- /dev/null +++ b/app/src/main/res/drawable/folder.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/lock.png b/app/src/main/res/drawable/lock.png new file mode 100644 index 0000000..43d4033 Binary files /dev/null and b/app/src/main/res/drawable/lock.png differ diff --git a/app/src/main/res/drawable/music.xml b/app/src/main/res/drawable/music.xml new file mode 100644 index 0000000..4cb87fb --- /dev/null +++ b/app/src/main/res/drawable/music.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/not_ads.xml b/app/src/main/res/drawable/not_ads.xml new file mode 100644 index 0000000..d257900 --- /dev/null +++ b/app/src/main/res/drawable/not_ads.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/privacy_policy.xml b/app/src/main/res/drawable/privacy_policy.xml new file mode 100644 index 0000000..d1b5820 --- /dev/null +++ b/app/src/main/res/drawable/privacy_policy.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/profile.xml b/app/src/main/res/drawable/profile.xml new file mode 100644 index 0000000..8593ee6 --- /dev/null +++ b/app/src/main/res/drawable/profile.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/rate_me.xml b/app/src/main/res/drawable/rate_me.xml new file mode 100644 index 0000000..c9565d0 --- /dev/null +++ b/app/src/main/res/drawable/rate_me.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/share.xml b/app/src/main/res/drawable/share.xml new file mode 100644 index 0000000..972437e --- /dev/null +++ b/app/src/main/res/drawable/share.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/storage.xml b/app/src/main/res/drawable/storage.xml new file mode 100644 index 0000000..7cedfa7 --- /dev/null +++ b/app/src/main/res/drawable/storage.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/tv_download.xml b/app/src/main/res/drawable/tv_download.xml new file mode 100644 index 0000000..3378c05 --- /dev/null +++ b/app/src/main/res/drawable/tv_download.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/tv_paste.xml b/app/src/main/res/drawable/tv_paste.xml new file mode 100644 index 0000000..4a798a0 --- /dev/null +++ b/app/src/main/res/drawable/tv_paste.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/version.xml b/app/src/main/res/drawable/version.xml new file mode 100644 index 0000000..6ee3199 --- /dev/null +++ b/app/src/main/res/drawable/version.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/font/poppins.ttf b/app/src/main/res/font/poppins.ttf new file mode 100644 index 0000000..246a861 Binary files /dev/null and b/app/src/main/res/font/poppins.ttf differ diff --git a/app/src/main/res/font/poppins_bold.ttf b/app/src/main/res/font/poppins_bold.ttf new file mode 100644 index 0000000..44313ca Binary files /dev/null and b/app/src/main/res/font/poppins_bold.ttf differ diff --git a/app/src/main/res/font/poppins_bold_italic.ttf b/app/src/main/res/font/poppins_bold_italic.ttf new file mode 100644 index 0000000..939fc7d Binary files /dev/null and b/app/src/main/res/font/poppins_bold_italic.ttf differ diff --git a/app/src/main/res/font/poppins_medium.ttf b/app/src/main/res/font/poppins_medium.ttf new file mode 100644 index 0000000..5b46f19 Binary files /dev/null and b/app/src/main/res/font/poppins_medium.ttf differ diff --git a/app/src/main/res/font/poppins_semibold.ttf b/app/src/main/res/font/poppins_semibold.ttf new file mode 100644 index 0000000..3bbad2a Binary files /dev/null and b/app/src/main/res/font/poppins_semibold.ttf differ diff --git a/app/src/main/res/font/poppins_semibold_italic.ttf b/app/src/main/res/font/poppins_semibold_italic.ttf new file mode 100644 index 0000000..74a7c43 Binary files /dev/null and b/app/src/main/res/font/poppins_semibold_italic.ttf differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..622c79b --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fetching_dialog.xml b/app/src/main/res/layout/fetching_dialog.xml new file mode 100644 index 0000000..d41e210 --- /dev/null +++ b/app/src/main/res/layout/fetching_dialog.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_app_settings.xml b/app/src/main/res/layout/fragment_app_settings.xml new file mode 100644 index 0000000..06dc0af --- /dev/null +++ b/app/src/main/res/layout/fragment_app_settings.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_download.xml b/app/src/main/res/layout/fragment_download.xml new file mode 100644 index 0000000..e3ee0c2 --- /dev/null +++ b/app/src/main/res/layout/fragment_download.xml @@ -0,0 +1,41 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..a53a205 --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,226 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +