diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml index f4859d01..b3a42253 100644 --- a/.github/workflows/makefile.yml +++ b/.github/workflows/makefile.yml @@ -2,9 +2,9 @@ name: Makefile CI (Test customfetch NOGUI) on: push: - branches: [ "main", "test", "windows" ] + branches: [ "main", "test", "android" ] pull_request: - branches: [ "main", "test", "windows" ] + branches: [ "main", "test", "android" ] jobs: build-deb: @@ -20,7 +20,7 @@ jobs: run: | sudo apt-get update sudo apt-get install libwayland-dev libgtk-3-dev pkg-config libgtkmm-3.0-dev libdconf-dev libglib2.0-dev libarchive-tools - sudo apt-get install binutils lintian debhelper devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y + sudo apt-get install binutils gettext lintian debhelper devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y - name: Clean run: make clean @@ -38,7 +38,7 @@ jobs: run: | cd /tmp/customfetch-0.10.2.orig sudo dpkg -i customfetch_0.10.2-1_amd64.deb - customfetch --wrap-lines=0 + customfetch --wrap-lines build_ubuntu-latest: @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Packages - run: sudo apt-get update && sudo apt-get install build-essential cmake neofetch tree libgtk-3-dev pkg-config libgtkmm-3.0-dev -y + run: sudo apt-get update && sudo apt-get install build-essential gettext cmake neofetch tree libgtk-3-dev pkg-config libgtkmm-3.0-dev -y - name: Clean run: make clean @@ -69,7 +69,7 @@ jobs: printf "getting 0x5353 hexcode\n" && grep -nri "5353" /sys/class/ || true - name: Test customfetch - run: ./build/debug/customfetch --wrap-lines=0 + run: ./build/debug/customfetch --wrap-lines build_Arch-AUR: @@ -82,11 +82,12 @@ jobs: - name: Install the packages run: pacman -Syyu git sudo base-devel fakeroot pkgconf tree neofetch --noconfirm --needed - - - name: get /etc/sudoers + + - name: Create a dedicated build user run: | - sed -i "s#root ALL=(ALL:ALL) ALL#root ALL=(ALL:ALL) NOPASSWD: ALL\nnobody ALL=(ALL:ALL) NOPASSWD: ALL#g" /etc/sudoers - cat /etc/sudoers + useradd -m -s /bin/bash builduser + echo "builduser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers + - name: get /etc/makepkg.conf run: | sed -i "s#purge debug lto#purge debug lto#" /etc/makepkg.conf @@ -95,9 +96,9 @@ jobs: - name: Build and install using makepkg run: | git clone https://aur.archlinux.org/customfetch-git.git - chown -R nobody customfetch-git + chown -R builduser customfetch-git cd customfetch-git - sudo -u nobody makepkg -si --noconfirm + sudo -u builduser makepkg -si --noconfirm #- name: Setup SSH session # uses: mxschmitt/action-tmate@v3 @@ -106,7 +107,7 @@ jobs: run: neofetch - name: Test customfetch - run: customfetch --wrap-lines=0 + run: customfetch --wrap-lines build_Arch: @@ -150,7 +151,29 @@ jobs: printf "/etc/os-release\n" && cat /etc/os-release - name: Test customfetch - run: ./build/debug/customfetch --wrap-lines=0 + run: ./build/debug/customfetch --wrap-lines + + build-android-app: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x android/gradlew + + - name: Build with Gradle + run: make distclean && make android_app + + - name: Check files + run: tree android/app/build/outputs/apk test-all-ascii-art: runs-on: ubuntu-latest @@ -159,7 +182,7 @@ jobs: - uses: actions/checkout@v4 - name: Install Packages - run: sudo apt-get update && sudo apt-get install build-essential tree pkg-config -y + run: sudo apt-get update && sudo apt-get install build-essential gettext tree pkg-config -y - name: Clean run: make clean @@ -167,7 +190,5 @@ jobs: - name: Compile run: sudo make install DEBUG=0 VENDOR_TEST=0 GUI_MODE=0 - # yes, i know too long - # the --color arguments are just for adding a missing color to the non done ascii - name: Test all the ascii art logos - run: for f in assets/ascii/*; do printf "\e[31m%s\e[0m\n" "$f" && customfetch --wrap-lines=0 -s "$f" -D assets --color "c1=!#fff111" --color "c2=!#00ff1a" --color "c3=!#faa311" --color "c4=!#343412" --color "c5=!#fff311" --color "c6=!#faa3aa" && sleep 1; done + run: for f in assets/ascii/*; do printf "\e[31m%s\e[0m\n" "$f" && customfetch --wrap-lines --source "$f" -D assets && sleep 1; done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2765f784..17f5f8f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,9 +28,8 @@ jobs: - name: Install Packages run: | sudo add-apt-repository ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt upgrade -y - sudo apt-get install build-essential g++-10 libwayland-dev pkg-config libarchive-tools libdconf-dev libglib2.0-dev -y + sudo apt-get update && sudo apt upgrade -y + sudo apt-get install build-essential g++-10 libwayland-dev gettext pkg-config libarchive-tools libdconf-dev libglib2.0-dev -y - name: Clean run: make clean @@ -56,9 +55,8 @@ jobs: - name: Install Packages run: | sudo add-apt-repository ppa:ubuntu-toolchain-r/test - sudo apt-get update - sudo apt upgrade -y - sudo apt-get install build-essential g++-10 libwayland-dev libgtk-3-dev pkg-config libdconf-dev libglib2.0-dev libgtkmm-3.0-dev libarchive-tools -y + sudo apt-get update && sudo apt upgrade -y + sudo apt-get install build-essential g++-10 libwayland-dev gettext libgtk-3-dev pkg-config libdconf-dev libglib2.0-dev libgtkmm-3.0-dev libarchive-tools -y - name: Clean run: make clean @@ -90,7 +88,7 @@ jobs: sudo apt-get update sudo apt-get upgrade -y sudo apt-get install libwayland-dev g++-10 libgtk-3-dev pkg-config libgtkmm-3.0-dev libdconf-dev libglib2.0-dev libarchive-tools - sudo apt-get install binutils lintian debhelper devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y + sudo apt-get install binutils gettext lintian debhelper devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y - name: Clean run: make clean @@ -127,7 +125,7 @@ jobs: sudo apt-get update sudo apt-get upgrade -y sudo apt-get install libwayland-dev g++-10 libgtk-3-dev pkg-config libgtkmm-3.0-dev libdconf-dev libglib2.0-dev libarchive-tools - sudo apt-get install binutils lintian debhelper devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y + sudo apt-get install binutils lintian debhelper gettext devscripts debmake autoconf automake autotools-dev dh-make fakeroot xutils pbuilder -y - name: Clean run: make clean @@ -149,10 +147,38 @@ jobs: with: name: customfetch-gui-deb-pkg path: /tmp/customfetch-0.10.2.orig/customfetch-gui_amd64.deb + + build-android-app: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x android/gradlew + + - name: Build with Gradle + run: make distclean && make android_app DEBUG=0 + + - name: Check files + run: tree android/app/build/outputs/apk + + - name: Upload to github artifacts + uses: actions/upload-artifact@v4 + with: + name: customfetch-android-app + path: android/app/build/outputs/apk/release/app-release.apk release: name: Create GitHub Release - needs: [build-tar, build-gui-tar, build-deb-gui, build-deb, get-version] + needs: [build-tar, build-gui-tar, build-deb-gui, build-deb, build-android-app, get-version] runs-on: ubuntu-latest permissions: contents: write @@ -217,3 +243,12 @@ jobs: asset_path: customfetch-gui-deb-pkg/customfetch-gui_amd64.deb asset_name: customfetch-gui_${{ needs.get-version.outputs.version }}_amd64.deb asset_content_type: application/vnd.debian.binary-package + + - uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.release.outputs.release-url }} + asset_path: customfetch-android-app/app-release.apk + asset_name: customfetch-android-app-${{ needs.get-version.outputs.version }}.apk + asset_content_type: application/vnd.android.package-archive diff --git a/.gitignore b/.gitignore index cf31f9b2..0a30bc79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,21 @@ -build/ +build*/ assets/distro_ascii_arts.json assets/distro_ascii_arts.txt cufetch test*.txt pci.ids output/ +useless_stuff/ customfetch customfetch_r cufetch_r -thinkpad-arch_logo.txt -tanjiro_logo.txt +locale/ scripts/ascii_parser.py scripts/dict_to_files.py scripts/test* *.tar.* +*.mo # Prerequisites *.d @@ -49,3 +50,4 @@ scripts/test* *.out *.app .cache +signing.properties diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..50e3e88b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,77 @@ +# Please use the makefile if something goes wrong +# + +cmake_minimum_required(VERSION 3.10) + +project(customfetch) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3 -O0 -DDEBUG=1 -Wall -Wextra -Wpedantic -Wno-unused-parameter") + +include_directories(include) + +file(GLOB + SRC + "src/*.cpp" + "src/query/unix/*.cpp" + "src/query/android/*.cpp" + "src/query/unix/utils/*.cpp" + "src/toml++/toml.cpp") + +add_executable(customfetch ${SRC}) + +# get git branch +execute_process( + COMMAND git rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +set(VERSION "0.10.2") +target_compile_definitions(customfetch PRIVATE + VERSION="${VERSION}" + BRANCH="${GIT_BRANCH}" +) + +# https://github.com/libcpr/cpr/blob/5f475522597b8f3721e2440daddeced7a969f24c/CMakeLists.txt#L39 +macro(add_option OPTION_NAME OPTION_TEXT OPTION_DEFAULT) + option(${OPTION_NAME} ${OPTION_TEXT} ${OPTION_DEFAULT}) + if(DEFINED ENV{${OPTION_NAME}}) + # Allow overriding the option through an environment variable + set(${OPTION_NAME} $ENV{${OPTION_NAME}}) + endif() + if(${OPTION_NAME}) + add_definitions(-D${OPTION_NAME}) + endif() + message(STATUS " ${OPTION_NAME}: ${${OPTION_NAME}}") +endmacro() + +message(STATUS "Set flags:") +message(STATUS "=================") +add_option(GUI_MODE "Enable GTK3 GUI mode" 0) +add_option(USE_DCONF "Compile customfetch with dconf support" 1) +#add_option(VARS "Add additional flags at CXXFLAGS" ) +message(STATUS "=================") + +if(GUI_MODE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(GTKMM REQUIRED gtkmm-3.0 gdkmm-3.0) + target_link_libraries(customfetch PUBLIC ${GTKMM_LIBRARIES}) + target_include_directories(customfetch PUBLIC ${GTKMM_INCLUDE_DIRS}) + target_compile_options(customfetch PUBLIC ${GTKMM_CFLAGS_OTHER}) +endif() + +if(USE_DCONF) + find_package(PkgConfig REQUIRED) + pkg_check_modules(DCONF dconf) + target_link_libraries(customfetch PUBLIC ${DCONF_LIBRARIES}) + target_include_directories(customfetch PUBLIC ${DCONF_INCLUDE_DIRS}) + target_compile_options(customfetch PUBLIC ${DCONF_CFLAGS_OTHER}) +endif() + + +# fmt +add_library(fmt STATIC + "src/fmt/os.cc" + "src/fmt/format.cc") + +target_link_libraries(customfetch PUBLIC fmt) diff --git a/Makefile b/Makefile index 880143f3..bdf5e5de 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,8 @@ CXX ?= g++ PREFIX ?= /usr MANPREFIX ?= $(PREFIX)/share/man APPPREFIX ?= $(PREFIX)/share/applications -VARS ?= +LOCALEDIR ?= $(PREFIX)/share/locale +VARS ?= -DENABLE_NLS=1 DEBUG ?= 1 GUI_MODE ?= 0 @@ -52,11 +53,11 @@ TARGET = $(NAME) OLDVERSION = 0.10.1 VERSION = 0.10.2 BRANCH = $(shell git rev-parse --abbrev-ref HEAD) -SRC = $(wildcard src/*.cpp src/query/unix/*.cpp src/query/unix/utils/*.cpp) +SRC = $(wildcard src/*.cpp src/query/unix/*.cpp src/query/android/*.cpp src/query/unix/utils/*.cpp) OBJ = $(SRC:.cpp=.o) LDFLAGS += -L./$(BUILDDIR)/fmt -lfmt -ldl CXXFLAGS ?= -mtune=generic -march=native -CXXFLAGS += -fvisibility=hidden -Iinclude -std=c++20 $(VARS) -DVERSION=\"$(VERSION)\" -DBRANCH=\"$(BRANCH)\" +CXXFLAGS += -fvisibility=hidden -Iinclude -std=c++20 $(VARS) -DVERSION=\"$(VERSION)\" -DBRANCH=\"$(BRANCH)\" -DLOCALEDIR=\"$(LOCALEDIR)\" all: fmt toml $(TARGET) @@ -77,15 +78,28 @@ $(TARGET): fmt toml $(OBJ) $(CXX) $(OBJ) $(BUILDDIR)/toml++/toml.o -o $(BUILDDIR)/$(TARGET) $(LDFLAGS) cd $(BUILDDIR)/ && ln -sf $(TARGET) cufetch -dist: +android_app: +ifeq ($(DEBUG), 1) + ./android/gradlew assembleDebug --project-dir=./android +else + ./android/gradlew assembleRelease --project-dir=./android +endif + @if [ $$? -eq 0 ]; then\ + echo "APK build successfully. Get it in $(CURDIR)/android/app/build/outputs/apk path and choose which to install (debug/release)";\ + fi + +locale: + scripts/make_mo.sh locale/ + +dist: locale ifeq ($(GUI_MODE), 1) - bsdtar -zcf $(NAME)-v$(VERSION).tar.gz LICENSE $(TARGET).desktop $(TARGET).1 assets/ascii/ -C $(BUILDDIR) $(TARGET) + bsdtar -zcf $(NAME)-v$(VERSION).tar.gz LICENSE $(TARGET).desktop locale/ $(TARGET).1 assets/ascii/ -C $(BUILDDIR) $(TARGET) else - bsdtar -zcf $(NAME)-v$(VERSION).tar.gz LICENSE $(TARGET).1 assets/ascii/ -C $(BUILDDIR) $(TARGET) + bsdtar -zcf $(NAME)-v$(VERSION).tar.gz LICENSE $(TARGET).1 locale/ assets/ascii/ -C $(BUILDDIR) $(TARGET) endif clean: - rm -rf $(BUILDDIR)/$(TARGET) $(OBJ) + rm -rf $(BUILDDIR)/$(TARGET) $(BUILDDIR)/libcustomfetch.a $(OBJ) distclean: rm -rf $(BUILDDIR) ./tests/$(BUILDDIR) $(OBJ) @@ -93,13 +107,14 @@ distclean: find . -type f -name "*.o" -exec rm -rf "{}" \; find . -type f -name "*.a" -exec rm -rf "{}" \; -install: $(TARGET) +install: $(TARGET) locale install $(BUILDDIR)/$(TARGET) -Dm 755 -v $(DESTDIR)$(PREFIX)/bin/$(TARGET) cd $(DESTDIR)$(PREFIX)/bin/ && ln -sf $(TARGET) cufetch mkdir -p $(DESTDIR)$(MANPREFIX)/man1/ sed -e "s/@VERSION@/$(VERSION)/g" -e "s/@BRANCH@/$(BRANCH)/g" < $(TARGET).1 > $(DESTDIR)$(MANPREFIX)/man1/$(TARGET).1 chmod 644 $(DESTDIR)$(MANPREFIX)/man1/$(TARGET).1 cd assets/ && find ascii/ -type f -exec install -Dm 644 "{}" "$(DESTDIR)$(PREFIX)/share/customfetch/{}" \; + find locale/ -type f -exec install -Dm 755 "{}" "$(DESTDIR)$(PREFIX)/share/{}" \; ifeq ($(GUI_MODE), 1) mkdir -p $(DESTDIR)$(APPPREFIX) cp -f $(TARGET).desktop $(DESTDIR)$(APPPREFIX)/$(TARGET).desktop @@ -117,4 +132,4 @@ delete: uninstall updatever: sed -i "s#$(OLDVERSION)#$(VERSION)#g" $(wildcard .github/workflows/*.yml) compile_flags.txt -.PHONY: $(TARGET) updatever remove uninstall delete dist distclean fmt toml install all +.PHONY: $(TARGET) updatever remove uninstall delete dist distclean fmt toml install all locale diff --git a/README.md b/README.md index ac5e22e2..f822d440 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- A system information fetch tool (or neofetch like program), which its focus point is the perfomance and customizability + A system information fetch tool (or neofetch like program), which its focus point is the performance and customizability

