├── .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 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
5 |
18 |
21 |
30 |
34 |
35 |
47 |
48 |
54 |
57 |
60 |
61 |
62 |
75 |
78 |
81 |
92 |
96 |
99 |
100 |
101 |
102 |
112 |
115 |
118 |
124 |
127 |
130 |
133 |
134 |
135 |
136 |
151 |
152 |
156 |
159 |
160 |
167 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.android.library) apply false
5 | alias(libs.plugins.kotlin.android) apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | activityKtxVersion = "1.9.1"
3 | agp = "8.6.0-rc01"
4 | hiddenapibypass = "4.3"
5 | coil = "3.0.0-alpha06"
6 | coreSplashscreen = "1.2.0-alpha01"
7 | fragmentKtx = "1.8.2"
8 | kotlin = "2.0.20-RC2"
9 | coreKtx = "1.13.1"
10 | appcompat = "1.7.0"
11 | lifecycleViewmodelKtx = "2.8.4"
12 | material = "1.13.0-alpha05"
13 | activity = "1.9.1"
14 | constraintlayout = "2.1.4"
15 | media3Exoplayer = "1.4.0"
16 | paletteKtx = "1.0.0"
17 | window = "1.3.0"
18 |
19 | [libraries]
20 | activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activityKtxVersion" }
21 | hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
22 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
23 | androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
24 | androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragmentKtx" }
25 | androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
26 | androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
27 | androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" }
28 | androidx-window = { module = "androidx.window:window", version.ref = "window" }
29 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
30 | coil = { module = "io.coil-kt.coil3:coil", version.ref = "coil" }
31 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
32 | androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
33 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
34 |
35 | [plugins]
36 | android-library = { id = "com.android.library", version.ref = "agp" }
37 | android-application = { id = "com.android.application", version.ref = "agp" }
38 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
39 |
40 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FoedusProgramme/Accord/b51d1d0f49558e19dca0bfbc97f331db9c71e28b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Aug 22 00:59:59 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | maven { url = uri("https://jitpack.io") }
20 | }
21 | }
22 |
23 | rootProject.name = "Accord"
24 | include(":libphonograph:libPhonograph", ":app", ":Cupertino:Cupertino")
--------------------------------------------------------------------------------