├── .gitignore ├── .gitmodules ├── .idea ├── .gitignore ├── .name ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── uk │ │ └── akane │ │ └── accord │ │ ├── Accord.kt │ │ ├── logic │ │ ├── Extensions.kt │ │ ├── comparators │ │ │ ├── AlphaNumericComparator.kt │ │ │ └── SupportComparator.kt │ │ └── utils │ │ │ ├── AnimationUtils.kt │ │ │ ├── BottomSheetUtils.kt │ │ │ ├── CalculationUtils.kt │ │ │ ├── ImageUtils.kt │ │ │ ├── MediaUtils.kt │ │ │ ├── SortedUtils.kt │ │ │ └── UiUtils.kt │ │ ├── setupwizard │ │ ├── SetupWizardActivity.kt │ │ ├── adapters │ │ │ └── SetupWizardViewPagerAdapter.kt │ │ ├── components │ │ │ └── LinkTextView.kt │ │ └── fragments │ │ │ ├── PermissionPageFragment.kt │ │ │ └── WelcomePageFragment.kt │ │ └── ui │ │ ├── MainActivity.kt │ │ ├── adapters │ │ ├── BrowseViewPagerAdapter.kt │ │ ├── HomeAdapter.kt │ │ ├── Sorter.kt │ │ ├── ViewPagerAdapter.kt │ │ └── browse │ │ │ └── SongAdapter.kt │ │ ├── components │ │ ├── AlphabetScroller.kt │ │ ├── BlendView.kt │ │ ├── FadingVerticalEdgeLayout.kt │ │ ├── FloatingPanelLayout.kt │ │ ├── FullPlayer.kt │ │ └── PreviewPlayer.kt │ │ ├── fragments │ │ ├── BrowseFragment.kt │ │ ├── HomeFragment.kt │ │ ├── LibraryFragment.kt │ │ ├── SearchFragment.kt │ │ ├── ViewPagerContainerFragment.kt │ │ └── browse │ │ │ ├── AlbumFragment.kt │ │ │ ├── ArtistFragment.kt │ │ │ ├── DateFragment.kt │ │ │ ├── GenreFragment.kt │ │ │ └── SongFragment.kt │ │ └── viewmodels │ │ └── AccordViewModel.kt │ └── res │ ├── color │ ├── cl_action_btn_icon.xml │ ├── cl_ask_perm_btn.xml │ ├── cl_ask_perm_text.xml │ └── cl_bottom_nav_label.xml │ ├── drawable │ ├── eg.jpeg │ ├── ic_album_stack.xml │ ├── ic_bottom_nav_browse.xml │ ├── ic_bottom_nav_home.xml │ ├── ic_bottom_nav_library.xml │ ├── ic_bottom_nav_search.xml │ ├── ic_default_cover.xml │ ├── ic_folder.xml │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_launcher_foreground_variant.xml │ ├── ic_master_play.xml │ ├── ic_master_shuffle.xml │ ├── ic_more_horiz.xml │ ├── ic_note.xml │ ├── ic_opened_books.xml │ ├── ic_prop_next.xml │ ├── ic_prop_pause.xml │ ├── ic_prop_play.xml │ ├── ic_prop_prev.xml │ ├── ic_sort_btn.xml │ ├── selected_chip_background.xml │ ├── shape_bottom_nav.xml │ ├── slider_container_background.xml │ ├── splash_anim.xml │ └── top_app_bar_divider.xml │ ├── font │ ├── inter_bold.ttf │ ├── inter_medium.ttf │ ├── inter_regular.ttf │ └── inter_semibold.ttf │ ├── layout │ ├── activity_main.xml │ ├── activity_setup_wizard.xml │ ├── fragment_browse.xml │ ├── fragment_browse_song.xml │ ├── fragment_home.xml │ ├── fragment_library.xml │ ├── fragment_permission_page.xml │ ├── fragment_search.xml │ ├── fragment_viewpager_container.xml │ ├── fragment_welcome_page.xml │ ├── layout_floating_panel.xml │ ├── layout_full_player.xml │ ├── layout_item_header.xml │ ├── layout_master_control.xml │ ├── layout_preview_player.xml │ ├── layout_song_item.xml │ └── view_blend.xml │ ├── menu │ ├── bottom_nav.xml │ └── menu_browse.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-mdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── mipmap-xxxhdpi │ ├── ic_launcher.webp │ └── ic_launcher_round.webp │ ├── resources.properties │ ├── values-night │ └── colors.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ ├── styleables.xml │ ├── styles.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libPhonograph"] 2 | path = libPhonograph 3 | url = https://github.com/AkaneTan/libPhonograph 4 | [submodule "Cupertino"] 5 | path = Cupertino 6 | url = https://github.com/FoedusProgramme/Cupertino 7 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Accord -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | /debug 4 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "uk.akane.accord" 8 | compileSdk = 35 9 | 10 | androidResources { 11 | generateLocaleConfig = true 12 | } 13 | 14 | defaultConfig { 15 | applicationId = "uk.akane.accord" 16 | minSdk = 31 17 | targetSdk = 35 18 | versionCode = 1 19 | versionName = "1.0" 20 | } 21 | 22 | buildFeatures { 23 | buildConfig = true 24 | } 25 | 26 | buildTypes { 27 | release { 28 | isMinifyEnabled = true 29 | isShrinkResources = true 30 | proguardFiles( 31 | getDefaultProguardFile("proguard-android-optimize.txt"), 32 | "proguard-rules.pro" 33 | ) 34 | } 35 | debug { 36 | applicationIdSuffix = ".debug" 37 | } 38 | } 39 | 40 | compileOptions { 41 | sourceCompatibility = JavaVersion.VERSION_1_8 42 | targetCompatibility = JavaVersion.VERSION_1_8 43 | } 44 | 45 | kotlinOptions { 46 | jvmTarget = "1.8" 47 | } 48 | } 49 | 50 | dependencies { 51 | 52 | implementation(project(":libphonograph:libPhonograph")) 53 | implementation(project(":Cupertino:Cupertino")) 54 | implementation(libs.androidx.core.ktx) 55 | implementation(libs.androidx.appcompat) 56 | implementation(libs.material) 57 | implementation(libs.androidx.activity) 58 | implementation(libs.androidx.constraintlayout) 59 | implementation(libs.activity.ktx) 60 | implementation(libs.androidx.fragment.ktx) 61 | implementation(libs.androidx.palette.ktx) 62 | implementation(libs.androidx.lifecycle.viewmodel.ktx) 63 | implementation(libs.androidx.core.splashscreen) 64 | implementation(libs.androidx.window) 65 | implementation(libs.androidx.media3.exoplayer) 66 | 67 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 8 | 10 | 12 | 13 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/Accord.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord 2 | 3 | import android.app.Application 4 | 5 | class Accord : Application() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/comparators/AlphaNumericComparator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Farbod Safaei 3 | * 2024 Akane Foundation 4 | * 5 | * Gramophone is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * Gramophone is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | package uk.akane.accord.logic.comparators 19 | 20 | import java.math.BigInteger 21 | import java.text.Collator 22 | import java.util.Locale 23 | import java.util.Objects 24 | 25 | /** 26 | * 27 | * An alphanumeric comparator for comparing strings in a human readable format. 28 | * It uses a combination numeric and alphabetic comparisons to compare two strings. 29 | * This class uses standard Java classes, independent of 3rd party libraries. 30 | * 31 | * 32 | * 33 | * 34 | * For given list of strings: 35 | *
 36 |  * file-01.doc
 37 |  * file-2.doc
 38 |  * file-03.doc
39 | * 40 | * 41 | * The regular lexicographical sort e.g. [java.util.Collections.sort] will result in a sorted list of: 42 | * 43 | *
 44 |  * file-01.doc
 45 |  * file-03.doc
 46 |  * file-2.doc
47 | * 48 | * 49 | * But using this class, the result will be a more human readable and organizable sorted list of: 50 | * 51 | *
 52 |  * file-01.doc
 53 |  * file-2.doc
 54 |  * file-03.doc
55 | * 56 | * 57 | * Additionally this comparator uses [java.text.Collator] class to correctly sort 58 | * strings containing special Unicode characters such as Umlauts and other similar letters of 59 | * alphabet in different languages, such as: å, è, ü, ö, ø, or ý. 60 | * 61 | * 62 | * For given list of strings: 63 | * 64 | *
 65 |  * b
 66 |  * e
 67 |  * ě
 68 |  * f
 69 |  * è
 70 |  * g
 71 |  * k
72 | * 73 | * 74 | * Using a regular lexicographical sort e.g. [java.util.Collections.sort], will 75 | * sort the collection in the following order: 76 | * 77 | *
[b, e, f, g, k, è, ě]
78 | * 79 | * 80 | * However using this class because of utilizing a [java.text.Collator], the previous values will be 81 | * sorted in following order: 82 | * 83 | *
[b, e, è, ě, f, g, k]
84 | * 85 | * @author Farbod Safaei - farbod@binaryheart.com 86 | */ 87 | @Suppress("unused") 88 | class AlphaNumericComparator : Comparator { 89 | private val collator: Collator 90 | 91 | /** 92 | * Default constructor, uses the default Locale and default collator strength 93 | * 94 | * @see AlphaNumericComparator 95 | */ 96 | constructor() { 97 | collator = Collator.getInstance() 98 | } 99 | 100 | /** 101 | * Constructor using the provided `Locale` and default collator strength 102 | * 103 | * @param locale Desired `Locale` 104 | */ 105 | constructor(locale: Locale) { 106 | collator = Collator.getInstance(Objects.requireNonNull(locale)) 107 | } 108 | 109 | /** 110 | * Constructor with given `Locale` and collator strength value 111 | * 112 | * @param locale Desired `Locale` 113 | * @param strength Collator strength value, any of collator values from: [java.text.Collator.PRIMARY], 114 | * [java.text.Collator.SECONDARY], [java.text.Collator.TERTIARY], 115 | * or [java.text.Collator.IDENTICAL] 116 | * @see java.text.Collator 117 | */ 118 | constructor(locale: Locale, strength: Int) { 119 | collator = Collator.getInstance(Objects.requireNonNull(locale)) 120 | collator.strength = strength 121 | } 122 | 123 | /** 124 | * Compares two given `String` parameters. Both string parameters will be trimmed before comparison. 125 | * 126 | * @param s1 the first string to be compared 127 | * @param s2 the second string to be compared 128 | * @return If any of the given parameters is `null` or is an empty string like 129 | * `""`, `-1` or `1` will be returned based on the order: 130 | * `-1` will be returned if the first parameter is `null` or empty, 131 | * `1` will be returned if the second parameter is `null` or empty. 132 | * When both are either `null` or empty or any combination of those, a 133 | * `0` will be returned. 134 | */ 135 | override fun compare(s1: String?, s2: String?): Int { 136 | var ss1 = s1 137 | var ss2 = s2 138 | if ((ss1 == null || ss1.trim { it <= ' ' } 139 | .isEmpty()) && ss2 != null && ss2.trim { it <= ' ' }.isNotEmpty()) { 140 | return -1 141 | } 142 | if ((ss2 == null || ss2.trim { it <= ' ' } 143 | .isEmpty()) && ss1 != null && ss1.trim { it <= ' ' }.isNotEmpty()) { 144 | return 1 145 | } 146 | if ((ss1 == null || ss1.trim { it <= ' ' } 147 | .isEmpty()) && (ss2 == null || ss2.trim { it <= ' ' } 148 | .isEmpty())) { 149 | return 0 150 | } 151 | assert(ss1 != null) 152 | ss1 = ss1!!.trim { it <= ' ' } 153 | assert(ss2 != null) 154 | ss2 = ss2!!.trim { it <= ' ' } 155 | var s1Index = 0 156 | var s2Index = 0 157 | while (s1Index < ss1.length && s2Index < ss2.length) { 158 | var result: Int 159 | val s1Slice = this.slice(ss1, s1Index) 160 | val s2Slice = this.slice(ss2, s2Index) 161 | s1Index += s1Slice.length 162 | s2Index += s2Slice.length 163 | result = 164 | if (Character.isDigit(s1Slice[0]) && Character.isDigit(s2Slice[0])) { 165 | compareDigits(s1Slice, s2Slice) 166 | } else { 167 | compareCollatedStrings(s1Slice, s2Slice) 168 | } 169 | if (result != 0) { 170 | return result 171 | } 172 | } 173 | return Integer.signum(ss1.length - ss2.length) 174 | } 175 | 176 | private fun slice(s: String, index: Int): String { 177 | var index1 = index 178 | val result = StringBuilder() 179 | if (Character.isDigit(s[index1])) { 180 | while (index1 < s.length && Character.isDigit(s[index1])) { 181 | result.append(s[index1]) 182 | index1++ 183 | } 184 | } else { 185 | result.append(s[index1]) 186 | } 187 | return result.toString() 188 | } 189 | 190 | private fun compareDigits(s1: String, s2: String): Int { 191 | return BigInteger(s1).compareTo(BigInteger(s2)) 192 | } 193 | 194 | private fun compareCollatedStrings(s1: String, s2: String): Int { 195 | return collator.compare(s1, s2) 196 | } 197 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/comparators/SupportComparator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Akane Foundation 3 | * 4 | * Gramophone is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Gramophone is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package uk.akane.accord.logic.comparators 19 | 20 | class SupportComparator( 21 | private val cmp: Comparator, 22 | private val fallback: Comparator?, 23 | private val invert: Boolean, 24 | private val convert: (T) -> U 25 | ) : Comparator { 26 | override fun compare(o1: T, o2: T): Int { 27 | val c1 = convert(o1) 28 | val c2 = convert(o2) 29 | val i = cmp.compare(c1, c2) * (if (invert) -1 else 1) 30 | if (i != 0) return i 31 | fallback?.let { return it.compare(o1, o2) } 32 | return 0 33 | } 34 | 35 | companion object { 36 | fun createDummyComparator(): Comparator { 37 | return Comparator { _, _ -> 0 } 38 | } 39 | 40 | fun createInversionComparator(cmp: Comparator, invert: Boolean = false, fallback: Comparator? = null): 41 | Comparator { 42 | if (!invert) return cmp 43 | return SupportComparator(cmp, fallback, true) { it } 44 | } 45 | 46 | fun createAlphanumericComparator( 47 | inverted: Boolean = false, 48 | cnv: (T) -> CharSequence, 49 | fallback: Comparator? = null 50 | ): Comparator { 51 | return SupportComparator( 52 | AlphaNumericComparator(), 53 | fallback, 54 | inverted 55 | ) { cnv(it).toString() } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/AnimationUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | import android.animation.TimeInterpolator 4 | import android.animation.ValueAnimator 5 | import android.view.View 6 | import android.view.animation.PathInterpolator 7 | import androidx.core.animation.doOnEnd 8 | import androidx.core.text.TextUtilsCompat 9 | import androidx.core.view.ViewCompat 10 | import java.util.Locale 11 | 12 | object AnimationUtils { 13 | 14 | val easingInterpolator = PathInterpolator(0.2f, 0f, 0f, 1f) 15 | const val FASTEST_DURATION = 150L 16 | const val FAST_DURATION = 256L 17 | const val MID_DURATION = 350L 18 | 19 | inline fun createValAnimator( 20 | fromValue: T, 21 | toValue: T, 22 | duration: Long = FAST_DURATION, 23 | interpolator: TimeInterpolator = easingInterpolator, 24 | isArgb: Boolean = false, 25 | crossinline doOnEnd: (() -> Unit) = {}, 26 | crossinline changedListener: (animatedValue: T) -> Unit, 27 | ) { 28 | when (T::class) { 29 | Int::class -> { 30 | if (!isArgb) 31 | ValueAnimator.ofInt(fromValue as Int, toValue as Int) 32 | else 33 | ValueAnimator.ofArgb(fromValue as Int, toValue as Int) 34 | } 35 | Float::class -> { 36 | ValueAnimator.ofFloat(fromValue as Float, toValue as Float) 37 | } 38 | else -> throw IllegalArgumentException("No valid animator type found!") 39 | }.apply { 40 | this.duration = duration 41 | this.interpolator = interpolator 42 | this.addUpdateListener { 43 | changedListener(this.animatedValue as T) 44 | } 45 | this.doOnEnd { 46 | doOnEnd() 47 | } 48 | start() 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/BottomSheetUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | object BottomSheetUtils { 4 | enum class BottomFrameManipulateState { 5 | HIDE_SHEET, SHOW_SHEET, HIDE_NAV, SHOW_NAV, HIDE_ALL, SHOW_ALL 6 | } 7 | enum class ComponentState { 8 | SHOWN, HIDDEN 9 | } 10 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/CalculationUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2024 Akane Foundation 3 | * 4 | * Gramophone is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Gramophone is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | package uk.akane.accord.logic.utils 19 | 20 | import androidx.annotation.ColorInt 21 | import androidx.annotation.IntRange 22 | import java.text.SimpleDateFormat 23 | import java.util.Date 24 | import java.util.Locale 25 | 26 | /** 27 | * [CalculationUtils] contains some methods for internal 28 | * calculation. 29 | */ 30 | object CalculationUtils { 31 | 32 | /** 33 | * [convertDurationToTimeStamp] makes a string format 34 | * of duration (presumably long) converts into timestamp 35 | * like 300 to 5:00. 36 | * 37 | * @param duration 38 | * @return 39 | */ 40 | fun convertDurationToTimeStamp(duration: Long): String { 41 | val minutes = duration / 1000 / 60 42 | val seconds = duration / 1000 - minutes * 60 43 | if (seconds < 10) { 44 | return "$minutes:0$seconds" 45 | } 46 | return "$minutes:$seconds" 47 | } 48 | 49 | /** 50 | * convertUnixTimestampToMonthDay: 51 | * Converts unix timestamp to Month - Day format. 52 | */ 53 | fun convertUnixTimestampToMonthDay(unixTimestamp: Long): String = 54 | SimpleDateFormat( 55 | "MM-dd", 56 | Locale.getDefault() 57 | ).format( 58 | Date(unixTimestamp * 1000) 59 | ) 60 | 61 | /** 62 | * Set the alpha component of `color` to be `alpha`. 63 | */ 64 | @ColorInt 65 | fun setAlphaComponent( 66 | @ColorInt color: Int, 67 | @IntRange(from = 0x0, to = 0xFF) alpha: Int 68 | ): Int { 69 | require(!(alpha < 0 || alpha > 255)) { "alpha must be between 0 and 255." } 70 | return color and 0x00ffffff or (alpha shl 24) 71 | } 72 | 73 | @Suppress("NOTHING_TO_INLINE") 74 | inline fun lerp(start: Float, stop: Float, amount: Float): Float { 75 | return start + (stop - start) * amount 76 | } 77 | 78 | /** 79 | * Returns the interpolation scalar (s) that satisfies the equation: `value = `[ ][.lerp]`(a, b, s)` 80 | * 81 | * 82 | * If `a == b`, then this function will return 0. 83 | */ 84 | @Suppress("NOTHING_TO_INLINE") 85 | inline fun lerpInv(a: Float, b: Float, value: Float): Float { 86 | return if (a != b) (value - a) / (b - a) else 0.0f 87 | } 88 | 89 | /** Returns the single argument constrained between [0.0, 1.0]. */ 90 | private fun saturate(value: Float): Float { 91 | return value.coerceAtLeast(0f).coerceAtMost(1f) 92 | } 93 | 94 | /** Returns the saturated (constrained between [0, 1]) result of [.lerpInv]. */ 95 | fun lerpInvSat(a: Float, b: Float, value: Float): Float { 96 | return saturate(lerpInv(a, b, value)) 97 | } 98 | 99 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/ImageUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | import android.content.ContentResolver 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.net.Uri 7 | import android.widget.ImageView 8 | import java.io.FileNotFoundException 9 | import java.io.InputStream 10 | 11 | object ImageUtils { 12 | fun ImageView.load(uri: Uri) { 13 | val bitmap = getBitmapFromUri(contentResolver = this.context.contentResolver, uri, this.height) 14 | bitmap?.let { 15 | this.setImageBitmap(bitmap) 16 | } 17 | } 18 | fun getBitmapFromUri(contentResolver: ContentResolver, uri: Uri, desiredSize: Int): Bitmap? { 19 | var inputStream: InputStream? = null 20 | return try { 21 | inputStream = contentResolver.openInputStream(uri) 22 | val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } 23 | BitmapFactory.decodeStream(inputStream, null, options) 24 | inputStream?.close() 25 | 26 | options.inSampleSize = calculateInSampleSize(options, desiredSize) 27 | options.inJustDecodeBounds = false 28 | inputStream = contentResolver.openInputStream(uri) 29 | BitmapFactory.decodeStream(inputStream, null, options) 30 | } catch (e: FileNotFoundException) { 31 | e.printStackTrace() 32 | null 33 | } finally { 34 | inputStream?.close() 35 | } 36 | } 37 | private fun calculateInSampleSize(options: BitmapFactory.Options, desiredSize: Int): Int { 38 | val (height, width) = options.run { outHeight to outWidth } 39 | var inSampleSize = 1 40 | if (height > desiredSize || width > desiredSize) { 41 | val halfHeight = height / 2 42 | val halfWidth = width / 2 43 | while ((halfHeight / inSampleSize) >= desiredSize && (halfWidth / inSampleSize) >= desiredSize) { 44 | inSampleSize *= 2 45 | } 46 | } 47 | return inSampleSize 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/MediaUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import androidx.media3.common.MediaItem 6 | import androidx.media3.common.MediaMetadata 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.Job 10 | import kotlinx.coroutines.launch 11 | import uk.akane.accord.ui.viewmodels.AccordViewModel 12 | import uk.akane.libphonograph.reader.Reader 13 | import uk.akane.libphonograph.reader.ReaderResult 14 | 15 | object MediaUtils { 16 | /** 17 | * [getAllSongs] gets all of your songs from your local disk. 18 | * 19 | * @param context 20 | * @return 21 | */ 22 | private fun getAllSongs(context: Context): ReaderResult { 23 | return Reader.readFromMediaStore(context, 24 | { uri, mediaId, mimeType, title, writer, compilation, composer, artist, 25 | albumTitle, albumArtist, artworkUri, cdTrackNumber, trackNumber, 26 | discNumber, genre, recordingDay, recordingMonth, recordingYear, 27 | releaseYear, artistId, albumId, genreId, author, addDate, duration, 28 | modifiedDate -> 29 | return@readFromMediaStore MediaItem 30 | .Builder() 31 | .setUri(uri) 32 | .setMediaId(mediaId.toString()) 33 | .setMimeType(mimeType) 34 | .setMediaMetadata( 35 | MediaMetadata 36 | .Builder() 37 | .setIsBrowsable(false) 38 | .setIsPlayable(true) 39 | .setTitle(title) 40 | .setWriter(writer) 41 | .setCompilation(compilation) 42 | .setComposer(composer) 43 | .setArtist(artist) 44 | .setAlbumTitle(albumTitle) 45 | .setAlbumArtist(albumArtist) 46 | .setArtworkUri(artworkUri) 47 | .setTrackNumber(trackNumber) 48 | .setDiscNumber(discNumber) 49 | .setGenre(genre) 50 | .setRecordingDay(recordingDay) 51 | .setRecordingMonth(recordingMonth) 52 | .setRecordingYear(recordingYear) 53 | .setReleaseYear(releaseYear) 54 | .setExtras(Bundle().apply { 55 | if (artistId != null) { 56 | putLong("ArtistId", artistId) 57 | } 58 | if (albumId != null) { 59 | putLong("AlbumId", albumId) 60 | } 61 | if (genreId != null) { 62 | putLong("GenreId", genreId) 63 | } 64 | putString("Author", author) 65 | if (addDate != null) { 66 | putLong("AddDate", addDate) 67 | } 68 | if (duration != null) { 69 | putLong("Duration", duration) 70 | } 71 | if (modifiedDate != null) { 72 | putLong("ModifiedDate", modifiedDate) 73 | } 74 | cdTrackNumber?.toIntOrNull() 75 | ?.let { it1 -> putInt("CdTrackNumber", it1) } 76 | }) 77 | .build(), 78 | ).build() 79 | }, 80 | shouldUseEnhancedCoverReading = null, 81 | shouldLoadPlaylists = false 82 | ) 83 | } 84 | 85 | fun updateLibraryWithInCoroutine(viewModel: AccordViewModel, context: Context, then: (() -> Unit)? = null) { 86 | val pairObject = getAllSongs(context) 87 | CoroutineScope(Dispatchers.Main).launch { 88 | viewModel.mediaItemList.value = pairObject.songList 89 | viewModel.albumItemList.value = pairObject.albumList!! 90 | viewModel.artistItemList.value = pairObject.artistList!! 91 | viewModel.albumArtistItemList.value = pairObject.albumArtistList!! 92 | viewModel.genreItemList.value = pairObject.genreList!! 93 | viewModel.dateItemList.value = pairObject.dateList!! 94 | viewModel.folderStructure.value = pairObject.folderStructure!! 95 | viewModel.shallowFolderStructure.value = pairObject.shallowFolder!! 96 | viewModel.allFolderSet.value = pairObject.folders 97 | then?.invoke() 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/SortedUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | object SortedUtils { 4 | data class Item ( 5 | val title: String?, 6 | val content: T? 7 | ) 8 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/logic/utils/UiUtils.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.logic.utils 2 | 3 | object UiUtils { 4 | data class ScreenCorners( 5 | val topLeft: Float, 6 | val topRight: Float, 7 | val bottomLeft: Float, 8 | val bottomRight: Float 9 | ) { 10 | fun getAvgRadius() = 11 | (topLeft + topRight + bottomLeft + bottomRight) / 4f 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/setupwizard/SetupWizardActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.setupwizard 2 | 3 | import android.content.Intent 4 | import android.content.res.ColorStateList 5 | import android.os.Bundle 6 | import androidx.activity.viewModels 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 9 | import androidx.core.view.ViewCompat 10 | import androidx.core.view.WindowInsetsCompat 11 | import androidx.viewpager2.widget.ViewPager2 12 | import com.google.android.material.button.MaterialButton 13 | import uk.akane.accord.R 14 | import uk.akane.accord.logic.enableEdgeToEdgeProperly 15 | import uk.akane.accord.logic.isEssentialPermissionGranted 16 | import uk.akane.accord.logic.setCurrentItemInterpolated 17 | import uk.akane.accord.logic.utils.AnimationUtils 18 | import uk.akane.accord.setupwizard.adapters.SetupWizardViewPagerAdapter 19 | import uk.akane.accord.ui.MainActivity 20 | import uk.akane.accord.ui.viewmodels.AccordViewModel 21 | 22 | class SetupWizardActivity : AppCompatActivity() { 23 | 24 | private lateinit var viewPager2: ViewPager2 25 | private lateinit var viewPagerAdapter: SetupWizardViewPagerAdapter 26 | private lateinit var continueButton: MaterialButton 27 | 28 | private var inactiveBtnColor = 0 29 | private var activeBtnColor = 0 30 | 31 | private var onInactiveBtnColor = 0 32 | private var onActiveBtnColor = 0 33 | 34 | override fun onCreate(savedInstanceState: Bundle?) { 35 | super.onCreate(savedInstanceState) 36 | 37 | inactiveBtnColor = getColor(R.color.accentColorFainted) 38 | activeBtnColor = getColor(R.color.accentColor) 39 | 40 | onInactiveBtnColor = getColor(R.color.onAccentColorFainted) 41 | onActiveBtnColor = getColor(R.color.onAccentColor) 42 | 43 | installSplashScreen() 44 | 45 | enableEdgeToEdgeProperly() 46 | 47 | setContentView(R.layout.activity_setup_wizard) 48 | 49 | viewPager2 = findViewById(R.id.sw_viewpager) 50 | continueButton = findViewById(R.id.continue_btn) 51 | viewPagerAdapter = SetupWizardViewPagerAdapter(supportFragmentManager, lifecycle) 52 | 53 | ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> 54 | val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 55 | v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) 56 | insets 57 | } 58 | 59 | viewPager2.adapter = viewPagerAdapter 60 | viewPager2.isUserInputEnabled = false 61 | viewPager2.offscreenPageLimit = 9999 62 | 63 | continueButton.setOnClickListener { 64 | if (viewPager2.currentItem + 1 < viewPagerAdapter.itemCount) { 65 | if (viewPager2.currentItem + 1 == 1 && !isEssentialPermissionGranted()) { 66 | continueButton.isEnabled = false 67 | AnimationUtils.createValAnimator( 68 | activeBtnColor, 69 | inactiveBtnColor, 70 | isArgb = true 71 | ) { 72 | continueButton.backgroundTintList = ColorStateList.valueOf( 73 | it 74 | ) 75 | } 76 | AnimationUtils.createValAnimator( 77 | onActiveBtnColor, 78 | onInactiveBtnColor, 79 | isArgb = true 80 | ) { 81 | continueButton.setTextColor( 82 | it 83 | ) 84 | } 85 | } 86 | viewPager2.setCurrentItemInterpolated(viewPager2.currentItem + 1) 87 | } else { 88 | this.startActivity( 89 | Intent(this, MainActivity::class.java) 90 | ) 91 | finish() 92 | return@setOnClickListener 93 | } 94 | } 95 | 96 | } 97 | 98 | fun releaseContinueButton() { 99 | AnimationUtils.createValAnimator( 100 | inactiveBtnColor, 101 | activeBtnColor, 102 | isArgb = true, 103 | doOnEnd = { 104 | continueButton.isEnabled = true 105 | } 106 | ) { 107 | continueButton.backgroundTintList = ColorStateList.valueOf( 108 | it 109 | ) 110 | } 111 | AnimationUtils.createValAnimator( 112 | onInactiveBtnColor, 113 | onActiveBtnColor, 114 | isArgb = true 115 | ) { 116 | continueButton.setTextColor( 117 | it 118 | ) 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/setupwizard/adapters/SetupWizardViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.setupwizard.adapters 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import uk.akane.accord.setupwizard.fragments.PermissionPageFragment 8 | import uk.akane.accord.setupwizard.fragments.WelcomePageFragment 9 | 10 | class SetupWizardViewPagerAdapter( 11 | fragmentManager: FragmentManager, 12 | lifecycle: Lifecycle 13 | ) : FragmentStateAdapter(fragmentManager, lifecycle) { 14 | 15 | override fun getItemCount(): Int = 2 16 | 17 | override fun createFragment(position: Int): Fragment = 18 | when (position) { 19 | 0 -> WelcomePageFragment() 20 | 1 -> PermissionPageFragment() 21 | else -> throw IllegalArgumentException("Didn't find desired fragment!") 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/setupwizard/components/LinkTextView.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.setupwizard.components 2 | 3 | import android.content.Context 4 | import android.text.method.LinkMovementMethod 5 | import android.util.AttributeSet 6 | import androidx.appcompat.widget.AppCompatTextView 7 | 8 | class LinkTextView @JvmOverloads constructor( 9 | context: Context, 10 | attrs: AttributeSet? = null, 11 | defStyleAttr: Int = 0 12 | ) : AppCompatTextView( 13 | context, 14 | attrs, 15 | defStyleAttr 16 | ) { 17 | init { 18 | movementMethod = LinkMovementMethod.getInstance() 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/setupwizard/fragments/PermissionPageFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.setupwizard.fragments 2 | 3 | import android.animation.LayoutTransition 4 | import android.content.res.ColorStateList 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.activity.result.contract.ActivityResultContracts 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import androidx.fragment.app.Fragment 13 | import com.google.android.material.button.MaterialButton 14 | import uk.akane.accord.R 15 | import uk.akane.accord.logic.hasMediaPermissionSeparation 16 | import uk.akane.accord.logic.isAlbumPermissionGranted 17 | import uk.akane.accord.logic.isEssentialPermissionGranted 18 | import uk.akane.accord.logic.utils.AnimationUtils 19 | import uk.akane.accord.setupwizard.SetupWizardActivity 20 | 21 | class PermissionPageFragment : Fragment() { 22 | 23 | private lateinit var storageConstraintLayout: ConstraintLayout 24 | private lateinit var musicConstraintLayout: ConstraintLayout 25 | private lateinit var albumConstraintLayout: ConstraintLayout 26 | 27 | private lateinit var storagePermissionButton: MaterialButton 28 | private lateinit var musicPermissionButton: MaterialButton 29 | private lateinit var albumPermissionButton: MaterialButton 30 | 31 | private val requestPermissionLauncher = 32 | registerForActivityResult( 33 | ActivityResultContracts.RequestPermission() 34 | ) { isSuccessful: Boolean -> 35 | handlePermissionSuccessful(isSuccessful) 36 | } 37 | 38 | private fun handlePermissionSuccessful(successful: Boolean) { 39 | if (successful) { 40 | if (requireContext().isEssentialPermissionGranted() && !musicPermissionButton.isChecked) { 41 | (requireActivity() as SetupWizardActivity).releaseContinueButton() 42 | } 43 | if (requireContext().hasMediaPermissionSeparation() && 44 | requireContext().isEssentialPermissionGranted() && !musicPermissionButton.isChecked) { 45 | AnimationUtils.createValAnimator( 46 | if (musicPermissionButton.isChecked) allowedColor else allowColor, 47 | if (musicPermissionButton.isChecked) allowColor else allowedColor, 48 | isArgb = true 49 | ) { it1 -> 50 | musicPermissionButton.backgroundTintList = ColorStateList.valueOf( 51 | it1 52 | ) 53 | } 54 | AnimationUtils.createValAnimator( 55 | if (musicPermissionButton.isChecked) onAllowedColor else onAllowColor, 56 | if (musicPermissionButton.isChecked) onAllowColor else onAllowedColor, 57 | isArgb = true, 58 | ) { it1 -> 59 | musicPermissionButton.setTextColor(it1) 60 | } 61 | musicPermissionButton.isChecked = !musicPermissionButton.isChecked 62 | musicPermissionButton.text = if (musicPermissionButton.isChecked) allowedString else allowString 63 | } else if ( 64 | !requireContext().hasMediaPermissionSeparation() && 65 | requireContext().isEssentialPermissionGranted() && !storagePermissionButton.isChecked) { 66 | AnimationUtils.createValAnimator( 67 | if (storagePermissionButton.isChecked) allowedColor else allowColor, 68 | if (storagePermissionButton.isChecked) allowColor else allowedColor, 69 | isArgb = true, 70 | ) { it1 -> 71 | storagePermissionButton.backgroundTintList = ColorStateList.valueOf( 72 | it1 73 | ) 74 | } 75 | AnimationUtils.createValAnimator( 76 | if (storagePermissionButton.isChecked) onAllowedColor else onAllowColor, 77 | if (storagePermissionButton.isChecked) onAllowColor else onAllowedColor, 78 | isArgb = true, 79 | ) { it1 -> 80 | storagePermissionButton.setTextColor(it1) 81 | } 82 | storagePermissionButton.isChecked = !storagePermissionButton.isChecked 83 | storagePermissionButton.text = if (storagePermissionButton.isChecked) allowedString else allowString 84 | } else if ( 85 | requireContext().hasMediaPermissionSeparation() && 86 | requireContext().isAlbumPermissionGranted() && !albumPermissionButton.isChecked) { 87 | AnimationUtils.createValAnimator( 88 | if (albumPermissionButton.isChecked) allowedColor else allowColor, 89 | if (albumPermissionButton.isChecked) allowColor else allowedColor, 90 | isArgb = true 91 | ) { it1 -> 92 | albumPermissionButton.backgroundTintList = ColorStateList.valueOf( 93 | it1 94 | ) 95 | } 96 | AnimationUtils.createValAnimator( 97 | if (albumPermissionButton.isChecked) onAllowedColor else onAllowColor, 98 | if (albumPermissionButton.isChecked) onAllowColor else onAllowedColor, 99 | isArgb = true, 100 | ) { it1 -> 101 | albumPermissionButton.setTextColor(it1) 102 | } 103 | albumPermissionButton.isChecked = !albumPermissionButton.isChecked 104 | albumPermissionButton.text = if (albumPermissionButton.isChecked) allowedString else allowString 105 | } 106 | } 107 | } 108 | 109 | private var allowString = "" 110 | private var allowedString = "" 111 | 112 | private var allowColor = 0 113 | private var allowedColor = 0 114 | 115 | private var onAllowColor = 0 116 | private var onAllowedColor = 0 117 | 118 | private lateinit var albumDescTextView: TextView 119 | override fun onCreateView( 120 | inflater: LayoutInflater, 121 | container: ViewGroup?, 122 | savedInstanceState: Bundle? 123 | ): View? { 124 | val rootView = inflater.inflate(R.layout.fragment_permission_page, container, false) 125 | 126 | allowString = getString(R.string.allow) 127 | allowedString = getString(R.string.allowed) 128 | 129 | allowColor = resources.getColor(R.color.setupWizardSurfaceColor, null) 130 | allowedColor = resources.getColor(R.color.accentColor, null) 131 | 132 | onAllowColor = resources.getColor(R.color.accentColor, null) 133 | onAllowedColor = resources.getColor(R.color.onAccentColor, null) 134 | 135 | storageConstraintLayout = rootView.findViewById(R.id.storage_card) 136 | musicConstraintLayout = rootView.findViewById(R.id.music_card) 137 | albumConstraintLayout = rootView.findViewById(R.id.photo_card) 138 | 139 | if (requireContext().hasMediaPermissionSeparation()) { 140 | storageConstraintLayout.visibility = View.GONE 141 | } else { 142 | musicConstraintLayout.visibility = View.GONE 143 | albumConstraintLayout.visibility = View.GONE 144 | albumDescTextView.visibility = View.GONE 145 | } 146 | 147 | storagePermissionButton = rootView.findViewById(R.id.storage_apply_btn) 148 | musicPermissionButton = rootView.findViewById(R.id.music_apply_btn) 149 | albumPermissionButton = rootView.findViewById(R.id.album_apply_btn) 150 | 151 | updateStatusImmediately() 152 | 153 | storageConstraintLayout.layoutTransition 154 | .enableTransitionType(LayoutTransition.CHANGING) 155 | musicConstraintLayout.layoutTransition 156 | .enableTransitionType(LayoutTransition.CHANGING) 157 | albumConstraintLayout.layoutTransition 158 | .enableTransitionType(LayoutTransition.CHANGING) 159 | 160 | musicPermissionButton.setOnClickListener { 161 | if (!requireContext().isEssentialPermissionGranted()) { 162 | requestPermissionLauncher.launch(android.Manifest.permission.READ_MEDIA_AUDIO) 163 | } 164 | } 165 | 166 | storagePermissionButton.setOnClickListener { 167 | if (!requireContext().isEssentialPermissionGranted()) { 168 | requestPermissionLauncher.launch(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE) 169 | } 170 | } 171 | 172 | albumPermissionButton.setOnClickListener { 173 | if (!requireContext().isAlbumPermissionGranted()) { 174 | requestPermissionLauncher.launch(android.Manifest.permission.READ_MEDIA_IMAGES) 175 | } 176 | } 177 | 178 | return rootView 179 | } 180 | 181 | override fun onResume() { 182 | super.onResume() 183 | updateStatusImmediately() 184 | } 185 | 186 | private fun updateStatusImmediately() { 187 | if (requireContext().hasMediaPermissionSeparation()) { 188 | storageConstraintLayout.visibility = View.GONE 189 | musicPermissionButton.isChecked = requireContext().isEssentialPermissionGranted() 190 | musicPermissionButton.text = if (musicPermissionButton.isChecked) allowedString else allowString 191 | albumPermissionButton.isChecked = requireContext().isAlbumPermissionGranted() 192 | albumPermissionButton.text = if (albumPermissionButton.isChecked) allowedString else allowString 193 | } else { 194 | musicConstraintLayout.visibility = View.GONE 195 | albumConstraintLayout.visibility = View.GONE 196 | albumDescTextView.visibility = View.GONE 197 | storagePermissionButton.isChecked = requireContext().isEssentialPermissionGranted() 198 | storagePermissionButton.text = if (storagePermissionButton.isChecked) allowedString else allowString 199 | } 200 | } 201 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/setupwizard/fragments/WelcomePageFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.setupwizard.fragments 2 | 3 | import android.os.Bundle 4 | import android.text.Html 5 | import android.text.method.LinkMovementMethod 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.TextView 10 | import androidx.fragment.app.Fragment 11 | import uk.akane.accord.R 12 | 13 | class WelcomePageFragment : Fragment() { 14 | override fun onCreateView( 15 | inflater: LayoutInflater, 16 | container: ViewGroup?, 17 | savedInstanceState: Bundle? 18 | ): View? { 19 | val rootView = inflater.inflate(R.layout.fragment_welcome_page, container, false) 20 | return rootView 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui 2 | 3 | import android.content.Intent 4 | import android.graphics.drawable.ShapeDrawable 5 | import android.graphics.drawable.shapes.RoundRectShape 6 | import android.os.Bundle 7 | import android.util.Log 8 | import android.view.RoundedCorner 9 | import android.view.View 10 | import androidx.activity.viewModels 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 13 | import androidx.core.view.ViewCompat 14 | import androidx.core.view.WindowInsetsCompat 15 | import com.google.android.material.bottomnavigation.BottomNavigationView 16 | import com.google.android.material.card.MaterialCardView 17 | import com.google.android.material.navigation.NavigationBarView.OnItemSelectedListener 18 | import com.google.android.material.shape.CornerFamily 19 | import uk.akane.accord.R 20 | import uk.akane.accord.logic.enableEdgeToEdgeProperly 21 | import uk.akane.accord.logic.isEssentialPermissionGranted 22 | import uk.akane.accord.logic.utils.MediaUtils 23 | import uk.akane.accord.logic.utils.UiUtils 24 | import uk.akane.accord.setupwizard.SetupWizardActivity 25 | import uk.akane.accord.ui.components.FloatingPanelLayout 26 | import uk.akane.accord.ui.viewmodels.AccordViewModel 27 | 28 | class MainActivity : AppCompatActivity() { 29 | 30 | private val accordViewModel: AccordViewModel by viewModels() 31 | 32 | companion object { 33 | const val DESIRED_SHRINK_RATIO = 0.85f 34 | } 35 | 36 | private lateinit var bottomNavigationView: BottomNavigationView 37 | private lateinit var floatingPanelLayout: FloatingPanelLayout 38 | private lateinit var shrinkContainerLayout: MaterialCardView 39 | private lateinit var shadeView: View 40 | private lateinit var screenCorners: UiUtils.ScreenCorners 41 | 42 | private var bottomInset: Int = 0 43 | private var bottomDefaultRadius: Int = 0 44 | 45 | private var bottomNavigationPanelColor: Int = 0 46 | 47 | private var isWindowColorSet: Boolean = false 48 | 49 | override fun onCreate(savedInstanceState: Bundle?) { 50 | super.onCreate(savedInstanceState) 51 | 52 | bottomDefaultRadius = resources.getDimensionPixelSize(R.dimen.bottom_panel_radius) 53 | bottomNavigationPanelColor = getColor(R.color.bottomNavigationPanelColor) 54 | 55 | if (isEssentialPermissionGranted()) { 56 | installSplashScreen() 57 | if (accordViewModel.mediaItemList.value?.isNotEmpty() != true) { 58 | MediaUtils.updateLibraryWithInCoroutine(accordViewModel, this) 59 | } 60 | } else { 61 | this.startActivity( 62 | Intent(this, SetupWizardActivity::class.java) 63 | ) 64 | finish() 65 | return 66 | } 67 | 68 | enableEdgeToEdgeProperly() 69 | setContentView(R.layout.activity_main) 70 | 71 | bottomNavigationView = findViewById(R.id.bottom_nav) 72 | floatingPanelLayout = findViewById(R.id.floating) 73 | shadeView = findViewById(R.id.shade) 74 | shrinkContainerLayout = findViewById(R.id.shrink_container) 75 | 76 | floatingPanelLayout.onSlideListener = object : FloatingPanelLayout.OnSlideListener { 77 | override fun onSlideStatusChanged(status: FloatingPanelLayout.SlideStatus) { 78 | when (status) { 79 | FloatingPanelLayout.SlideStatus.EXPANDED -> { 80 | 81 | } 82 | FloatingPanelLayout.SlideStatus.COLLAPSED -> { 83 | shrinkContainerLayout.apply { 84 | scaleX = 1f 85 | scaleY = 1f 86 | // setRenderEffect(null) 87 | } 88 | } 89 | FloatingPanelLayout.SlideStatus.SLIDING -> { 90 | 91 | } 92 | } 93 | } 94 | 95 | override fun onSlide(value: Float) { 96 | if (!isWindowColorSet) { 97 | findViewById(R.id.main).setBackgroundColor( 98 | getColor(R.color.windowColor) 99 | ) 100 | isWindowColorSet = true 101 | } 102 | shadeView.alpha = 0.5f * value 103 | shrinkContainerLayout.apply { 104 | scaleX = (1f - DESIRED_SHRINK_RATIO) * (1f - value) + DESIRED_SHRINK_RATIO 105 | scaleY = (1f - DESIRED_SHRINK_RATIO) * (1f - value) + DESIRED_SHRINK_RATIO 106 | } 107 | val cornerProgress = (screenCorners.getAvgRadius() - bottomDefaultRadius) * value + bottomDefaultRadius 108 | floatingPanelLayout.background = ShapeDrawable().apply { 109 | paint.color = bottomNavigationPanelColor 110 | shape = RoundRectShape( 111 | floatArrayOf( 112 | cornerProgress, cornerProgress, 113 | cornerProgress, cornerProgress, 114 | cornerProgress, cornerProgress, 115 | cornerProgress, cornerProgress, 116 | ), 117 | null, // No inner rectangle 118 | null // No inner radii 119 | ) 120 | } 121 | } 122 | 123 | } 124 | 125 | ViewCompat.setOnApplyWindowInsetsListener(bottomNavigationView) { v, windowInsetsCompat -> 126 | val insets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.navigationBars()) 127 | val windowInsets = windowInsetsCompat.toWindowInsets()!! 128 | screenCorners = UiUtils.ScreenCorners( 129 | (windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT)?.radius ?: 0).toFloat(), 130 | (windowInsets.getRoundedCorner(RoundedCorner.POSITION_TOP_RIGHT)?.radius ?: 0).toFloat(), 131 | (windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.radius ?: 0).toFloat(), 132 | (windowInsets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius ?: 0).toFloat() 133 | ) 134 | shrinkContainerLayout.shapeAppearanceModel = 135 | shrinkContainerLayout.shapeAppearanceModel 136 | .toBuilder() 137 | .setTopLeftCorner(CornerFamily.ROUNDED, screenCorners.topLeft) 138 | .setTopRightCorner(CornerFamily.ROUNDED, screenCorners.topRight) 139 | .setBottomLeftCorner(CornerFamily.ROUNDED, screenCorners.bottomLeft) 140 | .setBottomRightCorner(CornerFamily.ROUNDED, screenCorners.bottomRight) 141 | .build() 142 | bottomInset = insets.bottom 143 | v.setPadding( 144 | v.paddingLeft, 145 | v.paddingTop, 146 | v.paddingRight, 147 | insets.bottom 148 | ) 149 | WindowInsetsCompat.CONSUMED 150 | } 151 | 152 | } 153 | 154 | fun connectBottomNavigationView(listener : OnItemSelectedListener) { 155 | bottomNavigationView.setOnItemSelectedListener(listener) 156 | } 157 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/adapters/BrowseViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.adapters 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import uk.akane.accord.R 8 | import uk.akane.accord.ui.fragments.BrowseFragment 9 | import uk.akane.accord.ui.fragments.HomeFragment 10 | import uk.akane.accord.ui.fragments.LibraryFragment 11 | import uk.akane.accord.ui.fragments.SearchFragment 12 | import uk.akane.accord.ui.fragments.browse.AlbumFragment 13 | import uk.akane.accord.ui.fragments.browse.ArtistFragment 14 | import uk.akane.accord.ui.fragments.browse.DateFragment 15 | import uk.akane.accord.ui.fragments.browse.GenreFragment 16 | import uk.akane.accord.ui.fragments.browse.SongFragment 17 | 18 | class BrowseViewPagerAdapter( 19 | fragmentManager: FragmentManager, 20 | lifecycle: Lifecycle 21 | ) : FragmentStateAdapter(fragmentManager, lifecycle) { 22 | 23 | override fun getItemCount(): Int = 5 24 | 25 | override fun createFragment(position: Int): Fragment = 26 | when (position) { 27 | 0 -> SongFragment() 28 | 1 -> AlbumFragment() 29 | 2 -> ArtistFragment() 30 | 3 -> DateFragment() 31 | 4 -> GenreFragment() 32 | else -> throw IllegalArgumentException("Didn't find desired fragment!") 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/adapters/HomeAdapter.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.adapters 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import androidx.recyclerview.widget.RecyclerView 7 | import androidx.recyclerview.widget.RecyclerView.ViewHolder 8 | import uk.akane.accord.R 9 | 10 | class HomeAdapter : RecyclerView.Adapter() { 11 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = 12 | ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_song_item, parent, false)) 13 | 14 | override fun getItemCount(): Int = 100 15 | 16 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { 17 | } 18 | 19 | inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/adapters/ViewPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.adapters 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.fragment.app.FragmentManager 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.viewpager2.adapter.FragmentStateAdapter 7 | import uk.akane.accord.R 8 | import uk.akane.accord.ui.fragments.BrowseFragment 9 | import uk.akane.accord.ui.fragments.HomeFragment 10 | import uk.akane.accord.ui.fragments.LibraryFragment 11 | import uk.akane.accord.ui.fragments.SearchFragment 12 | 13 | class ViewPagerAdapter( 14 | fragmentManager: FragmentManager, 15 | lifecycle: Lifecycle 16 | ) : FragmentStateAdapter(fragmentManager, lifecycle) { 17 | companion object { 18 | val tabs: ArrayList = arrayListOf( 19 | R.id.home, 20 | R.id.browse, 21 | R.id.library, 22 | R.id.search 23 | ) 24 | } 25 | 26 | override fun getItemCount(): Int = tabs.count() 27 | 28 | override fun createFragment(position: Int): Fragment = 29 | when (position) { 30 | 0 -> HomeFragment() 31 | 1 -> BrowseFragment() 32 | 2 -> LibraryFragment() 33 | 3 -> SearchFragment() 34 | else -> throw IllegalArgumentException("Didn't find desired fragment!") 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/adapters/browse/SongAdapter.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.adapters.browse 2 | 3 | import android.util.Log 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import androidx.media3.common.MediaItem 10 | import androidx.recyclerview.widget.RecyclerView 11 | import uk.akane.accord.R 12 | import uk.akane.accord.logic.utils.ImageUtils.load 13 | import uk.akane.accord.logic.utils.SortedUtils 14 | 15 | class SongAdapter : RecyclerView.Adapter() { 16 | 17 | private val unsortedList: MutableList = mutableListOf() 18 | private val sortedList: MutableList> = mutableListOf() 19 | 20 | inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { 21 | val cover: ImageView? = view.findViewById(R.id.cover) 22 | val title: TextView? = view.findViewById(R.id.title) 23 | val subtitle: TextView? = view.findViewById(R.id.subtitle) 24 | val header: TextView? = view.findViewById(R.id.header) 25 | } 26 | 27 | fun update(updatedList: MutableList) { 28 | 29 | unsortedList.clear() 30 | unsortedList.addAll(updatedList) 31 | 32 | val map = mutableMapOf>() 33 | 34 | ('A'..'Z').forEach { map[it] = mutableListOf() } 35 | map['#'] = mutableListOf() 36 | 37 | for (mediaItem in unsortedList) { 38 | val title = (mediaItem.mediaMetadata.title?.firstOrNull()?.uppercase() ?: "#")[0] 39 | if (title in 'A'..'Z') { 40 | map[title]?.add(mediaItem) 41 | } else { 42 | map['#']?.add(mediaItem) 43 | } 44 | } 45 | 46 | for ((key, value) in map) { 47 | if (value.isNotEmpty()) { 48 | sortedList.add(SortedUtils.Item(key.toString(), null)) 49 | value.sortedBy { it.mediaMetadata.title.toString() }.forEach { 50 | sortedList.add(SortedUtils.Item(null, it)) 51 | } 52 | } 53 | } 54 | 55 | this.notifyDataSetChanged() 56 | } 57 | 58 | override fun getItemViewType(position: Int): Int { 59 | return if (position == 0) { 60 | 0 61 | } else if (sortedList[position - 1].title != null) { 62 | Log.d("TAG", "Hoooo, ${sortedList[position - 1].title}") 63 | 2 64 | } else { 65 | 1 66 | } 67 | } 68 | 69 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = 70 | ViewHolder(LayoutInflater.from(parent.context).inflate( 71 | when (viewType) { 72 | 0 -> R.layout.layout_master_control 73 | 1 -> R.layout.layout_song_item 74 | 2 -> R.layout.layout_item_header 75 | else -> throw IllegalArgumentException() 76 | }, 77 | parent, 78 | false 79 | ) 80 | ) 81 | 82 | override fun getItemCount(): Int = 83 | sortedList.size + 1 84 | 85 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 86 | when (holder.itemViewType) { 87 | 1 -> { 88 | val mediaItem = sortedList[position - 1].content!! 89 | holder.cover!!.post { 90 | mediaItem.mediaMetadata.artworkUri?.let { holder.cover.load(it) } 91 | } 92 | holder.title!!.text = mediaItem.mediaMetadata.title 93 | holder.subtitle!!.text = mediaItem.mediaMetadata.artist 94 | holder.itemView.isClickable = true 95 | } 96 | 2 -> { 97 | holder.header!!.text = sortedList[position - 1].title!! 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/components/AlphabetScroller.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.components 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.util.AttributeSet 7 | import android.view.View 8 | import androidx.core.content.res.ResourcesCompat 9 | import uk.akane.accord.R 10 | 11 | class AlphabetScroller @JvmOverloads constructor( 12 | context: Context, 13 | attrs: AttributeSet? = null, 14 | defStyleAttr: Int = 0 15 | ) : View(context, attrs, defStyleAttr) { 16 | 17 | private val letters = ('A'..'Z') + '#' 18 | private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { 19 | textSize = resources.getDimensionPixelSize(R.dimen.scroller_char_size).toFloat() 20 | color = resources.getColor(R.color.accentColor, null) 21 | typeface = ResourcesCompat.getFont(context, R.font.inter_semibold) 22 | } 23 | 24 | private val scrollerWidth = resources.getDimensionPixelSize(R.dimen.scroller_width) 25 | private val fatWidth = paint.measureText("W") 26 | 27 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 28 | super.onMeasure(widthMeasureSpec, heightMeasureSpec) 29 | setMeasuredDimension( 30 | scrollerWidth, 31 | MeasureSpec.makeMeasureSpec((letters.size * (paint.textSize + 10)).toInt(), MeasureSpec.EXACTLY) // 文字高度 + 间距 32 | ) 33 | } 34 | 35 | override fun onDraw(canvas: Canvas) { 36 | super.onDraw(canvas) 37 | 38 | var y = paint.textSize + 5 39 | 40 | letters.forEach { letter -> 41 | canvas.drawText( 42 | letter.toString(), 43 | width - fatWidth + (fatWidth - paint.measureText(letter.toString())) / 2, 44 | y, 45 | paint 46 | ) 47 | y += paint.textSize + 10 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/components/BlendView.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.components 2 | 3 | import android.animation.ValueAnimator 4 | import android.content.ContentResolver 5 | import android.content.Context 6 | import android.graphics.Bitmap 7 | import android.graphics.BitmapFactory 8 | import android.graphics.Canvas 9 | import android.graphics.ColorMatrix 10 | import android.graphics.ColorMatrixColorFilter 11 | import android.graphics.Paint 12 | import android.graphics.RenderEffect 13 | import android.graphics.Shader 14 | import android.net.Uri 15 | import android.os.Handler 16 | import android.os.Looper 17 | import android.util.AttributeSet 18 | import android.view.WindowManager 19 | import android.view.animation.AnimationUtils 20 | import android.widget.FrameLayout 21 | import android.widget.ImageSwitcher 22 | import android.widget.ImageView 23 | import androidx.constraintlayout.widget.ConstraintLayout 24 | import androidx.core.content.ContextCompat 25 | import androidx.core.graphics.drawable.toDrawable 26 | import androidx.core.view.doOnLayout 27 | import kotlinx.coroutines.CoroutineScope 28 | import kotlinx.coroutines.Dispatchers 29 | import kotlinx.coroutines.launch 30 | import kotlinx.coroutines.withContext 31 | import uk.akane.accord.R 32 | import java.io.FileNotFoundException 33 | import java.io.InputStream 34 | import kotlin.math.ceil 35 | 36 | class BlendView @JvmOverloads constructor( 37 | context: Context, 38 | attrs: AttributeSet? = null, 39 | defStyleAttr: Int = 0, 40 | defStyleRes: Int = 0 41 | ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) { 42 | 43 | private val imageViewTS: ImageSwitcher 44 | private val imageViewBE: ImageSwitcher 45 | private val imageViewBG: ImageSwitcher 46 | private val rotateFrame: ConstraintLayout 47 | 48 | private var isAnimationOngoing: Boolean = true 49 | private val handler = Handler(Looper.getMainLooper()) 50 | private val overlayColor = ContextCompat.getColor(context, R.color.frontShadeColor) 51 | private var previousBitmap: Bitmap? = null 52 | 53 | companion object { 54 | const val VIEW_TRANSIT_DURATION: Long = 400 55 | const val FULL_BLUR_RADIUS: Float = 80F 56 | const val SHALLOW_BLUR_RADIUS: Float = 60F 57 | const val UPDATE_RUNNABLE_INTERVAL: Long = 34 58 | const val CYCLE: Int = 360 59 | const val SATURATION_FACTOR: Float = 1.2F 60 | const val PICTURE_SIZE: Int = 60 61 | } 62 | 63 | init { 64 | inflate(context, R.layout.view_blend, this) 65 | imageViewTS = findViewById(R.id.type1) 66 | imageViewBE = findViewById(R.id.type3) 67 | imageViewBG = findViewById(R.id.bg) 68 | rotateFrame = findViewById(R.id.rotate_frame) 69 | 70 | initializeImageSwitchers() 71 | 72 | this.setRenderEffect( 73 | RenderEffect.createBlurEffect(FULL_BLUR_RADIUS, FULL_BLUR_RADIUS, Shader.TileMode.MIRROR) 74 | ) 75 | } 76 | 77 | private fun initializeImageSwitchers() { 78 | val animationIn = AnimationUtils.loadAnimation(context, android.R.anim.fade_in).apply { 79 | duration = VIEW_TRANSIT_DURATION 80 | } 81 | val animationOut = AnimationUtils.loadAnimation(context, android.R.anim.fade_out).apply { 82 | duration = VIEW_TRANSIT_DURATION 83 | } 84 | val factoryList = listOf(imageViewTS, imageViewBE, imageViewBG) 85 | 86 | factoryList.forEach { switcher -> 87 | switcher.setFactory { 88 | ImageView(context).apply { 89 | scaleType = ImageView.ScaleType.CENTER_CROP 90 | layoutParams = FrameLayout.LayoutParams( 91 | FrameLayout.LayoutParams.MATCH_PARENT, 92 | FrameLayout.LayoutParams.MATCH_PARENT 93 | ) 94 | setLayerType(LAYER_TYPE_SOFTWARE, null) 95 | } 96 | } 97 | switcher.inAnimation = animationIn 98 | switcher.outAnimation = animationOut 99 | } 100 | } 101 | 102 | override fun dispatchDraw(canvas: Canvas) { 103 | super.dispatchDraw(canvas) 104 | canvas.drawColor(overlayColor) 105 | } 106 | 107 | override fun onAttachedToWindow() { 108 | super.onAttachedToWindow() 109 | adjustViewScale() 110 | } 111 | 112 | private fun adjustViewScale() { 113 | doOnLayout { 114 | val windowMetrics = (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).currentWindowMetrics 115 | val screenHeight = windowMetrics.bounds.height() 116 | val screenWidth = windowMetrics.bounds.width() 117 | 118 | val viewWidth = width.toFloat() 119 | val viewHeight = height.toFloat() 120 | 121 | val finalScale = ceil((screenWidth / viewWidth).coerceAtLeast(screenHeight / viewHeight)) 122 | 123 | this.scaleX = finalScale 124 | this.scaleY = finalScale 125 | } 126 | } 127 | 128 | fun setImageUri(uri: Uri) { 129 | CoroutineScope(Dispatchers.IO).launch { 130 | val originalBitmap = getBitmapFromUri(context.contentResolver, uri) 131 | if (originalBitmap != null && !areBitmapsSame(originalBitmap, previousBitmap)) { 132 | enhanceBitmap(originalBitmap).let { enhancedBitmap -> 133 | withContext(Dispatchers.Main) { 134 | updateImageViews(enhancedBitmap) 135 | } 136 | } 137 | previousBitmap = originalBitmap 138 | } else if (originalBitmap == null) { 139 | withContext(Dispatchers.Main) { 140 | clearImageViews() 141 | } 142 | previousBitmap = null 143 | } 144 | } 145 | } 146 | 147 | private fun updateImageViews(bitmap: Bitmap) { 148 | imageViewTS.setImageDrawable(cropTopLeftQuarter(bitmap).toDrawable(resources)) 149 | imageViewBE.setImageDrawable(cropBottomRightQuarter(bitmap).toDrawable(resources)) 150 | imageViewBG.setImageDrawable(bitmap.toDrawable(resources)) 151 | } 152 | 153 | private fun clearImageViews() { 154 | imageViewTS.setImageDrawable(null) 155 | imageViewBE.setImageDrawable(null) 156 | imageViewBG.setImageDrawable(null) 157 | } 158 | 159 | fun animateBlurRadius(enlarge: Boolean, duration: Long) { 160 | val fromVal = if (enlarge) SHALLOW_BLUR_RADIUS else FULL_BLUR_RADIUS 161 | val toVal = if (enlarge) FULL_BLUR_RADIUS else SHALLOW_BLUR_RADIUS 162 | ValueAnimator.ofFloat(fromVal, toVal).apply { 163 | this.duration = duration 164 | addUpdateListener { animator -> 165 | val radius = animator.animatedValue as Float 166 | val renderEffect = RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.MIRROR) 167 | post { this@BlendView.setRenderEffect(renderEffect) } 168 | } 169 | start() 170 | } 171 | } 172 | 173 | fun startRotationAnimation() { 174 | if (this.alpha > 0) { 175 | handler.removeCallbacks(rotationRunnable) 176 | isAnimationOngoing = true 177 | handler.postDelayed(rotationRunnable, UPDATE_RUNNABLE_INTERVAL) 178 | } 179 | } 180 | 181 | fun stopRotationAnimation() { 182 | handler.removeCallbacks(rotationRunnable) 183 | isAnimationOngoing = false 184 | } 185 | 186 | 187 | private val rotationRunnable = object : Runnable { 188 | override fun run() { 189 | imageViewTS.rotation = (imageViewTS.rotation + 1.2f) % CYCLE 190 | imageViewBE.rotation = (imageViewBE.rotation + .67f) % CYCLE 191 | rotateFrame.rotation = (rotateFrame.rotation - .6f) % CYCLE 192 | if (isAnimationOngoing) { 193 | handler.postDelayed(this, UPDATE_RUNNABLE_INTERVAL) 194 | } 195 | } 196 | } 197 | 198 | private fun getBitmapFromUri(contentResolver: ContentResolver, uri: Uri): Bitmap? { 199 | var inputStream: InputStream? = null 200 | return try { 201 | inputStream = contentResolver.openInputStream(uri) 202 | val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } 203 | BitmapFactory.decodeStream(inputStream, null, options) 204 | inputStream?.close() 205 | 206 | options.inSampleSize = calculateInSampleSize(options) 207 | options.inJustDecodeBounds = false 208 | inputStream = contentResolver.openInputStream(uri) 209 | BitmapFactory.decodeStream(inputStream, null, options) 210 | } catch (e: FileNotFoundException) { 211 | e.printStackTrace() 212 | null 213 | } finally { 214 | inputStream?.close() 215 | } 216 | } 217 | 218 | private fun calculateInSampleSize(options: BitmapFactory.Options): Int { 219 | val (height, width) = options.run { outHeight to outWidth } 220 | var inSampleSize = 1 221 | if (height > PICTURE_SIZE || width > PICTURE_SIZE) { 222 | val halfHeight = height / 2 223 | val halfWidth = width / 2 224 | while ((halfHeight / inSampleSize) >= PICTURE_SIZE && (halfWidth / inSampleSize) >= PICTURE_SIZE) { 225 | inSampleSize *= 2 226 | } 227 | } 228 | return inSampleSize 229 | } 230 | 231 | private fun enhanceBitmap(bitmap: Bitmap): Bitmap { 232 | val width = bitmap.width 233 | val height = bitmap.height 234 | 235 | val enhancedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) 236 | enhancedBitmap.density = bitmap.density 237 | 238 | val enhancePaint = Paint() 239 | val colorMatrix = ColorMatrix().apply { setSaturation(SATURATION_FACTOR) } 240 | enhancePaint.colorFilter = ColorMatrixColorFilter(colorMatrix) 241 | 242 | val canvas = Canvas(enhancedBitmap) 243 | canvas.drawBitmap(bitmap, 0f, 0f, enhancePaint) 244 | 245 | return enhancedBitmap 246 | } 247 | 248 | 249 | private fun cropTopLeftQuarter(bitmap: Bitmap): Bitmap { 250 | val quarterWidth = bitmap.width / 2 251 | val quarterHeight = bitmap.height / 2 252 | return Bitmap.createBitmap(bitmap, 0, 0, quarterWidth, quarterHeight) 253 | } 254 | 255 | private fun cropBottomRightQuarter(bitmap: Bitmap): Bitmap { 256 | val quarterWidth = bitmap.width / 2 257 | val quarterHeight = bitmap.height / 2 258 | return Bitmap.createBitmap(bitmap, quarterWidth, quarterHeight, quarterWidth, quarterHeight) 259 | } 260 | 261 | private fun areBitmapsSame(b1: Bitmap?, b2: Bitmap?): Boolean { 262 | return b1 != null && b2 != null && b1.width == b2.width && b1.height == b2.height && b1.sameAs(b2) 263 | } 264 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/components/FadingVerticalEdgeLayout.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.components 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.LinearGradient 7 | import android.graphics.Paint 8 | import android.graphics.PorterDuff 9 | import android.graphics.PorterDuffXfermode 10 | import android.graphics.Rect 11 | import android.graphics.Shader 12 | import android.util.AttributeSet 13 | import android.util.TypedValue 14 | import android.widget.FrameLayout 15 | import androidx.core.view.doOnLayout 16 | import uk.akane.accord.R 17 | import kotlin.math.min 18 | 19 | open class FadingVerticalEdgeLayout : FrameLayout { 20 | private var fadeTop = false 21 | private var fadeBottom = false 22 | private var gradientSizeTop = 0 23 | private var gradientSizeBottom = 0 24 | private var gradientPaintTop: Paint? = null 25 | private var gradientPaintBottom: Paint? = null 26 | private var gradientRectTop: Rect? = null 27 | private var gradientRectBottom: Rect? = null 28 | private var gradientDirtyFlags = 0 29 | 30 | constructor(context: Context?) : super(context!!) { 31 | init(null) 32 | } 33 | 34 | constructor(context: Context?, attrs: AttributeSet?) : super( 35 | context!!, attrs 36 | ) { 37 | init(attrs) 38 | } 39 | 40 | constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( 41 | context!!, attrs, defStyleAttr 42 | ) { 43 | init(attrs) 44 | } 45 | 46 | private fun init(attrs: AttributeSet?) { 47 | val defaultSize = TypedValue.applyDimension( 48 | TypedValue.COMPLEX_UNIT_DIP, DEFAULT_GRADIENT_SIZE_DP.toFloat(), 49 | resources.displayMetrics 50 | ).toInt() 51 | if (attrs != null) { 52 | val arr = 53 | context.obtainStyledAttributes(attrs, R.styleable.FadingVerticalEdgeLayout, 0, 0) 54 | val flags = arr.getInt(R.styleable.FadingVerticalEdgeLayout_fel_edge, 0) 55 | fadeTop = flags and FADE_EDGE_TOP == FADE_EDGE_TOP 56 | fadeBottom = flags and FADE_EDGE_BOTTOM == FADE_EDGE_BOTTOM 57 | gradientSizeTop = 58 | arr.getDimensionPixelSize(R.styleable.FadingVerticalEdgeLayout_fel_size_top, defaultSize) 59 | gradientSizeBottom = 60 | arr.getDimensionPixelSize(R.styleable.FadingVerticalEdgeLayout_fel_size_bottom, defaultSize) 61 | if (fadeTop && gradientSizeTop > 0) { 62 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_TOP 63 | } 64 | if (fadeBottom && gradientSizeBottom > 0) { 65 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_BOTTOM 66 | } 67 | arr.recycle() 68 | } else { 69 | gradientSizeBottom = defaultSize 70 | gradientSizeTop = gradientSizeBottom 71 | } 72 | val mode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) 73 | gradientPaintTop = Paint(Paint.ANTI_ALIAS_FLAG) 74 | gradientPaintTop!!.setXfermode(mode) 75 | gradientPaintBottom = Paint(Paint.ANTI_ALIAS_FLAG) 76 | gradientPaintBottom!!.setXfermode(mode) 77 | gradientRectTop = Rect() 78 | gradientRectBottom = Rect() 79 | } 80 | 81 | override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { 82 | if (paddingTop != top) { 83 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_TOP 84 | } 85 | if (paddingBottom != bottom) { 86 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_BOTTOM 87 | } 88 | super.setPadding(left, top, right, bottom) 89 | } 90 | 91 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 92 | super.onSizeChanged(w, h, oldw, oldh) 93 | if (h != oldh) { 94 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_TOP 95 | gradientDirtyFlags = gradientDirtyFlags or DIRTY_FLAG_BOTTOM 96 | } 97 | } 98 | 99 | override fun onAttachedToWindow() { 100 | super.onAttachedToWindow() 101 | doOnLayout { 102 | updateGradientIfNecessary() 103 | } 104 | } 105 | 106 | private var previousPaddingBottom = 0 107 | private var previousPaddingTop = 0 108 | 109 | override fun dispatchDraw(canvas: Canvas) { 110 | if (previousPaddingBottom != paddingBottom || previousPaddingTop != paddingTop) { 111 | updateGradientIfNecessary() 112 | previousPaddingBottom = paddingBottom 113 | previousPaddingTop = paddingTop 114 | } 115 | val count = canvas.saveLayer(null, null) 116 | super.dispatchDraw(canvas) 117 | drawFade(canvas) 118 | canvas.restoreToCount(count) 119 | } 120 | 121 | private fun updateGradientIfNecessary() { 122 | if (gradientDirtyFlags and DIRTY_FLAG_TOP == DIRTY_FLAG_TOP) { 123 | gradientDirtyFlags = gradientDirtyFlags and DIRTY_FLAG_TOP.inv() 124 | initTopGradient() 125 | } 126 | if (gradientDirtyFlags and DIRTY_FLAG_BOTTOM == DIRTY_FLAG_BOTTOM) { 127 | gradientDirtyFlags = gradientDirtyFlags and DIRTY_FLAG_BOTTOM.inv() 128 | initBottomGradient() 129 | } 130 | } 131 | 132 | private fun drawFade(canvas: Canvas) { 133 | if (fadeTop && gradientSizeTop > 0) { 134 | canvas.drawRect(gradientRectTop!!, gradientPaintTop!!) 135 | } 136 | if (fadeBottom && gradientSizeBottom > 0) { 137 | canvas.drawRect(gradientRectBottom!!, gradientPaintBottom!!) 138 | } 139 | } 140 | 141 | private fun initTopGradient() { 142 | val actualHeight = height - paddingTop - paddingBottom 143 | val size = 144 | min(gradientSizeTop.toDouble(), actualHeight.toDouble()).toInt() 145 | val l = getPaddingLeft() 146 | val t = paddingTop 147 | val r = width - getPaddingRight() 148 | val b = t + size 149 | gradientRectTop!![l, t, r] = b 150 | val gradient = LinearGradient( 151 | l.toFloat(), 152 | t.toFloat(), 153 | l.toFloat(), 154 | b.toFloat(), 155 | FADE_COLORS, 156 | null, 157 | Shader.TileMode.CLAMP 158 | ) 159 | gradientPaintTop!!.setShader(gradient) 160 | } 161 | 162 | private fun initBottomGradient() { 163 | val actualHeight = height - paddingTop - paddingBottom 164 | val size = 165 | min(gradientSizeBottom.toDouble(), actualHeight.toDouble()).toInt() 166 | val l = getPaddingLeft() 167 | val t = paddingTop + actualHeight - size 168 | val r = width - getPaddingRight() 169 | val b = t + size 170 | gradientRectBottom!![l, t, r] = b 171 | val gradient = LinearGradient( 172 | l.toFloat(), 173 | t.toFloat(), 174 | l.toFloat(), 175 | b.toFloat(), 176 | FADE_COLORS_REVERSE, 177 | null, 178 | Shader.TileMode.CLAMP 179 | ) 180 | gradientPaintBottom!!.setShader(gradient) 181 | } 182 | 183 | companion object { 184 | private const val DEFAULT_GRADIENT_SIZE_DP = 80 185 | const val FADE_EDGE_TOP = 1 186 | const val FADE_EDGE_BOTTOM = 2 187 | private const val DIRTY_FLAG_TOP = 1 188 | private const val DIRTY_FLAG_BOTTOM = 2 189 | private val FADE_COLORS = intArrayOf(Color.TRANSPARENT, Color.BLACK) 190 | private val FADE_COLORS_REVERSE = intArrayOf(Color.BLACK, Color.TRANSPARENT) 191 | } 192 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/components/FullPlayer.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.components 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import android.view.WindowInsets 7 | import androidx.constraintlayout.widget.ConstraintLayout 8 | import androidx.core.content.res.ResourcesCompat 9 | import androidx.core.view.WindowInsetsCompat 10 | import androidx.core.view.marginBottom 11 | import androidx.core.view.marginLeft 12 | import androidx.core.view.marginRight 13 | import androidx.core.view.marginTop 14 | import androidx.core.view.updateLayoutParams 15 | import uk.akane.accord.R 16 | import uk.akane.accord.logic.getUriToDrawable 17 | import uk.akane.cupertino.widget.OverlayDivider 18 | 19 | class FullPlayer @JvmOverloads constructor( 20 | context: Context, 21 | attrs: AttributeSet? = null, 22 | defStyleAttr: Int = 0, 23 | defStyleRes: Int = 0 24 | ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) { 25 | 26 | private var initialMargin = IntArray(4) 27 | 28 | private var blendView: BlendView 29 | private var overlayDivider: OverlayDivider 30 | 31 | init { 32 | inflate(context, R.layout.layout_full_player, this) 33 | 34 | blendView = findViewById(R.id.blend_view) 35 | overlayDivider = findViewById(R.id.divider) 36 | 37 | blendView.setImageUri(context.getUriToDrawable(R.drawable.eg)) 38 | blendView.startRotationAnimation() 39 | clipToOutline = true 40 | } 41 | 42 | override fun dispatchApplyWindowInsets(platformInsets: WindowInsets): WindowInsets { 43 | if (initialMargin[3] != 0) return super.dispatchApplyWindowInsets(platformInsets) 44 | val insets = WindowInsetsCompat.toWindowInsetsCompat(platformInsets) 45 | val floatingInsets = insets.getInsets( 46 | WindowInsetsCompat.Type.systemBars() 47 | or WindowInsetsCompat.Type.displayCutout() 48 | ) 49 | Log.d(TAG, "marginBottom: ${marginBottom}, InsetsBottom: ${floatingInsets.bottom}, marginTop: ${floatingInsets.top}") 50 | if (floatingInsets.bottom != 0) { 51 | initialMargin = intArrayOf( 52 | marginLeft, 53 | marginTop + floatingInsets.top, 54 | marginRight, 55 | marginBottom + floatingInsets.bottom 56 | ) 57 | Log.d(TAG, "initTop: ${initialMargin[1]}") 58 | overlayDivider.updateLayoutParams { 59 | topMargin = initialMargin[1] + overlayDivider.marginTop 60 | } 61 | } 62 | return super.dispatchApplyWindowInsets(platformInsets) 63 | } 64 | 65 | companion object { 66 | const val TAG = "FullPlayer" 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/components/PreviewPlayer.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.components 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.util.Log 6 | import androidx.constraintlayout.widget.ConstraintLayout 7 | import com.google.android.material.button.MaterialButton 8 | import uk.akane.accord.R 9 | 10 | class PreviewPlayer @JvmOverloads constructor( 11 | context: Context, 12 | attrs: AttributeSet? = null, 13 | defStyleAttr: Int = 0, 14 | defStyleRes: Int = 0 15 | ) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) { 16 | private var controlMaterialButton: MaterialButton 17 | init { 18 | inflate(context, R.layout.layout_preview_player, this) 19 | controlMaterialButton = findViewById(R.id.control_btn) 20 | controlMaterialButton.setOnClickListener { 21 | Log.d("TAG","hi yes") 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/BrowseFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import uk.akane.accord.R 9 | 10 | class BrowseFragment: Fragment() { 11 | 12 | override fun onCreateView( 13 | inflater: LayoutInflater, 14 | container: ViewGroup?, 15 | savedInstanceState: Bundle? 16 | ): View? { 17 | val rootView = inflater.inflate(R.layout.fragment_browse, container, false) 18 | 19 | return rootView 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/HomeFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.activityViewModels 9 | import com.google.android.material.appbar.AppBarLayout 10 | import uk.akane.accord.R 11 | import uk.akane.accord.logic.applyOffsetListener 12 | import uk.akane.accord.logic.enableEdgeToEdgePaddingListener 13 | import uk.akane.accord.ui.viewmodels.AccordViewModel 14 | 15 | class HomeFragment: Fragment() { 16 | private val accordViewModel: AccordViewModel by activityViewModels() 17 | 18 | private lateinit var appBarLayout: AppBarLayout 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View? { 24 | val rootView = inflater.inflate(R.layout.fragment_home, container, false) 25 | 26 | appBarLayout = rootView.findViewById(R.id.appbarlayout) 27 | 28 | appBarLayout.enableEdgeToEdgePaddingListener() 29 | appBarLayout.applyOffsetListener() 30 | 31 | 32 | 33 | return rootView 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/LibraryFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.google.android.material.appbar.AppBarLayout 9 | import uk.akane.accord.R 10 | import uk.akane.accord.logic.applyOffsetListener 11 | import uk.akane.accord.logic.enableEdgeToEdgePaddingListener 12 | 13 | class LibraryFragment: Fragment() { 14 | private lateinit var appBarLayout: AppBarLayout 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View? { 20 | val rootView = inflater.inflate(R.layout.fragment_library, container, false) 21 | appBarLayout = rootView.findViewById(R.id.appbarlayout) 22 | 23 | appBarLayout.enableEdgeToEdgePaddingListener() 24 | appBarLayout.applyOffsetListener() 25 | 26 | return rootView 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/SearchFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.google.android.material.appbar.AppBarLayout 9 | import uk.akane.accord.R 10 | import uk.akane.accord.logic.applyOffsetListener 11 | import uk.akane.accord.logic.enableEdgeToEdgePaddingListener 12 | 13 | class SearchFragment: Fragment() { 14 | private lateinit var appBarLayout: AppBarLayout 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View? { 20 | val rootView = inflater.inflate(R.layout.fragment_search, container, false) 21 | appBarLayout = rootView.findViewById(R.id.appbarlayout) 22 | 23 | appBarLayout.enableEdgeToEdgePaddingListener() 24 | appBarLayout.applyOffsetListener() 25 | return rootView 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/ViewPagerContainerFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.viewpager2.widget.ViewPager2 10 | import uk.akane.accord.R 11 | import uk.akane.accord.logic.setCurrentItemInterpolated 12 | import uk.akane.accord.ui.MainActivity 13 | import uk.akane.accord.ui.adapters.ViewPagerAdapter 14 | 15 | class ViewPagerContainerFragment : Fragment() { 16 | 17 | private lateinit var viewPager2: ViewPager2 18 | private lateinit var viewPagerAdapter: ViewPagerAdapter 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? { 25 | val rootView = inflater.inflate(R.layout.fragment_viewpager_container, container, false) 26 | 27 | viewPager2 = rootView.findViewById(R.id.viewpager2) 28 | viewPagerAdapter = ViewPagerAdapter(childFragmentManager, lifecycle) 29 | 30 | viewPager2.adapter = viewPagerAdapter 31 | viewPager2.isUserInputEnabled = false 32 | viewPager2.offscreenPageLimit = 9999 33 | 34 | (requireActivity() as MainActivity).connectBottomNavigationView { 35 | Log.d("TAG", "upon selection") 36 | when (it.itemId) { 37 | R.id.home -> viewPager2.setCurrentItemInterpolated(0) 38 | R.id.browse -> viewPager2.setCurrentItemInterpolated(1) 39 | R.id.library -> viewPager2.setCurrentItemInterpolated(2) 40 | R.id.search -> viewPager2.setCurrentItemInterpolated(3) 41 | else -> throw IllegalArgumentException("Illegal itemId: ${it.itemId}") 42 | } 43 | true 44 | } 45 | 46 | return rootView 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/browse/AlbumFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments.browse 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | class AlbumFragment : Fragment() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/browse/ArtistFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments.browse 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | class ArtistFragment : Fragment() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/browse/DateFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments.browse 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | class DateFragment : Fragment() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/browse/GenreFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments.browse 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | class GenreFragment : Fragment() { 6 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/fragments/browse/SongFragment.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.fragments.browse 2 | 3 | import android.os.Bundle 4 | import android.util.Log 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.fragment.app.activityViewModels 10 | import androidx.lifecycle.Observer 11 | import androidx.media3.common.MediaItem 12 | import androidx.recyclerview.widget.LinearLayoutManager 13 | import androidx.recyclerview.widget.RecyclerView 14 | import uk.akane.accord.R 15 | import uk.akane.accord.ui.adapters.browse.SongAdapter 16 | import uk.akane.accord.ui.viewmodels.AccordViewModel 17 | 18 | class SongFragment : Fragment(), Observer> { 19 | 20 | private lateinit var recyclerView: RecyclerView 21 | private lateinit var songAdapter: SongAdapter 22 | private lateinit var layoutManager: LinearLayoutManager 23 | private val accordViewModel: AccordViewModel by activityViewModels() 24 | 25 | override fun onCreateView( 26 | inflater: LayoutInflater, 27 | container: ViewGroup?, 28 | savedInstanceState: Bundle? 29 | ): View? { 30 | val rootView = inflater.inflate(R.layout.fragment_browse_song, container, false) 31 | 32 | recyclerView = rootView.findViewById(R.id.rv) 33 | songAdapter = SongAdapter() 34 | layoutManager = LinearLayoutManager(requireContext()) 35 | recyclerView.adapter = songAdapter 36 | recyclerView.layoutManager = layoutManager 37 | 38 | accordViewModel.mediaItemList.observeForever(this) 39 | return rootView 40 | } 41 | 42 | override fun onDestroyView() { 43 | accordViewModel.mediaItemList.removeObserver(this) 44 | super.onDestroyView() 45 | } 46 | 47 | override fun onChanged(value: List) { 48 | Log.d("TAG", "CHANGED!!!!!!") 49 | songAdapter.update(value.toMutableList()) 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /app/src/main/java/uk/akane/accord/ui/viewmodels/AccordViewModel.kt: -------------------------------------------------------------------------------- 1 | package uk.akane.accord.ui.viewmodels 2 | 3 | import androidx.lifecycle.MutableLiveData 4 | import androidx.lifecycle.SavedStateHandle 5 | import androidx.lifecycle.ViewModel 6 | import androidx.media3.common.MediaItem 7 | import uk.akane.accord.logic.utils.BottomSheetUtils 8 | import uk.akane.libphonograph.items.Album 9 | import uk.akane.libphonograph.items.Artist 10 | import uk.akane.libphonograph.items.Date 11 | import uk.akane.libphonograph.items.FileNode 12 | import uk.akane.libphonograph.items.Genre 13 | 14 | class AccordViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { 15 | companion object { 16 | const val BOTTOM_SHEET_STATE_TOKEN = "bottomSheetState" 17 | const val BOTTOM_SHEET_PEEK_HEIGHT_TOKEN = "bottomSheetPeekHeight" 18 | const val BOTTOM_NAV_STATE_TOKEN = "bottomNavState" 19 | const val BOTTOM_NAV_TRANSLATION_TOKEN = "bottomNavTrans" 20 | } 21 | var bottomSheetState: BottomSheetUtils.ComponentState 22 | get() = savedStateHandle[BOTTOM_SHEET_STATE_TOKEN] ?: BottomSheetUtils.ComponentState.HIDDEN 23 | set(value) { 24 | savedStateHandle[BOTTOM_SHEET_STATE_TOKEN] = value 25 | } 26 | var bottomNavState: BottomSheetUtils.ComponentState 27 | get() = savedStateHandle[BOTTOM_NAV_STATE_TOKEN] ?: BottomSheetUtils.ComponentState.SHOWN 28 | set(value) { 29 | savedStateHandle[BOTTOM_NAV_STATE_TOKEN] = value 30 | } 31 | var bottomNavTranslation: Float 32 | get() = savedStateHandle[BOTTOM_NAV_TRANSLATION_TOKEN] ?: 0f 33 | set(value) { 34 | savedStateHandle[BOTTOM_NAV_TRANSLATION_TOKEN] = value 35 | } 36 | var bottomSheetPeekHeight: Int 37 | get() = savedStateHandle[BOTTOM_SHEET_PEEK_HEIGHT_TOKEN] ?: 0 38 | set(value) { 39 | savedStateHandle[BOTTOM_SHEET_PEEK_HEIGHT_TOKEN] = value 40 | } 41 | val mediaItemList: MutableLiveData> = MutableLiveData() 42 | val albumItemList: MutableLiveData>> = MutableLiveData() 43 | val albumArtistItemList: MutableLiveData>> = MutableLiveData() 44 | val artistItemList: MutableLiveData>> = MutableLiveData() 45 | val genreItemList: MutableLiveData>> = MutableLiveData() 46 | val dateItemList: MutableLiveData>> = MutableLiveData() 47 | val folderStructure: MutableLiveData> = MutableLiveData() 48 | val shallowFolderStructure: MutableLiveData> = MutableLiveData() 49 | val allFolderSet: MutableLiveData> = MutableLiveData() 50 | } -------------------------------------------------------------------------------- /app/src/main/res/color/cl_action_btn_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/cl_ask_perm_btn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/cl_ask_perm_text.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/color/cl_bottom_nav_label.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/eg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/drawable/eg.jpeg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_album_stack.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 17 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bottom_nav_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bottom_nav_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bottom_nav_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_bottom_nav_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_default_cover.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 15 | 20 | 25 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground_variant.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 15 | 16 | 21 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_master_play.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_master_shuffle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_horiz.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_note.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_opened_books.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prop_next.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prop_pause.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prop_play.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_prop_prev.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_sort_btn.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 13 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/selected_chip_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shape_bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 10 | 11 | 12 | 15 | 16 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/slider_container_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/splash_anim.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 14 | 19 | 20 | 21 | 24 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/top_app_bar_divider.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/font/inter_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/font/inter_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/font/inter_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/font/inter_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/inter_semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/font/inter_semibold.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 20 | 21 | 28 | 29 | 38 | 39 | 46 | 47 | 48 | 49 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_setup_wizard.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_browse_song.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_library.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 23 | 24 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 14 | 15 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_viewpager_container.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_welcome_page.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | 21 | 33 | 34 | 45 | 46 | 63 | 64 | 75 | 76 | 90 | 91 | 104 | 105 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_floating_panel.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_full_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 31 | 32 | 41 | 42 | 43 | 44 | 57 | 58 | 71 | 72 | 79 | 80 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_item_header.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_master_control.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 22 | 23 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_preview_player.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 32 | 33 | 44 | 45 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_song_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 19 | 20 | 38 | 39 | 56 | 57 | 69 | 70 | 82 | 83 | -------------------------------------------------------------------------------- /app/src/main/res/layout/view_blend.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 29 | 30 | 36 | 37 | 43 | 44 | 52 | 53 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_browse.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/resources.properties: -------------------------------------------------------------------------------- 1 | unqualifiedResLocale=en-US -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FA2D48 4 | #1AFA2D48 5 | 6 | #1A000000 7 | 8 | #000000 9 | #D9FFFFFF 10 | #0DFFFFFF 11 | #80FFFFFF 12 | 13 | #000000 14 | #111114 15 | 16 | #30FFFFFF 17 | #12FFFFFF 18 | 19 | #66000000 20 | 21 | #000000 22 | #111114 23 | #38383A 24 | #98989F 25 | #00000000 26 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 协律 4 | 搜索 5 | 资料库 6 | 浏览 7 | 主页 8 | 未在播放 9 | 继续 10 | 欢迎使用 11 | 在您的安卓设备上无广告播放本地歌曲和播放列表,下载音乐离线收听。通过空间音频,感受环绕音效带来的沉浸式体验。 12 | 协律是一款自由且脱机使用的应用程序。协律不会将您的任何数据发送至网络,也不会将其用于任何其他用途。协律的源代码已在线发布,并遵循 GPL-3.0 许可证,但某些限制适用。 13 | 检查源代码 … 14 | 需要权限 15 | 我们先需要这些权限来正常播放您的音乐。请检查各项的描述。 16 | 储存 17 | 允许我们访问您的全部播放清单和音乐文件 18 | 音乐 19 | 相册 20 | 允许我们正确地为您的专辑识别文件封面 21 | 如果没有相册权限,我们只能模糊地读取您的专辑封面。 22 | 获取 23 | 已获取 24 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FA233B 4 | #FB5E76 5 | #FA233B 6 | #FFFFFF 7 | #1AFA233B 8 | #1AFFFFFF 9 | #1A000000 10 | 11 | #FFFFFF 12 | #D9000000 13 | #0D000000 14 | #80000000 15 | 16 | #30000000 17 | #12000000 18 | 19 | #F8F8FC 20 | #FFFFFF 21 | 22 | #000 23 | 24 | 25 | #40000000 26 | 27 | 28 | #E5E5E5 29 | #FCFCFC 30 | #C6C6C8 31 | #8A8A8E 32 | #33000000 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 67dp 4 | 100dp 5 | 8dp 6 | 52dp 7 | 56dp 8 | 0dp 9 | 16dp 10 | 110dp 11 | 58dp 12 | 0.3dp 13 | 29dp 14 | 0dp 15 | 40dp 16 | 8dp 17 | 36dp 18 | 10dp 19 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Accord 3 | Search 4 | Library 5 | Browse 6 | Home 7 | Not playing 8 | Top Picks 9 | Continue 10 | Welcome to 11 | Play local songs and playlists ad-free on your Android device. Download music to listen offline. Experience sound all around you with Spatial Audio. 12 | Accord is a freeware and an offline app. Accord will not send any of your data online or use them for any purposes. Accord source code is published online with GPL-3.0 licenses. Certain restrictions are applied. 13 | Check source code … 14 | Need permissions 15 | Permissions are needed in order to play your music normally. Please check the description for each entry. 16 | Storage 17 | Allow access to your music 18 | Music 19 | Album 20 | Allow access to your album cover 21 | Without the album permission, we can only read your album cover vaguely. 22 | Allow 23 | Allowed 24 | Sort 25 | Songs 26 | Albums 27 | Artists 28 | Genres 29 | Dates 30 | Play 31 | Shuffle 32 | Playlists 33 | -------------------------------------------------------------------------------- /app/src/main/res/values/styleables.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 21 | 30 | 34 | 47 | 54 | 57 | 60 | 75 | 78 | 81 | 92 | 96 | 99 | 112 | 115 | 118 | 124 | 127 | 130 | 133 | 151 | 156 | 159 | 167 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 18 | 19 |