@@ -30,12 +30,20 @@

+

+ It's even an android widget and GTK3 app +

+ + + +

+ +

## Key Features - -* **GUI mode (GTK3)**: Run customfetch even as a GUI application +* Run customfetch as a **terminal** or **GTK3 application** or even as an **android widget** * Really easy customizable and fast, check [Config (with explanation)](#config-with-explanation) section -* Super lightweight, 3.1MB max +* Super lightweight, 3.1MB max (compiled as terminal and GTK3 app for desktop) ## Depends currently requires **C++20**, but it's possible to compile with C++17 too (not officially supported) @@ -94,6 +102,14 @@ yay -S customfetch-git yay -S customfetch-gui-git ``` +## Android app (source) +```bash +# need java 17 + gradle 8.9 installed. +# It's suggested to build from android studio, +# so you that you can install the NDK library +./android/gradlew assembleDebug --project-dir=./android +``` + ### Compile from (source) (unstable) ```bash # clone the git dir diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..aa724b77 --- /dev/null +++ b/android/.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/android/.idea/.gitignore b/android/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/android/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/android/.idea/.name b/android/.idea/.name new file mode 100644 index 00000000..f42fe1a8 --- /dev/null +++ b/android/.idea/.name @@ -0,0 +1 @@ +customfetch_android \ No newline at end of file diff --git a/android/.idea/codeStyles/Project.xml b/android/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..a27d1e9c --- /dev/null +++ b/android/.idea/codeStyles/Project.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/android/.idea/codeStyles/codeStyleConfig.xml b/android/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/android/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/android/.idea/compiler.xml b/android/.idea/compiler.xml new file mode 100644 index 00000000..b86273d9 --- /dev/null +++ b/android/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/deploymentTargetSelector.xml b/android/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..b268ef36 --- /dev/null +++ b/android/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/android/.idea/gradle.xml b/android/.idea/gradle.xml new file mode 100644 index 00000000..7b3006b6 --- /dev/null +++ b/android/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + + \ No newline at end of file diff --git a/android/.idea/kotlinc.xml b/android/.idea/kotlinc.xml new file mode 100644 index 00000000..148fdd24 --- /dev/null +++ b/android/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/android/.idea/migrations.xml b/android/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/android/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/misc.xml b/android/.idea/misc.xml new file mode 100644 index 00000000..b2c751a3 --- /dev/null +++ b/android/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/android/.idea/runConfigurations.xml b/android/.idea/runConfigurations.xml new file mode 100644 index 00000000..16660f1d --- /dev/null +++ b/android/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/android/.idea/vcs.xml b/android/.idea/vcs.xml new file mode 100644 index 00000000..6c0b8635 --- /dev/null +++ b/android/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt new file mode 100644 index 00000000..2d98a764 --- /dev/null +++ b/android/CMakeLists.txt @@ -0,0 +1,43 @@ +# Same file as the one before, just used to be compiled for android + +cmake_minimum_required(VERSION 3.10) + +project(customfetch) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3 -O0 -DDEBUG=1 -Wall -Wextra -Wpedantic -Wno-unused-parameter") + +include_directories(${CMAKE_SOURCE_DIR}/../include) + +file(GLOB + SRC + "${CMAKE_SOURCE_DIR}/../src/*.cpp" + "${CMAKE_SOURCE_DIR}/../src/query/unix/*.cpp" + "${CMAKE_SOURCE_DIR}/../src/query/android/*.cpp" + "${CMAKE_SOURCE_DIR}/../src/query/unix/utils/*.cpp" + "${CMAKE_SOURCE_DIR}/../src/toml++/toml.cpp" + "app/src/main/cpp/customfetch_android.cpp" +) + +add_library(customfetch SHARED ${SRC}) + +# get git branch +execute_process( + COMMAND git rev-parse --abbrev-ref HEAD + OUTPUT_VARIABLE GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE +) + +set(VERSION "0.10.2") +target_compile_definitions(customfetch PRIVATE + VERSION="${VERSION}" + BRANCH="${GIT_BRANCH}" + DANDROID_APP=1 +) + +# fmt +add_library(fmt STATIC + "${CMAKE_SOURCE_DIR}/../src/fmt/os.cc" + "${CMAKE_SOURCE_DIR}/../src/fmt/format.cc") + +# add library +target_link_libraries(customfetch fmt log android) diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 00000000..c4abc0b5 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,87 @@ +import java.util.Properties + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "org.toni.customfetch_android" + compileSdk = 35 + + defaultConfig { + applicationId = "org.toni.customfetch_android" + minSdk = 26 + targetSdk = 35 + versionCode = 1 + versionName = "0.10.2" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + externalNativeBuild { + cmake { + cppFlags += "-I${rootDir}/../include -DANDROID_APP=1" + targets("customfetch") + } + } + } + + signingConfigs { + create("release") { + val propertiesFile = rootProject.file("signing.properties") + if (propertiesFile.exists()) { + val properties = Properties().apply { + load(propertiesFile.reader()) + } + storeFile = File(properties.getProperty("storeFilePath")) + storePassword = properties.getProperty("storePassword") + keyPassword = properties.getProperty("keyPassword") + keyAlias = properties.getProperty("keyAlias") + } + } + } + + buildTypes { + release { + getByName("release") { + signingConfig = signingConfigs.getByName("release") + } + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = "11" + } + buildFeatures { + viewBinding = true + } + externalNativeBuild { + cmake { + path = file("../CMakeLists.txt") + version = "3.22.1" + } + } +} + +dependencies { + implementation(libs.colorpickerview) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.material) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.preference.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/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/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..16ea7d90 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/assets b/android/app/src/main/assets new file mode 120000 index 00000000..2b56518e --- /dev/null +++ b/android/app/src/main/assets @@ -0,0 +1 @@ +../../../../assets \ No newline at end of file diff --git a/android/app/src/main/cpp/customfetch_android.cpp b/android/app/src/main/cpp/customfetch_android.cpp new file mode 100644 index 00000000..6bc4ad4d --- /dev/null +++ b/android/app/src/main/cpp/customfetch_android.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include "util.hpp" + +#define ARRAY_SIZE(x) sizeof(x) / sizeof(x[0]) + +extern std::string mainAndroid_and_render(int argc, char *argv[], JNIEnv *env, jobject obj, bool do_not_load_config); + +extern "C" JNIEXPORT jstring JNICALL +Java_org_toni_customfetch_1android_widget_CustomfetchMainRender_mainAndroid(JNIEnv *env, jobject obj, jstring args, jboolean do_not_load_config) { + const std::string& str_args = env->GetStringUTFChars(args, nullptr); + const std::vector& tokens = split(str_args, ' '); + char *argv[tokens.size()]; + for (size_t i = 0; i < tokens.size(); ++i) + argv[i] = strdup(tokens[i].c_str()); + + return env->NewStringUTF(mainAndroid_and_render(ARRAY_SIZE(argv), argv, env, obj, do_not_load_config).c_str()); +} \ No newline at end of file diff --git a/android/app/src/main/java/org/toni/customfetch_android/AboutMeFragment.kt b/android/app/src/main/java/org/toni/customfetch_android/AboutMeFragment.kt new file mode 100644 index 00000000..e0b78116 --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/AboutMeFragment.kt @@ -0,0 +1,44 @@ +package org.toni.customfetch_android + +import android.os.Bundle +import androidx.core.text.HtmlCompat +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import org.toni.customfetch_android.databinding.AboutMeFragmentBinding + +class AboutMeFragment : Fragment() { + + private var _binding: AboutMeFragmentBinding? = null + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = AboutMeFragmentBinding.inflate(inflater, container, false) + binding.toolbar.apply { + setNavigationIcon(R.drawable.arrow_back) + setNavigationOnClickListener { _ -> + requireActivity().supportFragmentManager.popBackStack() + } + } + + binding.burntGithubLink.movementMethod = LinkMovementMethod.getInstance() + binding.toniGithubLink.movementMethod = LinkMovementMethod.getInstance() + binding.bcppDiscordLink.movementMethod = LinkMovementMethod.getInstance() + binding.fmtlibGithubLink.movementMethod = LinkMovementMethod.getInstance() + binding.tomlLibGithubLink.movementMethod = LinkMovementMethod.getInstance() + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/org/toni/customfetch_android/MainActivity.kt b/android/app/src/main/java/org/toni/customfetch_android/MainActivity.kt new file mode 100644 index 00000000..59c8587d --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/MainActivity.kt @@ -0,0 +1,151 @@ +package org.toni.customfetch_android + +import android.content.Intent +import android.content.res.AssetManager +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.FragmentTransaction +import org.toni.customfetch_android.databinding.ActivityMainBinding +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.nio.file.Files +import kotlin.io.path.Path + +// kinda magic numbers +const val TEST_CONFIG_FILE_RC = 2 + +class MainActivity : AppCompatActivity() { + private lateinit var binding: ActivityMainBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + val alert = AlertDialog.Builder(this) + .setTitle("Grant external storage management permission") + .setMessage("Customfetch needs permissions to manage external storage to able to access config files.\n"+ + "By default we going to read/write the following directories:\n"+ + "/storage/emulated/0/.config/\n"+ + "/storage/emulated/0/.config/customfetch/") + .setPositiveButton("Grant permission" + ) { _, _ -> + val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION) + startActivity(intent) + } + .setIcon(R.drawable.icon_alert_yellow) + + val view: View = layoutInflater.inflate(R.layout.grant_perm, null, false) + alert.setView(view) + alert.show() + } + } + + if (!Files.exists(Path(filesDir.absolutePath + "/ascii"))) + copyToAssetFolder(assets, filesDir.absolutePath, "ascii") + + binding.discordLink.movementMethod = LinkMovementMethod.getInstance() + binding.redditLink.movementMethod = LinkMovementMethod.getInstance() + + binding.testConfigFile.setOnClickListener { _ -> + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "*/*" + } + startActivityForResult(intent, TEST_CONFIG_FILE_RC) + } + + binding.aboutMe.setOnClickListener { _ -> + val fragment = AboutMeFragment() + val transaction: FragmentTransaction = + supportFragmentManager.beginTransaction() + transaction.replace(android.R.id.content, fragment) + transaction.addToBackStack(null).commit() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (resultCode == RESULT_OK && data != null && data.data != null) { + data.data?.let { uri -> + when(requestCode) { + TEST_CONFIG_FILE_RC -> { + val fragment = TestConfigFragment().apply { + configFile = PathUtil.getPath(this@MainActivity, uri) + } + val transaction: FragmentTransaction = + supportFragmentManager.beginTransaction() + transaction.replace(android.R.id.content, fragment) + transaction.addToBackStack(null).commit() + } + else -> {} + } + } + } + } +} + +const val TAG: String = "AssetCopy" + +internal fun copyToAssetFolder(assets: AssetManager, absolutePath: String, assetSubFolder: String) { + try { + copyDirectory(assets, assetSubFolder, absolutePath) + Log.d(TAG, "All files copied to: $absolutePath") + } catch (e: IOException) { + Log.e(TAG, "Failed to copy asset folder: $assetSubFolder", e) + } +} + +@Throws(IOException::class) +private fun copyDirectory( + assetManager: AssetManager, + sourceDir: String, + destinationDir: String +) { + val files = assetManager.list(sourceDir) + if (files.isNullOrEmpty()) + return + + val destDir = File(destinationDir, sourceDir) + if (!destDir.exists() && !destDir.mkdirs()) + throw IOException("Failed to create directory: ${destDir.absolutePath}") + + for (fileName in files) { + val assetPath = "$sourceDir/$fileName" + val destPath = destDir.path + "/" + fileName + if (isDirectory(assetManager, assetPath)) + copyDirectory(assetManager, assetPath, destinationDir) + else + copyFile(assetManager, assetPath, destPath) + } +} + +@Throws(IOException::class) +private fun isDirectory(assetManager: AssetManager, path: String): Boolean { + val files = assetManager.list(path) + return !files.isNullOrEmpty() +} + +@Throws(IOException::class) +private fun copyFile(assetManager: AssetManager, assetPath: String, destPath: String) { + assetManager.open(assetPath).use { `in` -> + FileOutputStream(destPath).use { out -> + val buffer = ByteArray(8192) + var bytesRead: Int + while ((`in`.read(buffer).also { bytesRead = it }) != -1) + out.write(buffer, 0, bytesRead) + + Log.d(TAG, "File copied: $destPath") + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/org/toni/customfetch_android/PathUtil.java b/android/app/src/main/java/org/toni/customfetch_android/PathUtil.java new file mode 100644 index 00000000..84337abb --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/PathUtil.java @@ -0,0 +1,98 @@ +package org.toni.customfetch_android; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import java.net.URISyntaxException; + +/** + * Created by Aki on 1/7/2017. + * original + */ + +public class PathUtil { + /* + * Gets the file path of the given Uri. + */ + @SuppressLint("NewApi") + public static String getPath(Context context, Uri uri) throws URISyntaxException { + final boolean needToCheckUri = Build.VERSION.SDK_INT >= 19; + String selection = null; + String[] selectionArgs = null; + // Uri is different in versions after KITKAT (Android 4.4), we need to + // deal with different Uris. + if (needToCheckUri && DocumentsContract.isDocumentUri(context.getApplicationContext(), uri)) { + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } else if (isDownloadsDocument(uri)) { + final String id = DocumentsContract.getDocumentId(uri); + uri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + } else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + if ("image".equals(type)) { + uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + selection = "_id=?"; + selectionArgs = new String[]{ split[1] }; + } + } + if ("content".equalsIgnoreCase(uri.getScheme())) { + String[] projection = { MediaStore.Images.Media.DATA }; + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); + assert cursor != null; + int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + if (cursor.moveToFirst()) { + return cursor.getString(column_index); + } + cursor.close(); + } catch (Exception e) { + } + } else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + return null; + } + + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/org/toni/customfetch_android/TestConfigFragment.kt b/android/app/src/main/java/org/toni/customfetch_android/TestConfigFragment.kt new file mode 100644 index 00000000..951d9d5a --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/TestConfigFragment.kt @@ -0,0 +1,66 @@ +package org.toni.customfetch_android + +import android.content.res.ColorStateList +import android.graphics.Color +import android.os.Bundle +import android.text.SpannableString +import android.text.TextPaint +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.HtmlCompat +import androidx.fragment.app.Fragment +import org.toni.customfetch_android.databinding.TestConfigFragmentBinding +import org.toni.customfetch_android.widget.customfetchRender + + +class TestConfigFragment : Fragment() { + + private var _binding: TestConfigFragmentBinding? = null + // This property is only valid between onCreateView and + // onDestroyView. + private val binding get() = _binding!! + + var configFile = "" + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = TestConfigFragmentBinding.inflate(inflater, container, false) + binding.toolbar.apply { + setNavigationIcon(R.drawable.arrow_back) + setNavigationOnClickListener { _ -> + requireActivity().supportFragmentManager.popBackStack() + } + } + + val result = customfetchRender.getParsedContent( + AppCompatActivity(), + 0, + 0f, + false, + TextPaint(), + "-C $configFile -Nnm \$", // this is the important thing + false + ) + + if (result.contentEquals("android")) { + binding.titleResult.setTextColor(Color.GREEN) + binding.titleResult.text = "SUCCESS" + binding.testConfigResult.text = "config file '$configFile' works!!" + } else { + binding.titleResult.setTextColor(Color.RED) + binding.titleResult.text = "FAILURE" + binding.testConfigResult.text = result + } + + return binding.root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file diff --git a/android/app/src/main/java/org/toni/customfetch_android/widget/customfetch.kt b/android/app/src/main/java/org/toni/customfetch_android/widget/customfetch.kt new file mode 100644 index 00000000..11638e11 --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/widget/customfetch.kt @@ -0,0 +1,133 @@ +package org.toni.customfetch_android.widget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.os.Bundle +import android.text.TextPaint +import android.util.Log +import android.widget.RemoteViews +import org.toni.customfetch_android.R + +/** + * Implementation of App Widget functionality. + * App Widget Configuration implemented in [customfetchConfigureActivity] + */ +class customfetch : AppWidgetProvider() { + var firstFun = true + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + // There may be multiple widgets active, so update all of them + if (!firstFun) + for (appWidgetId in appWidgetIds) + updateAppWidget(context, appWidgetManager, appWidgetId) + firstFun = false + } + + override fun onDeleted(context: Context, appWidgetIds: IntArray) { + // When the user deletes the widget, delete the preference associated with it. + for (appWidgetId in appWidgetIds) { + deleteConfigPrefs(context, appWidgetId) + } + } + + override fun onAppWidgetOptionsChanged( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int, + newOptions: Bundle + ) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } +} + +// https://stackoverflow.com/a/58501760 +class WidgetSizeProvider( + private val context: Context // Do not pass Application context +) { + + private val appWidgetManager = AppWidgetManager.getInstance(context) + + fun getWidgetsSize(widgetId: Int): Pair { + val isPortrait = (context.resources.configuration.orientation == ORIENTATION_PORTRAIT) + val width = getWidgetWidth(isPortrait, widgetId) + val height = getWidgetHeight(isPortrait, widgetId) + val widthInPx = context.dip(width) + val heightInPx = context.dip(height) + return widthInPx to heightInPx + } + + private fun getWidgetWidth(isPortrait: Boolean, widgetId: Int): Int = + if (isPortrait) { + getWidgetSizeInDp(widgetId, AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH) + } else { + getWidgetSizeInDp(widgetId, AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH) + } + + private fun getWidgetHeight(isPortrait: Boolean, widgetId: Int): Int = + if (isPortrait) { + getWidgetSizeInDp(widgetId, AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT) + } else { + getWidgetSizeInDp(widgetId, AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) + } + + private fun getWidgetSizeInDp(widgetId: Int, key: String): Int = + appWidgetManager.getAppWidgetOptions(widgetId).getInt(key, 0) + + private fun Context.dip(value: Int): Int = (value * resources.displayMetrics.density).toInt() + +} + +internal fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + +) { + val disableLineWrap = getDisableLineWrap(context, appWidgetId) + val bgColor = getBgColor(context, appWidgetId) + + // create a TextPaint to be used to measure text size + val textPaint = TextPaint() + val textSizeSp = 8f + val textSizePx = textSizeSp * context.resources.displayMetrics.scaledDensity + textPaint.textSize = textSizePx + + val additionalTruncateWidth = getTruncateWidthPref(context, appWidgetId).toFloat() + var width = WidgetSizeProvider(context).getWidgetsSize(appWidgetId).first.toFloat() + if (additionalTruncateWidth > 0.20) + width *= additionalTruncateWidth + + Log.d("widthTesting", "textSizePx = $textSizePx") + Log.d("widthTesting", "width = $width") + + val parsedContent = + customfetchRender.getParsedContent( + context, + appWidgetId, + width, + disableLineWrap, + textPaint + ) + + // Construct the RemoteViews object + val views = RemoteViews(context.packageName, R.layout.customfetch) + views.setTextViewText(R.id.customfetch_text, parsedContent) + views.setInt(R.id.widget_root, "setBackgroundColor", bgColor); + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views) +} diff --git a/android/app/src/main/java/org/toni/customfetch_android/widget/customfetchConfigureActivity.kt b/android/app/src/main/java/org/toni/customfetch_android/widget/customfetchConfigureActivity.kt new file mode 100644 index 00000000..4e36c626 --- /dev/null +++ b/android/app/src/main/java/org/toni/customfetch_android/widget/customfetchConfigureActivity.kt @@ -0,0 +1,288 @@ +package org.toni.customfetch_android.widget + +import android.annotation.SuppressLint +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.Editable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.TextUtils.TruncateAt +import android.text.TextUtils.ellipsize +import android.text.TextWatcher +import android.util.TypedValue +import android.view.View +import android.widget.CheckBox +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioGroup +import android.widget.TextView +import android.widget.Toast +import androidx.core.graphics.toColorInt +import androidx.core.text.HtmlCompat +import com.skydoves.colorpickerview.ColorPickerView +import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener +import com.skydoves.colorpickerview.sliders.AlphaSlideBar +import com.skydoves.colorpickerview.sliders.BrightnessSlideBar +import org.toni.customfetch_android.R +import org.toni.customfetch_android.databinding.CustomfetchConfigureBinding +import java.io.File +import java.nio.file.Files +import kotlin.io.path.Path + + +/** + * The configuration screen for the [customfetch] AppWidget. + */ +class customfetchConfigureActivity : Activity() { + private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID + private lateinit var binding: CustomfetchConfigureBinding + private var onAddWidget = View.OnClickListener { + val context = this@customfetchConfigureActivity + + // When the button is clicked, store the string locally + saveConfigPrefs( + context, + appWidgetId, + binding.argumentsConfigure.text.toString(), + binding.additionalTruncateWidth.text.toString(), + disableLineWrap, + bgColor) + + // It is the responsibility of the configuration activity to update the app widget + val appWidgetManager = AppWidgetManager.getInstance(context) + updateAppWidget(context, appWidgetManager, appWidgetId) + + // Make sure we pass back the original appWidgetId + val resultValue = Intent() + resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + setResult(RESULT_OK, resultValue) + finish() + } + + // truncate text + private var disableLineWrap = false + private var bgColor = 0 + + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") + public override fun onCreate(icicle: Bundle?) { + super.onCreate(icicle) + + // Set the result to CANCELED. This will cause the widget host to cancel + // out of the widget placement if the user presses the back button. + setResult(RESULT_CANCELED) + + binding = CustomfetchConfigureBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.addButton.setOnClickListener(onAddWidget) + + // Find the widget id from the intent. + val extras = intent.extras + if (extras != null) { + appWidgetId = extras.getInt( + AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID + ) + } + + // If this activity was started with an intent without an app widget ID, finish with an error. + if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { + finish() + return + } + + binding.argumentsConfigure.setText(getArgsPref(this@customfetchConfigureActivity, appWidgetId)) + binding.additionalTruncateWidth.setText("0.6") + binding.argsHelp.text = customfetchRender.mainAndroid("customfetch --help", true) + + binding.showModulesList.setOnCheckedChangeListener { _, isChecked -> + binding.argsHelp.text = customfetchRender.mainAndroid("customfetch ${if (isChecked) "-l" else "-h"}", true) + } + + binding.disableWrapLinesCheck.setOnCheckedChangeListener { _, isChecked -> + disableLineWrap = isChecked + } + + binding.selectBgColor.setOnCheckedChangeListener { _, checkedId -> + when (checkedId) { + R.id.radio_system_bg_color -> { + binding.customColorSelect.visibility = View.GONE + val typedValue = TypedValue() + this.theme.resolveAttribute(android.R.attr.colorBackground, typedValue, true) + bgColor = typedValue.data + } + + R.id.radio_transparent_bg -> { + binding.customColorSelect.visibility = View.GONE + bgColor = 0x00FFFFFF + } + + R.id.radio_custom_colors -> { + binding.customColorSelect.visibility = View.VISIBLE + // disable scroll when interacting with the color picker + binding.colorPickerView.setOnTouchListener { view, _ -> + view.parent.requestDisallowInterceptTouchEvent(true) + false // allow colorPickerView to handle the touch event + } + + // if modified edittext and it's valid, apply to the preview + // else if modified in the color picker, apply to the edittext + var hexColor = "" + binding.colorPickerHex.addTextChangedListener (object : TextWatcher { + override fun afterTextChanged(s: Editable) { + val col = s.toString() + if (isValidHex(col)) { + binding.colorPreview.setBackgroundColor(Color.parseColor(col)) + binding.colorPickerView.setInitialColor(col.toColorInt()) + hexColor = col + bgColor = col.toColorInt() + } + } + override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} + }) + var firstRun = true + binding.colorPickerView.setColorListener(ColorEnvelopeListener { envelope, _ -> + if (firstRun) + hexColor = "#${envelope.hexCode}" + + if (hexColor != "#${envelope.hexCode}") { + binding.colorPickerHex.setText("#${envelope.hexCode}") + hexColor = "#${envelope.hexCode}" + } + binding.colorPreview.setBackgroundColor(envelope.color) + bgColor = envelope.color + firstRun = false + }) + binding.colorPickerView.attachAlphaSlider(binding.alphaSlideBar) + binding.colorPickerView.attachBrightnessSlider(binding.brightnessSlideBar) + } + } + } + } + + private fun isValidHex(color: String): Boolean = + color.matches("^#[0-9A-Fa-f]{8}$".toRegex()) +} + +class CustomfetchMainRender { + fun getParsedContent( + context: Context, + appWidgetId: Int, + width: Float, + disableLineWrap: Boolean, + paint: TextPaint, + otherArguments: String = "", + postToast: Boolean = true, + doNotLoadConfig: Boolean = false + ): SpannableStringBuilder { + val parsedContent = SpannableStringBuilder() + val arguments = otherArguments.ifEmpty { + getArgsPref(context, appWidgetId) + } + val htmlContent = mainAndroid("customfetch $arguments", doNotLoadConfig) + + val errorFile = "/storage/emulated/0/.config/customfetch/error_log.txt" + val errorLock = "/storage/emulated/0/.config/customfetch/error.lock" + if (Files.exists(Path(errorLock))) { + val file = File(errorLock) + val error = file.bufferedReader().use { it.readText() } + if (postToast) { + val handler = Handler(Looper.getMainLooper()) + handler.post { + Toast.makeText(context, error, Toast.LENGTH_LONG).show() + } + handler.post { + Toast.makeText(context, "read error logs at $errorFile", Toast.LENGTH_LONG) + .show() + } + } + file.delete() + parsedContent.append("read error logs at $errorFile\n\n$error") + return parsedContent + } + + if (disableLineWrap) { + val eachLine = htmlContent!!.split("
").map { it.trim() } + for (line in eachLine) { + var parsedLine = HtmlCompat.fromHtml(line, HtmlCompat.FROM_HTML_MODE_COMPACT) + parsedLine = + ellipsize(parsedLine, paint, width, TruncateAt.END) as Spanned + parsedContent.appendLine(parsedLine) + } + } else { + parsedContent.append(htmlContent?.let { + HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT) + }) + } + + return parsedContent + } + + external fun mainAndroid(argv: String, doNotLoadConfig: Boolean): String? + companion object { + init { + System.loadLibrary("customfetch") + } + } +} +val customfetchRender = CustomfetchMainRender() + +private const val PREFS_NAME = "org.toni.customfetch_android.customfetch" +private const val PREF_PREFIX_KEY = "appwidget_" + +// Save the preferences to the SharedPreferences object for this widget +internal fun saveConfigPrefs( + context: Context, + appWidgetId: Int, + args: String, + truncateWidth: String, + disableLineWrap: Boolean, + bgColor: Int +) { + val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() + prefs.putString(PREF_PREFIX_KEY + appWidgetId + "_args", args) + prefs.putString(PREF_PREFIX_KEY + appWidgetId + "_truncateWidth", truncateWidth) + prefs.putBoolean(PREF_PREFIX_KEY + appWidgetId + "_disableLineWrap", disableLineWrap) + prefs.putInt(PREF_PREFIX_KEY + appWidgetId + "_bgColor", bgColor) + prefs.apply() +} + +internal fun getArgsPref(context: Context, appWidgetId: Int): String { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val args = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_args", null) + return args ?: "-D ${context.filesDir.absolutePath} -a small" +} + +internal fun getDisableLineWrap(context: Context, appWidgetId: Int): Boolean { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val value = prefs.getBoolean(PREF_PREFIX_KEY + appWidgetId + "_disableLineWrap", false) + return value +} + +internal fun getBgColor(context: Context, appWidgetId: Int): Int { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val value = prefs.getInt(PREF_PREFIX_KEY + appWidgetId + "_bgColor", 0) + return value +} + +internal fun getTruncateWidthPref(context: Context, appWidgetId: Int): String { + val prefs = context.getSharedPreferences(PREFS_NAME, 0) + val value = prefs.getString(PREF_PREFIX_KEY + appWidgetId + "_truncateWidth", null) + return value ?: "0" +} + +internal fun deleteConfigPrefs(context: Context, appWidgetId: Int) { + val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit() + prefs.remove(PREF_PREFIX_KEY + appWidgetId + "_args") + prefs.remove(PREF_PREFIX_KEY + appWidgetId + "_disableLineWrap") + prefs.remove(PREF_PREFIX_KEY + appWidgetId + "_truncateWidth") + prefs.remove(PREF_PREFIX_KEY + appWidgetId + "_bgColor") + prefs.apply() +} diff --git a/android/app/src/main/res/drawable-night/grant_perm.png b/android/app/src/main/res/drawable-night/grant_perm.png new file mode 100644 index 00000000..a8740eab Binary files /dev/null and b/android/app/src/main/res/drawable-night/grant_perm.png differ diff --git a/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png new file mode 100644 index 00000000..429c58b2 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/example_appwidget_preview.png differ diff --git a/android/app/src/main/res/drawable-nodpi/grant_perm.png b/android/app/src/main/res/drawable-nodpi/grant_perm.png new file mode 100644 index 00000000..f6732a37 Binary files /dev/null and b/android/app/src/main/res/drawable-nodpi/grant_perm.png differ diff --git a/android/app/src/main/res/drawable-v21/app_widget_background.xml b/android/app/src/main/res/drawable-v21/app_widget_background.xml new file mode 100644 index 00000000..785445c6 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml new file mode 100644 index 00000000..007e2872 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/app_widget_inner_view_background.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/arrow_back.xml b/android/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 00000000..af7a5657 --- /dev/null +++ b/android/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/better_cpp_discord_pfp.png b/android/app/src/main/res/drawable/better_cpp_discord_pfp.png new file mode 100644 index 00000000..9dcbf538 Binary files /dev/null and b/android/app/src/main/res/drawable/better_cpp_discord_pfp.png differ diff --git a/android/app/src/main/res/drawable/burnt_github_pfp.png b/android/app/src/main/res/drawable/burnt_github_pfp.png new file mode 100644 index 00000000..0d5db231 Binary files /dev/null and b/android/app/src/main/res/drawable/burnt_github_pfp.png differ diff --git a/android/app/src/main/res/drawable/discord_mark_blue.png b/android/app/src/main/res/drawable/discord_mark_blue.png new file mode 100644 index 00000000..e9dc50d7 Binary files /dev/null and b/android/app/src/main/res/drawable/discord_mark_blue.png differ diff --git a/android/app/src/main/res/drawable/fmtlib_logo.png b/android/app/src/main/res/drawable/fmtlib_logo.png new file mode 100644 index 00000000..19c66274 Binary files /dev/null and b/android/app/src/main/res/drawable/fmtlib_logo.png differ diff --git a/android/app/src/main/res/drawable/github_mark.png b/android/app/src/main/res/drawable/github_mark.png new file mode 100644 index 00000000..6cb3b705 Binary files /dev/null and b/android/app/src/main/res/drawable/github_mark.png differ diff --git a/android/app/src/main/res/drawable/github_mark_white.png b/android/app/src/main/res/drawable/github_mark_white.png new file mode 100644 index 00000000..50b81752 Binary files /dev/null and b/android/app/src/main/res/drawable/github_mark_white.png differ diff --git a/android/app/src/main/res/drawable/github_pfp.png b/android/app/src/main/res/drawable/github_pfp.png new file mode 100644 index 00000000..bca86976 Binary files /dev/null and b/android/app/src/main/res/drawable/github_pfp.png differ diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/icon_alert_yellow.xml b/android/app/src/main/res/drawable/icon_alert_yellow.xml new file mode 100644 index 00000000..2a895298 --- /dev/null +++ b/android/app/src/main/res/drawable/icon_alert_yellow.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/reddit_logo_16.png b/android/app/src/main/res/drawable/reddit_logo_16.png new file mode 100644 index 00000000..ce7f853a Binary files /dev/null and b/android/app/src/main/res/drawable/reddit_logo_16.png differ diff --git a/android/app/src/main/res/drawable/round_border.xml b/android/app/src/main/res/drawable/round_border.xml new file mode 100644 index 00000000..49d670d3 --- /dev/null +++ b/android/app/src/main/res/drawable/round_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/tomlplusplus_banner.png b/android/app/src/main/res/drawable/tomlplusplus_banner.png new file mode 100644 index 00000000..c9ee9963 Binary files /dev/null and b/android/app/src/main/res/drawable/tomlplusplus_banner.png differ diff --git a/android/app/src/main/res/layout/about_me_fragment.xml b/android/app/src/main/res/layout/about_me_fragment.xml new file mode 100644 index 00000000..020c3e3c --- /dev/null +++ b/android/app/src/main/res/layout/about_me_fragment.xml @@ -0,0 +1,259 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..be52e545 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/customfetch.xml b/android/app/src/main/res/layout/customfetch.xml new file mode 100644 index 00000000..f350c3a0 --- /dev/null +++ b/android/app/src/main/res/layout/customfetch.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/android/app/src/main/res/layout/customfetch_configure.xml b/android/app/src/main/res/layout/customfetch_configure.xml new file mode 100644 index 00000000..ad5126b9 --- /dev/null +++ b/android/app/src/main/res/layout/customfetch_configure.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +