Compare commits

...

10 Commits
0.1.0 ... 0.3.3

Author SHA1 Message Date
d6c5e937df MOAR 2025-08-16 02:35:12 -06:00
829bbbff7a More cleanup 2025-08-16 02:34:29 -06:00
e1ebf412bd Add .gitignore 2025-08-16 02:33:43 -06:00
5c133b655e Remove files I dont need committed... 2025-08-16 02:33:11 -06:00
cc1edbc65c 0.3.3 2025-08-16 02:31:52 -06:00
ca770b9db3 0.3.2 - Optimizations 2025-08-16 00:48:38 -06:00
7edb7c8191 0.3.1 - Bugfix for status bar 2025-08-15 19:38:01 -06:00
1ca6b33882 Build 2025-08-15 19:31:35 -06:00
bd6b5cc652 0.3.0 - Filtering and Better Scales 2025-08-15 19:30:50 -06:00
6e16a30429 0.2.0 quick fixes 2025-08-15 14:51:30 -06:00
109 changed files with 759 additions and 3287 deletions

34
.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Android Studio generated files and folders
captures/
.externalNativeBuild/
.cxx/
*.aab
*.apk
output-metadata.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks
*.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling
*.hprof

Binary file not shown.

View File

@@ -1,769 +0,0 @@
package org.gradle.accessors.dm;
import org.gradle.api.NonNullApi;
import org.gradle.api.artifacts.MinimalExternalModuleDependency;
import org.gradle.plugin.use.PluginDependency;
import org.gradle.api.artifacts.ExternalModuleDependencyBundle;
import org.gradle.api.artifacts.MutableVersionConstraint;
import org.gradle.api.provider.Provider;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.internal.catalog.AbstractExternalDependencyFactory;
import org.gradle.api.internal.catalog.DefaultVersionCatalog;
import java.util.Map;
import org.gradle.api.internal.attributes.ImmutableAttributesFactory;
import org.gradle.api.internal.artifacts.dsl.CapabilityNotationParser;
import javax.inject.Inject;
/**
* A catalog of dependencies accessible via the {@code libs} extension.
*/
@NonNullApi
public class LibrariesForLibs extends AbstractExternalDependencyFactory {
private final AbstractExternalDependencyFactory owner = this;
private final AndroidxLibraryAccessors laccForAndroidxLibraryAccessors = new AndroidxLibraryAccessors(owner);
private final CoilLibraryAccessors laccForCoilLibraryAccessors = new CoilLibraryAccessors(owner);
private final KotlinxLibraryAccessors laccForKotlinxLibraryAccessors = new KotlinxLibraryAccessors(owner);
private final VersionAccessors vaccForVersionAccessors = new VersionAccessors(providers, config);
private final BundleAccessors baccForBundleAccessors = new BundleAccessors(objects, providers, config, attributesFactory, capabilityNotationParser);
private final PluginAccessors paccForPluginAccessors = new PluginAccessors(providers, config);
@Inject
public LibrariesForLibs(DefaultVersionCatalog config, ProviderFactory providers, ObjectFactory objects, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) {
super(config, providers, objects, attributesFactory, capabilityNotationParser);
}
/**
* Dependency provider for <b>junit</b> with <b>junit:junit</b> coordinates and
* with version reference <b>junit</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getJunit() {
return create("junit");
}
/**
* Dependency provider for <b>mpandroidchart</b> with <b>com.github.PhilJay:MPAndroidChart</b> coordinates and
* with version <b>v3.1.0</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getMpandroidchart() {
return create("mpandroidchart");
}
/**
* Group of libraries at <b>androidx</b>
*/
public AndroidxLibraryAccessors getAndroidx() {
return laccForAndroidxLibraryAccessors;
}
/**
* Group of libraries at <b>coil</b>
*/
public CoilLibraryAccessors getCoil() {
return laccForCoilLibraryAccessors;
}
/**
* Group of libraries at <b>kotlinx</b>
*/
public KotlinxLibraryAccessors getKotlinx() {
return laccForKotlinxLibraryAccessors;
}
/**
* Group of versions at <b>versions</b>
*/
public VersionAccessors getVersions() {
return vaccForVersionAccessors;
}
/**
* Group of bundles at <b>bundles</b>
*/
public BundleAccessors getBundles() {
return baccForBundleAccessors;
}
/**
* Group of plugins at <b>plugins</b>
*/
public PluginAccessors getPlugins() {
return paccForPluginAccessors;
}
public static class AndroidxLibraryAccessors extends SubDependencyFactory {
private final AndroidxActivityLibraryAccessors laccForAndroidxActivityLibraryAccessors = new AndroidxActivityLibraryAccessors(owner);
private final AndroidxComposeLibraryAccessors laccForAndroidxComposeLibraryAccessors = new AndroidxComposeLibraryAccessors(owner);
private final AndroidxCoreLibraryAccessors laccForAndroidxCoreLibraryAccessors = new AndroidxCoreLibraryAccessors(owner);
private final AndroidxEspressoLibraryAccessors laccForAndroidxEspressoLibraryAccessors = new AndroidxEspressoLibraryAccessors(owner);
private final AndroidxLifecycleLibraryAccessors laccForAndroidxLifecycleLibraryAccessors = new AndroidxLifecycleLibraryAccessors(owner);
private final AndroidxNavigationLibraryAccessors laccForAndroidxNavigationLibraryAccessors = new AndroidxNavigationLibraryAccessors(owner);
private final AndroidxRoomLibraryAccessors laccForAndroidxRoomLibraryAccessors = new AndroidxRoomLibraryAccessors(owner);
private final AndroidxUiLibraryAccessors laccForAndroidxUiLibraryAccessors = new AndroidxUiLibraryAccessors(owner);
public AndroidxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>junit</b> with <b>androidx.test.ext:junit</b> coordinates and
* with version reference <b>junitVersion</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getJunit() {
return create("androidx.junit");
}
/**
* Dependency provider for <b>material3</b> with <b>androidx.compose.material3:material3</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getMaterial3() {
return create("androidx.material3");
}
/**
* Group of libraries at <b>androidx.activity</b>
*/
public AndroidxActivityLibraryAccessors getActivity() {
return laccForAndroidxActivityLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.compose</b>
*/
public AndroidxComposeLibraryAccessors getCompose() {
return laccForAndroidxComposeLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.core</b>
*/
public AndroidxCoreLibraryAccessors getCore() {
return laccForAndroidxCoreLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.espresso</b>
*/
public AndroidxEspressoLibraryAccessors getEspresso() {
return laccForAndroidxEspressoLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.lifecycle</b>
*/
public AndroidxLifecycleLibraryAccessors getLifecycle() {
return laccForAndroidxLifecycleLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.navigation</b>
*/
public AndroidxNavigationLibraryAccessors getNavigation() {
return laccForAndroidxNavigationLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.room</b>
*/
public AndroidxRoomLibraryAccessors getRoom() {
return laccForAndroidxRoomLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.ui</b>
*/
public AndroidxUiLibraryAccessors getUi() {
return laccForAndroidxUiLibraryAccessors;
}
}
public static class AndroidxActivityLibraryAccessors extends SubDependencyFactory {
public AndroidxActivityLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>compose</b> with <b>androidx.activity:activity-compose</b> coordinates and
* with version reference <b>activityCompose</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCompose() {
return create("androidx.activity.compose");
}
}
public static class AndroidxComposeLibraryAccessors extends SubDependencyFactory {
public AndroidxComposeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>bom</b> with <b>androidx.compose:compose-bom</b> coordinates and
* with version reference <b>composeBom</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getBom() {
return create("androidx.compose.bom");
}
}
public static class AndroidxCoreLibraryAccessors extends SubDependencyFactory {
public AndroidxCoreLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>ktx</b> with <b>androidx.core:core-ktx</b> coordinates and
* with version reference <b>coreKtx</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getKtx() {
return create("androidx.core.ktx");
}
}
public static class AndroidxEspressoLibraryAccessors extends SubDependencyFactory {
public AndroidxEspressoLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>core</b> with <b>androidx.test.espresso:espresso-core</b> coordinates and
* with version reference <b>espressoCore</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCore() {
return create("androidx.espresso.core");
}
}
public static class AndroidxLifecycleLibraryAccessors extends SubDependencyFactory {
private final AndroidxLifecycleRuntimeLibraryAccessors laccForAndroidxLifecycleRuntimeLibraryAccessors = new AndroidxLifecycleRuntimeLibraryAccessors(owner);
private final AndroidxLifecycleViewmodelLibraryAccessors laccForAndroidxLifecycleViewmodelLibraryAccessors = new AndroidxLifecycleViewmodelLibraryAccessors(owner);
public AndroidxLifecycleLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Group of libraries at <b>androidx.lifecycle.runtime</b>
*/
public AndroidxLifecycleRuntimeLibraryAccessors getRuntime() {
return laccForAndroidxLifecycleRuntimeLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.lifecycle.viewmodel</b>
*/
public AndroidxLifecycleViewmodelLibraryAccessors getViewmodel() {
return laccForAndroidxLifecycleViewmodelLibraryAccessors;
}
}
public static class AndroidxLifecycleRuntimeLibraryAccessors extends SubDependencyFactory {
public AndroidxLifecycleRuntimeLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>ktx</b> with <b>androidx.lifecycle:lifecycle-runtime-ktx</b> coordinates and
* with version reference <b>lifecycleRuntimeKtx</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getKtx() {
return create("androidx.lifecycle.runtime.ktx");
}
}
public static class AndroidxLifecycleViewmodelLibraryAccessors extends SubDependencyFactory {
public AndroidxLifecycleViewmodelLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>compose</b> with <b>androidx.lifecycle:lifecycle-viewmodel-compose</b> coordinates and
* with version reference <b>viewmodel</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCompose() {
return create("androidx.lifecycle.viewmodel.compose");
}
}
public static class AndroidxNavigationLibraryAccessors extends SubDependencyFactory {
public AndroidxNavigationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>compose</b> with <b>androidx.navigation:navigation-compose</b> coordinates and
* with version reference <b>navigation</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCompose() {
return create("androidx.navigation.compose");
}
}
public static class AndroidxRoomLibraryAccessors extends SubDependencyFactory {
public AndroidxRoomLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>compiler</b> with <b>androidx.room:room-compiler</b> coordinates and
* with version reference <b>room</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCompiler() {
return create("androidx.room.compiler");
}
/**
* Dependency provider for <b>ktx</b> with <b>androidx.room:room-ktx</b> coordinates and
* with version reference <b>room</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getKtx() {
return create("androidx.room.ktx");
}
/**
* Dependency provider for <b>runtime</b> with <b>androidx.room:room-runtime</b> coordinates and
* with version reference <b>room</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getRuntime() {
return create("androidx.room.runtime");
}
}
public static class AndroidxUiLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier {
private final AndroidxUiTestLibraryAccessors laccForAndroidxUiTestLibraryAccessors = new AndroidxUiTestLibraryAccessors(owner);
private final AndroidxUiToolingLibraryAccessors laccForAndroidxUiToolingLibraryAccessors = new AndroidxUiToolingLibraryAccessors(owner);
public AndroidxUiLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>ui</b> with <b>androidx.compose.ui:ui</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> asProvider() {
return create("androidx.ui");
}
/**
* Dependency provider for <b>graphics</b> with <b>androidx.compose.ui:ui-graphics</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getGraphics() {
return create("androidx.ui.graphics");
}
/**
* Group of libraries at <b>androidx.ui.test</b>
*/
public AndroidxUiTestLibraryAccessors getTest() {
return laccForAndroidxUiTestLibraryAccessors;
}
/**
* Group of libraries at <b>androidx.ui.tooling</b>
*/
public AndroidxUiToolingLibraryAccessors getTooling() {
return laccForAndroidxUiToolingLibraryAccessors;
}
}
public static class AndroidxUiTestLibraryAccessors extends SubDependencyFactory {
public AndroidxUiTestLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>junit4</b> with <b>androidx.compose.ui:ui-test-junit4</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getJunit4() {
return create("androidx.ui.test.junit4");
}
/**
* Dependency provider for <b>manifest</b> with <b>androidx.compose.ui:ui-test-manifest</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getManifest() {
return create("androidx.ui.test.manifest");
}
}
public static class AndroidxUiToolingLibraryAccessors extends SubDependencyFactory implements DependencyNotationSupplier {
public AndroidxUiToolingLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>tooling</b> with <b>androidx.compose.ui:ui-tooling</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> asProvider() {
return create("androidx.ui.tooling");
}
/**
* Dependency provider for <b>preview</b> with <b>androidx.compose.ui:ui-tooling-preview</b> coordinates and
* with <b>no version specified</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getPreview() {
return create("androidx.ui.tooling.preview");
}
}
public static class CoilLibraryAccessors extends SubDependencyFactory {
public CoilLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>compose</b> with <b>io.coil-kt:coil-compose</b> coordinates and
* with version reference <b>coil</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getCompose() {
return create("coil.compose");
}
}
public static class KotlinxLibraryAccessors extends SubDependencyFactory {
private final KotlinxCoroutinesLibraryAccessors laccForKotlinxCoroutinesLibraryAccessors = new KotlinxCoroutinesLibraryAccessors(owner);
private final KotlinxSerializationLibraryAccessors laccForKotlinxSerializationLibraryAccessors = new KotlinxSerializationLibraryAccessors(owner);
public KotlinxLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Group of libraries at <b>kotlinx.coroutines</b>
*/
public KotlinxCoroutinesLibraryAccessors getCoroutines() {
return laccForKotlinxCoroutinesLibraryAccessors;
}
/**
* Group of libraries at <b>kotlinx.serialization</b>
*/
public KotlinxSerializationLibraryAccessors getSerialization() {
return laccForKotlinxSerializationLibraryAccessors;
}
}
public static class KotlinxCoroutinesLibraryAccessors extends SubDependencyFactory {
public KotlinxCoroutinesLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>android</b> with <b>org.jetbrains.kotlinx:kotlinx-coroutines-android</b> coordinates and
* with version reference <b>kotlinxCoroutines</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getAndroid() {
return create("kotlinx.coroutines.android");
}
}
public static class KotlinxSerializationLibraryAccessors extends SubDependencyFactory {
public KotlinxSerializationLibraryAccessors(AbstractExternalDependencyFactory owner) { super(owner); }
/**
* Dependency provider for <b>json</b> with <b>org.jetbrains.kotlinx:kotlinx-serialization-json</b> coordinates and
* with version reference <b>kotlinxSerialization</b>
* <p>
* This dependency was declared in catalog libs.versions.toml
*/
public Provider<MinimalExternalModuleDependency> getJson() {
return create("kotlinx.serialization.json");
}
}
public static class VersionAccessors extends VersionFactory {
public VersionAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Version alias <b>activityCompose</b> with value <b>1.10.1</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getActivityCompose() { return getVersion("activityCompose"); }
/**
* Version alias <b>agp</b> with value <b>8.9.1</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getAgp() { return getVersion("agp"); }
/**
* Version alias <b>coil</b> with value <b>2.7.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getCoil() { return getVersion("coil"); }
/**
* Version alias <b>composeBom</b> with value <b>2024.09.00</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getComposeBom() { return getVersion("composeBom"); }
/**
* Version alias <b>coreKtx</b> with value <b>1.15.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getCoreKtx() { return getVersion("coreKtx"); }
/**
* Version alias <b>espressoCore</b> with value <b>3.7.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getEspressoCore() { return getVersion("espressoCore"); }
/**
* Version alias <b>junit</b> with value <b>4.13.2</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getJunit() { return getVersion("junit"); }
/**
* Version alias <b>junitVersion</b> with value <b>1.3.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getJunitVersion() { return getVersion("junitVersion"); }
/**
* Version alias <b>kotlin</b> with value <b>2.0.21</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getKotlin() { return getVersion("kotlin"); }
/**
* Version alias <b>kotlinxCoroutines</b> with value <b>1.9.0</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getKotlinxCoroutines() { return getVersion("kotlinxCoroutines"); }
/**
* Version alias <b>kotlinxSerialization</b> with value <b>1.7.1</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getKotlinxSerialization() { return getVersion("kotlinxSerialization"); }
/**
* Version alias <b>ksp</b> with value <b>2.0.21-1.0.25</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getKsp() { return getVersion("ksp"); }
/**
* Version alias <b>lifecycleRuntimeKtx</b> with value <b>2.9.2</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getLifecycleRuntimeKtx() { return getVersion("lifecycleRuntimeKtx"); }
/**
* Version alias <b>navigation</b> with value <b>2.8.4</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getNavigation() { return getVersion("navigation"); }
/**
* Version alias <b>room</b> with value <b>2.6.1</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getRoom() { return getVersion("room"); }
/**
* Version alias <b>viewmodel</b> with value <b>2.9.2</b>
* <p>
* If the version is a rich version and cannot be represented as a
* single version string, an empty string is returned.
* <p>
* This version was declared in catalog libs.versions.toml
*/
public Provider<String> getViewmodel() { return getVersion("viewmodel"); }
}
public static class BundleAccessors extends BundleFactory {
public BundleAccessors(ObjectFactory objects, ProviderFactory providers, DefaultVersionCatalog config, ImmutableAttributesFactory attributesFactory, CapabilityNotationParser capabilityNotationParser) { super(objects, providers, config, attributesFactory, capabilityNotationParser); }
}
public static class PluginAccessors extends PluginFactory {
private final AndroidPluginAccessors paccForAndroidPluginAccessors = new AndroidPluginAccessors(providers, config);
private final KotlinPluginAccessors paccForKotlinPluginAccessors = new KotlinPluginAccessors(providers, config);
public PluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Plugin provider for <b>ksp</b> with plugin id <b>com.google.devtools.ksp</b> and
* with version reference <b>ksp</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getKsp() { return createPlugin("ksp"); }
/**
* Group of plugins at <b>plugins.android</b>
*/
public AndroidPluginAccessors getAndroid() {
return paccForAndroidPluginAccessors;
}
/**
* Group of plugins at <b>plugins.kotlin</b>
*/
public KotlinPluginAccessors getKotlin() {
return paccForKotlinPluginAccessors;
}
}
public static class AndroidPluginAccessors extends PluginFactory {
public AndroidPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Plugin provider for <b>android.application</b> with plugin id <b>com.android.application</b> and
* with version reference <b>agp</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getApplication() { return createPlugin("android.application"); }
}
public static class KotlinPluginAccessors extends PluginFactory {
public KotlinPluginAccessors(ProviderFactory providers, DefaultVersionCatalog config) { super(providers, config); }
/**
* Plugin provider for <b>kotlin.android</b> with plugin id <b>org.jetbrains.kotlin.android</b> and
* with version reference <b>kotlin</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getAndroid() { return createPlugin("kotlin.android"); }
/**
* Plugin provider for <b>kotlin.compose</b> with plugin id <b>org.jetbrains.kotlin.plugin.compose</b> and
* with version reference <b>kotlin</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getCompose() { return createPlugin("kotlin.compose"); }
/**
* Plugin provider for <b>kotlin.serialization</b> with plugin id <b>org.jetbrains.kotlin.plugin.serialization</b> and
* with version reference <b>kotlin</b>
* <p>
* This plugin was declared in catalog libs.versions.toml
*/
public Provider<PluginDependency> getSerialization() { return createPlugin("kotlin.serialization"); }
}
}

View File

@@ -1,2 +0,0 @@
#Fri Aug 15 14:37:16 MDT 2025
gradle.version=8.11.1

View File

@@ -1,2 +0,0 @@
#Fri Aug 15 12:29:02 MDT 2025
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home

Binary file not shown.

View File

@@ -1,87 +0,0 @@
kotlin version: 2.0.21
error message: java.lang.IllegalStateException: Storage for [/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] is already registered
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.set(PersistentStorage.kt:96)
at org.jetbrains.kotlin.incremental.LookupStorage.addFileIfNeeded(LookupStorage.kt:165)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll$lambda$4(LookupStorage.kt:117)
at org.jetbrains.kotlin.utils.CollectionsKt.keysToMap(collections.kt:117)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll(LookupStorage.kt:117)
at org.jetbrains.kotlin.incremental.BuildUtilKt.update(buildUtil.kt:134)
at com.google.devtools.ksp.LookupStorageWrapperImpl.update(IncrementalContext.kt:231)
at com.google.devtools.ksp.common.IncrementalContextBase.updateLookupCache(IncrementalContextBase.kt:133)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCaches(IncrementalContextBase.kt:365)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCachesAndOutputs(IncrementalContextBase.kt:471)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:362)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Suppressed: java.lang.Exception: Storage[/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] registration stack trace
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageIfExists(LazyStorage.kt:53)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.get(LazyStorage.kt:76)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.get(PersistentStorage.kt:92)
at org.jetbrains.kotlin.incremental.LookupStorage.get(LookupStorage.kt:99)
at com.google.devtools.ksp.LookupStorageWrapperImpl.get(IncrementalContext.kt:224)
at com.google.devtools.ksp.common.IncrementalContextBase.calcDirtyFiles(IncrementalContextBase.kt:234)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:196)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
... 23 more

View File

@@ -1,87 +0,0 @@
kotlin version: 2.0.21
error message: java.lang.IllegalStateException: Storage for [/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] is already registered
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:410)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageOrCreateNew(LazyStorage.kt:59)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.set(LazyStorage.kt:80)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.set(PersistentStorage.kt:96)
at org.jetbrains.kotlin.incremental.LookupStorage.addFileIfNeeded(LookupStorage.kt:165)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll$lambda$4(LookupStorage.kt:117)
at org.jetbrains.kotlin.utils.CollectionsKt.keysToMap(collections.kt:117)
at org.jetbrains.kotlin.incremental.LookupStorage.addAll(LookupStorage.kt:117)
at org.jetbrains.kotlin.incremental.BuildUtilKt.update(buildUtil.kt:134)
at com.google.devtools.ksp.LookupStorageWrapperImpl.update(IncrementalContext.kt:231)
at com.google.devtools.ksp.common.IncrementalContextBase.updateLookupCache(IncrementalContextBase.kt:133)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCaches(IncrementalContextBase.kt:365)
at com.google.devtools.ksp.common.IncrementalContextBase.updateCachesAndOutputs(IncrementalContextBase.kt:471)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:362)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.repeatAnalysisIfNeeded(KotlinToJVMBytecodeCompiler.kt:282)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200)
at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:714)
at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Suppressed: java.lang.Exception: Storage[/Users/atridad/Developer/personal/OpenClimb/app/build/kspCaches/debug/symbolLookups/id-to-file.tab] registration stack trace
at org.jetbrains.kotlin.com.intellij.util.io.FilePageCache.registerPagedFileStorage(FilePageCache.java:437)
at org.jetbrains.kotlin.com.intellij.util.io.PagedFileStorage.<init>(PagedFileStorage.java:72)
at org.jetbrains.kotlin.com.intellij.util.io.ResizeableMappedFile.<init>(ResizeableMappedFile.java:55)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentBTreeEnumerator.<init>(PersistentBTreeEnumerator.java:128)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentEnumerator.createDefaultEnumerator(PersistentEnumerator.java:52)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:165)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapImpl.<init>(PersistentMapImpl.java:140)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.buildImplementation(PersistentMapBuilder.java:88)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentMapBuilder.build(PersistentMapBuilder.java:71)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:45)
at org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap.<init>(PersistentHashMap.java:71)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.createMap(LazyStorage.kt:62)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.getStorageIfExists(LazyStorage.kt:53)
at org.jetbrains.kotlin.incremental.storage.LazyStorage.get(LazyStorage.kt:76)
at org.jetbrains.kotlin.incremental.storage.PersistentStorageWrapper.get(PersistentStorage.kt:92)
at org.jetbrains.kotlin.incremental.LookupStorage.get(LookupStorage.kt:99)
at com.google.devtools.ksp.LookupStorageWrapperImpl.get(IncrementalContext.kt:224)
at com.google.devtools.ksp.common.IncrementalContextBase.calcDirtyFiles(IncrementalContextBase.kt:234)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:196)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
... 23 more

View File

@@ -1,3 +0,0 @@
{
"java.configuration.updateBuildConfiguration": "disabled"
}

View File

@@ -14,15 +14,15 @@ android {
applicationId = "com.atridad.openclimb" applicationId = "com.atridad.openclimb"
minSdk = 31 minSdk = 31
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 6
versionName = "0.1.0" versionName = "0.3.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
release { release {
isMinifyEnabled = false isMinifyEnabled = true
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro" "proguard-rules.pro"

Binary file not shown.

View File

@@ -1,37 +0,0 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.atridad.openclimb",
"variantName": "release",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "0.1.0",
"outputFile": "app-release.apk"
}
],
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 31
}

View File

@@ -9,12 +9,10 @@ import java.time.LocalDateTime
@Serializable @Serializable
enum class AttemptResult { enum class AttemptResult {
SUCCESS, // Completed the problem/route SUCCESS,
FALL, // Fell but made progress FALL,
NO_PROGRESS, // Couldn't make meaningful progress NO_PROGRESS,
FLASH, // Completed on first try FLASH,
REDPOINT, // Completed after previous attempts
ONSIGHT // Completed on first try without prior knowledge
} }
@Entity( @Entity(

View File

@@ -31,10 +31,10 @@ data class ClimbSession(
@PrimaryKey @PrimaryKey
val id: String, val id: String,
val gymId: String, val gymId: String,
val date: String, // ISO date string val date: String,
val startTime: String? = null, // When session was started val startTime: String? = null,
val endTime: String? = null, // When session was completed val endTime: String? = null,
val duration: Long? = null, // Duration in minutes (calculated when completed) val duration: Long? = null,
val status: SessionStatus = SessionStatus.ACTIVE, val status: SessionStatus = SessionStatus.ACTIVE,
val notes: String? = null, val notes: String? = null,
val createdAt: String, val createdAt: String,
@@ -65,7 +65,7 @@ data class ClimbSession(
val start = LocalDateTime.parse(startTime) val start = LocalDateTime.parse(startTime)
val end = LocalDateTime.parse(endTime) val end = LocalDateTime.parse(endTime)
java.time.Duration.between(start, end).toMinutes() java.time.Duration.between(start, end).toMinutes()
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
} else null } else null

View File

@@ -5,5 +5,13 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class ClimbType { enum class ClimbType {
ROPE, ROPE,
BOULDER BOULDER;
/**
* Get the display name
*/
fun getDisplayName(): String = when (this) {
ROPE -> "Rope"
BOULDER -> "Bouldering"
}
} }

View File

@@ -4,23 +4,68 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
enum class DifficultySystem { enum class DifficultySystem {
// Rope climbing systems // Bouldering
YDS, // Yosemite Decimal System (5.1 - 5.15d) V_SCALE, // V-Scale (VB - V17)
FRENCH, // French system (3 - 9c+) FONT, // Fontainebleau (3 - 8C+)
UIAA, // UIAA system (I - XII+)
BRITISH, // British system (Mod - E11)
// Bouldering systems // Rope
V_SCALE, // V-Scale (VB - V17) YDS, // Yosemite Decimal System (5.0 - 5.15d)
FONT, // Fontainebleau (3 - 9A+)
// Custom system for gyms that use their own colors/naming // Custom difficulty systems
CUSTOM CUSTOM;
/**
* Get the display name for the UI
*/
fun getDisplayName(): String = when (this) {
V_SCALE -> "V Scale"
FONT -> "Font Scale"
YDS -> "YDS (Yosemite)"
CUSTOM -> "Custom"
}
/**
* Check if this system is for bouldering
*/
fun isBoulderingSystem(): Boolean = when (this) {
V_SCALE, FONT -> true
YDS -> false
CUSTOM -> true // Custom is available for all
}
/**
* Check if this system is for rope climbing
*/
fun isRopeSystem(): Boolean = when (this) {
YDS -> true
V_SCALE, FONT -> false
CUSTOM -> true
}
/**
* Get available grades for this system
*/
fun getAvailableGrades(): List<String> = when (this) {
V_SCALE -> listOf("VB", "V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13", "V14", "V15", "V16", "V17")
FONT -> listOf("3", "4A", "4B", "4C", "5A", "5B", "5C", "6A", "6A+", "6B", "6B+", "6C", "6C+", "7A", "7A+", "7B", "7B+", "7C", "7C+", "8A", "8A+", "8B", "8B+", "8C", "8C+")
YDS -> listOf("5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "5.10a", "5.10b", "5.10c", "5.10d", "5.11a", "5.11b", "5.11c", "5.11d", "5.12a", "5.12b", "5.12c", "5.12d", "5.13a", "5.13b", "5.13c", "5.13d", "5.14a", "5.14b", "5.14c", "5.14d", "5.15a", "5.15b", "5.15c", "5.15d")
CUSTOM -> emptyList()
}
companion object {
/**
* Get all difficulty systems based on type
*/
fun getSystemsForClimbType(climbType: ClimbType): List<DifficultySystem> = when (climbType) {
ClimbType.BOULDER -> entries.filter { it.isBoulderingSystem() }
ClimbType.ROPE -> entries.filter { it.isRopeSystem() }
}
}
} }
@Serializable @Serializable
data class DifficultyGrade( data class DifficultyGrade(
val system: DifficultySystem, val system: DifficultySystem,
val grade: String, val grade: String,
val numericValue: Int // For comparison and analytics val numericValue: Int
) )

View File

@@ -13,10 +13,10 @@ data class Gym(
val name: String, val name: String,
val location: String? = null, val location: String? = null,
val supportedClimbTypes: List<ClimbType>, val supportedClimbTypes: List<ClimbType>,
val difficultySystems: List<DifficultySystem>, // What systems this gym uses val difficultySystems: List<DifficultySystem>,
val customDifficultyGrades: List<String> = emptyList(), // For gyms using colors/custom names val customDifficultyGrades: List<String> = emptyList(),
val notes: String? = null, val notes: String? = null,
val createdAt: String, // ISO string format for serialization val createdAt: String,
val updatedAt: String val updatedAt: String
) { ) {
companion object { companion object {

View File

@@ -28,12 +28,12 @@ data class Problem(
val description: String? = null, val description: String? = null,
val climbType: ClimbType, val climbType: ClimbType,
val difficulty: DifficultyGrade, val difficulty: DifficultyGrade,
val setter: String? = null, // Route setter name val setter: String? = null,
val tags: List<String> = emptyList(), // e.g., "overhang", "slab", "crimpy" val tags: List<String> = emptyList(),
val location: String? = null, // Wall section, area in gym val location: String? = null,
val imagePaths: List<String> = emptyList(), // Local file paths to photos val imagePaths: List<String> = emptyList(),
val isActive: Boolean = true, // Whether the problem is still up val isActive: Boolean = true,
val dateSet: String? = null, // When the problem was set val dateSet: String? = null,
val notes: String? = null, val notes: String? = null,
val createdAt: String, val createdAt: String,
val updatedAt: String val updatedAt: String

View File

@@ -1,50 +0,0 @@
package com.atridad.openclimb.data.model
import kotlinx.serialization.Serializable
import java.time.LocalDateTime
@Serializable
data class ProblemProgress(
val problemId: String,
val totalAttempts: Int,
val successfulAttempts: Int,
val firstAttemptDate: String,
val lastAttemptDate: String,
val bestResult: AttemptResult,
val averageAttempts: Double,
val successRate: Double,
val personalBest: String? = null, // Highest hold or completion details
val notes: String? = null
)
@Serializable
data class SessionSummary(
val sessionId: String,
val date: String,
val totalAttempts: Int,
val successfulAttempts: Int,
val uniqueProblems: Int,
val avgDifficulty: Double,
val maxDifficulty: DifficultyGrade,
val climbTypes: List<ClimbType>,
val duration: Long?, // in minutes
val notes: String? = null
)
@Serializable
data class ClimbingStats(
val totalSessions: Int,
val totalAttempts: Int,
val totalSuccesses: Int,
val overallSuccessRate: Double,
val uniqueProblemsAttempted: Int,
val uniqueProblemsCompleted: Int,
val averageSessionDuration: Double, // in minutes
val favoriteGym: String?,
val mostAttemptedDifficulty: DifficultyGrade?,
val currentStreak: Int, // consecutive sessions
val longestStreak: Int,
val firstClimbDate: String?,
val lastClimbDate: String?,
val improvementTrend: String? = null // "improving", "stable", "declining"
)

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.os.Environment import android.os.Environment
import com.atridad.openclimb.data.database.OpenClimbDatabase import com.atridad.openclimb.data.database.OpenClimbDatabase
import com.atridad.openclimb.data.model.* import com.atridad.openclimb.data.model.*
import com.atridad.openclimb.utils.ImageUtils
import com.atridad.openclimb.utils.ZipExportImportUtils import com.atridad.openclimb.utils.ZipExportImportUtils
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -14,7 +13,7 @@ import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
class ClimbRepository( class ClimbRepository(
private val database: OpenClimbDatabase, database: OpenClimbDatabase,
private val context: Context private val context: Context
) { ) {
private val gymDao = database.gymDao() private val gymDao = database.gymDao()
@@ -40,7 +39,6 @@ class ClimbRepository(
fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems() fun getAllProblems(): Flow<List<Problem>> = problemDao.getAllProblems()
suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id) suspend fun getProblemById(id: String): Problem? = problemDao.getProblemById(id)
fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId) fun getProblemsByGym(gymId: String): Flow<List<Problem>> = problemDao.getProblemsByGym(gymId)
fun getActiveProblems(): Flow<List<Problem>> = problemDao.getActiveProblems()
suspend fun insertProblem(problem: Problem) = problemDao.insertProblem(problem) suspend fun insertProblem(problem: Problem) = problemDao.insertProblem(problem)
suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem) suspend fun updateProblem(problem: Problem) = problemDao.updateProblem(problem)
suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem) suspend fun deleteProblem(problem: Problem) = problemDao.deleteProblem(problem)
@@ -50,17 +48,14 @@ class ClimbRepository(
fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions() fun getAllSessions(): Flow<List<ClimbSession>> = sessionDao.getAllSessions()
suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id) suspend fun getSessionById(id: String): ClimbSession? = sessionDao.getSessionById(id)
fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId) fun getSessionsByGym(gymId: String): Flow<List<ClimbSession>> = sessionDao.getSessionsByGym(gymId)
fun getRecentSessions(limit: Int = 10): Flow<List<ClimbSession>> = sessionDao.getRecentSessions(limit)
suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession() suspend fun getActiveSession(): ClimbSession? = sessionDao.getActiveSession()
fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow() fun getActiveSessionFlow(): Flow<ClimbSession?> = sessionDao.getActiveSessionFlow()
fun getSessionsByStatus(status: SessionStatus): Flow<List<ClimbSession>> = sessionDao.getSessionsByStatus(status)
suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session) suspend fun insertSession(session: ClimbSession) = sessionDao.insertSession(session)
suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session) suspend fun updateSession(session: ClimbSession) = sessionDao.updateSession(session)
suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session) suspend fun deleteSession(session: ClimbSession) = sessionDao.deleteSession(session)
// Attempt operations // Attempt operations
fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts() fun getAllAttempts(): Flow<List<Attempt>> = attemptDao.getAllAttempts()
suspend fun getAttemptById(id: String): Attempt? = attemptDao.getAttemptById(id)
fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId) fun getAttemptsBySession(sessionId: String): Flow<List<Attempt>> = attemptDao.getAttemptsBySession(sessionId)
fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId) fun getAttemptsByProblem(problemId: String): Flow<List<Attempt>> = attemptDao.getAttemptsByProblem(problemId)
suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt) suspend fun insertAttempt(attempt: Attempt) = attemptDao.insertAttempt(attempt)
@@ -69,7 +64,7 @@ class ClimbRepository(
// JSON Export functionality // JSON Export
suspend fun exportAllDataToJson(directory: File? = null): File { suspend fun exportAllDataToJson(directory: File? = null): File {
val exportDir = directory ?: File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "OpenClimb") val exportDir = directory ?: File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "OpenClimb")
if (!exportDir.exists()) { if (!exportDir.exists()) {
@@ -124,12 +119,12 @@ class ClimbRepository(
val jsonContent = file.readText() val jsonContent = file.readText()
val importData = json.decodeFromString<ClimbDataExport>(jsonContent) val importData = json.decodeFromString<ClimbDataExport>(jsonContent)
// Import gyms (replace if exists due to primary key constraint) // Import gyms
importData.gyms.forEach { gym -> importData.gyms.forEach { gym ->
try { try {
gymDao.insertGym(gym) gymDao.insertGym(gym)
} catch (e: Exception) { } catch (_: Exception) {
// If insertion fails due to primary key conflict, update instead // If insertion fails, update instead
gymDao.updateGym(gym) gymDao.updateGym(gym)
} }
} }
@@ -138,7 +133,7 @@ class ClimbRepository(
importData.problems.forEach { problem -> importData.problems.forEach { problem ->
try { try {
problemDao.insertProblem(problem) problemDao.insertProblem(problem)
} catch (e: Exception) { } catch (_: Exception) {
problemDao.updateProblem(problem) problemDao.updateProblem(problem)
} }
} }
@@ -147,7 +142,7 @@ class ClimbRepository(
importData.sessions.forEach { session -> importData.sessions.forEach { session ->
try { try {
sessionDao.insertSession(session) sessionDao.insertSession(session)
} catch (e: Exception) { } catch (_: Exception) {
sessionDao.updateSession(session) sessionDao.updateSession(session)
} }
} }
@@ -156,7 +151,7 @@ class ClimbRepository(
importData.attempts.forEach { attempt -> importData.attempts.forEach { attempt ->
try { try {
attemptDao.insertAttempt(attempt) attemptDao.insertAttempt(attempt)
} catch (e: Exception) { } catch (_: Exception) {
attemptDao.updateAttempt(attempt) attemptDao.updateAttempt(attempt)
} }
} }
@@ -166,7 +161,7 @@ class ClimbRepository(
} }
} }
// ZIP Export functionality with images // ZIP Export with images
suspend fun exportAllDataToZip(directory: File? = null): File { suspend fun exportAllDataToZip(directory: File? = null): File {
val allGyms = gymDao.getAllGyms().first() val allGyms = gymDao.getAllGyms().first()
val allProblems = problemDao.getAllProblems().first() val allProblems = problemDao.getAllProblems().first()
@@ -206,7 +201,7 @@ class ClimbRepository(
attempts = attempts attempts = attempts
) )
// Collect all referenced image paths // Collect all image paths
val referencedImagePaths = problems.flatMap { it.imagePaths }.toSet() val referencedImagePaths = problems.flatMap { it.imagePaths }.toSet()
ZipExportImportUtils.createExportZipToUri( ZipExportImportUtils.createExportZipToUri(
@@ -228,12 +223,12 @@ class ClimbRepository(
importResult.importedImagePaths importResult.importedImagePaths
) )
// Import gyms (replace if exists due to primary key constraint) // Import gyms
importData.gyms.forEach { gym -> importData.gyms.forEach { gym ->
try { try {
gymDao.insertGym(gym) gymDao.insertGym(gym)
} catch (e: Exception) { } catch (e: Exception) {
// If insertion fails due to primary key conflict, update instead // If insertion fails update instead
gymDao.updateGym(gym) gymDao.updateGym(gym)
} }
} }

View File

@@ -6,7 +6,6 @@ import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder import android.os.IBinder
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.atridad.openclimb.MainActivity import com.atridad.openclimb.MainActivity
@@ -16,7 +15,6 @@ import com.atridad.openclimb.data.repository.ClimbRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
class SessionTrackingService : Service() { class SessionTrackingService : Service() {
@@ -40,9 +38,10 @@ class SessionTrackingService : Service() {
} }
} }
fun createStopIntent(context: Context): Intent { fun createStopIntent(context: Context, sessionId: String): Intent {
return Intent(context, SessionTrackingService::class.java).apply { return Intent(context, SessionTrackingService::class.java).apply {
action = ACTION_STOP_SESSION action = ACTION_STOP_SESSION
putExtra(EXTRA_SESSION_ID, sessionId)
} }
} }
} }
@@ -65,7 +64,21 @@ class SessionTrackingService : Service() {
} }
} }
ACTION_STOP_SESSION -> { ACTION_STOP_SESSION -> {
stopSessionTracking() val sessionId = intent.getStringExtra(EXTRA_SESSION_ID)
serviceScope.launch {
try {
val targetSession = when {
sessionId != null -> repository.getSessionById(sessionId)
else -> repository.getActiveSession()
}
if (targetSession != null && targetSession.status == com.atridad.openclimb.data.model.SessionStatus.ACTIVE) {
val completed = with(com.atridad.openclimb.data.model.ClimbSession) { targetSession.complete() }
repository.updateSession(completed)
}
} finally {
stopSessionTracking()
}
}
} }
} }
return START_STICKY return START_STICKY
@@ -113,31 +126,32 @@ class SessionTrackingService : Service() {
remainingMinutes > 0 -> "${remainingMinutes}m" remainingMinutes > 0 -> "${remainingMinutes}m"
else -> "< 1m" else -> "< 1m"
} }
} catch (e: Exception) { } catch (_: Exception) {
"Active" "Active"
} }
} ?: "Active" } ?: "Active"
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("OpenClimb Session Active") .setContentTitle("Climbing Session Active")
.setContentText("${gym?.name ?: "Gym"}$duration${attempts.size} attempts") .setContentText("${gym?.name ?: "Gym"}$duration${attempts.size} attempts")
.setSmallIcon(R.drawable.ic_launcher_foreground) .setSmallIcon(R.drawable.ic_mountains)
.setOngoing(true) .setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setContentIntent(createOpenAppIntent()) .setContentIntent(createOpenAppIntent())
.addAction( .addAction(
R.drawable.ic_launcher_foreground, R.drawable.ic_mountains,
"Open Session", "Open Session",
createOpenAppIntent() createOpenAppIntent()
) )
.addAction( .addAction(
R.drawable.ic_launcher_foreground, android.R.drawable.ic_menu_close_clear_cancel,
"End Session", "End Session",
createStopIntent() createStopPendingIntent(sessionId)
) )
.build() .build()
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
} catch (e: Exception) { } catch (_: Exception) {
// Handle errors gracefully // Handle errors gracefully
stopSessionTracking() stopSessionTracking()
} }
@@ -155,8 +169,8 @@ class SessionTrackingService : Service() {
) )
} }
private fun createStopIntent(): PendingIntent { private fun createStopPendingIntent(sessionId: String): PendingIntent {
val intent = createStopIntent(this) val intent = createStopIntent(this, sessionId)
return PendingIntent.getService( return PendingIntent.getService(
this, this,
1, 1,
@@ -166,19 +180,17 @@ class SessionTrackingService : Service() {
} }
private fun createNotificationChannel() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel(
val channel = NotificationChannel( CHANNEL_ID,
CHANNEL_ID, "Session Tracking",
"Session Tracking", NotificationManager.IMPORTANCE_LOW
NotificationManager.IMPORTANCE_LOW ).apply {
).apply { description = "Shows active climbing session information"
description = "Shows active climbing session information" setShowBadge(false)
setShowBadge(false)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
} }
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -27,8 +27,6 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModelFactory
fun OpenClimbApp() { fun OpenClimbApp() {
val navController = rememberNavController() val navController = rememberNavController()
val context = LocalContext.current val context = LocalContext.current
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStackEntry?.destination?.route
val database = remember { OpenClimbDatabase.getDatabase(context) } val database = remember { OpenClimbDatabase.getDatabase(context) }
val repository = remember { ClimbRepository(database, context) } val repository = remember { ClimbRepository(database, context) }
@@ -87,9 +85,6 @@ fun OpenClimbApp() {
viewModel = viewModel, viewModel = viewModel,
onNavigateToSessionDetail = { sessionId -> onNavigateToSessionDetail = { sessionId ->
navController.navigate(Screen.SessionDetail(sessionId)) navController.navigate(Screen.SessionDetail(sessionId))
},
onNavigateToAddSession = { gymId ->
navController.navigate(Screen.AddEditSession(gymId = gymId))
} }
) )
} }
@@ -113,9 +108,6 @@ fun OpenClimbApp() {
viewModel = viewModel, viewModel = viewModel,
onNavigateToProblemDetail = { problemId -> onNavigateToProblemDetail = { problemId ->
navController.navigate(Screen.ProblemDetail(problemId)) navController.navigate(Screen.ProblemDetail(problemId))
},
onNavigateToAddProblem = { gymId ->
navController.navigate(Screen.AddEditProblem(gymId = gymId))
} }
) )
} }
@@ -141,9 +133,6 @@ fun OpenClimbApp() {
viewModel = viewModel, viewModel = viewModel,
onNavigateToGymDetail = { gymId -> onNavigateToGymDetail = { gymId ->
navController.navigate(Screen.GymDetail(gymId)) navController.navigate(Screen.GymDetail(gymId))
},
onNavigateToAddGym = {
navController.navigate(Screen.AddEditGym())
} }
) )
} }
@@ -158,6 +147,7 @@ fun OpenClimbApp() {
// Detail screens // Detail screens
composable<Screen.SessionDetail> { backStackEntry -> composable<Screen.SessionDetail> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.SessionDetail>() val args = backStackEntry.toRoute<Screen.SessionDetail>()
LaunchedEffect(Unit) { fabConfig = null }
SessionDetailScreen( SessionDetailScreen(
sessionId = args.sessionId, sessionId = args.sessionId,
viewModel = viewModel, viewModel = viewModel,
@@ -170,6 +160,7 @@ fun OpenClimbApp() {
composable<Screen.ProblemDetail> { backStackEntry -> composable<Screen.ProblemDetail> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.ProblemDetail>() val args = backStackEntry.toRoute<Screen.ProblemDetail>()
LaunchedEffect(Unit) { fabConfig = null }
ProblemDetailScreen( ProblemDetailScreen(
problemId = args.problemId, problemId = args.problemId,
viewModel = viewModel, viewModel = viewModel,
@@ -182,6 +173,7 @@ fun OpenClimbApp() {
composable<Screen.GymDetail> { backStackEntry -> composable<Screen.GymDetail> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.GymDetail>() val args = backStackEntry.toRoute<Screen.GymDetail>()
LaunchedEffect(Unit) { fabConfig = null }
GymDetailScreen( GymDetailScreen(
gymId = args.gymId, gymId = args.gymId,
viewModel = viewModel, viewModel = viewModel,
@@ -195,6 +187,7 @@ fun OpenClimbApp() {
composable<Screen.AddEditGym> { backStackEntry -> composable<Screen.AddEditGym> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.AddEditGym>() val args = backStackEntry.toRoute<Screen.AddEditGym>()
LaunchedEffect(Unit) { fabConfig = null }
AddEditGymScreen( AddEditGymScreen(
gymId = args.gymId, gymId = args.gymId,
viewModel = viewModel, viewModel = viewModel,
@@ -204,6 +197,7 @@ fun OpenClimbApp() {
composable<Screen.AddEditProblem> { backStackEntry -> composable<Screen.AddEditProblem> { backStackEntry ->
val args = backStackEntry.toRoute<Screen.AddEditProblem>() val args = backStackEntry.toRoute<Screen.AddEditProblem>()
LaunchedEffect(Unit) { fabConfig = null }
AddEditProblemScreen( AddEditProblemScreen(
problemId = args.problemId, problemId = args.problemId,
gymId = args.gymId, gymId = args.gymId,
@@ -247,17 +241,15 @@ fun OpenClimbBottomNavigation(navController: NavHostController) {
selected = isSelected, selected = isSelected,
onClick = { onClick = {
navController.navigate(item.screen) { navController.navigate(item.screen) {
// Pop up to the start destination of the graph to // Clear the entire back stack and go to the selected tab's root screen
// avoid building up a large stack of destinations popUpTo(0) {
// on the back stack as users select items inclusive = true
popUpTo(Screen.Sessions) {
saveState = true
} }
// Avoid multiple copies of the same destination when // Avoid multiple copies of the same destination when
// reselecting the same item // reselecting the same item
launchSingleTop = true launchSingleTop = true
// Restore state when reselecting a previously selected item // Don't restore state - always start fresh when switching tabs
restoreState = true restoreState = false
} }
} }
) )

View File

@@ -5,12 +5,10 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.atridad.openclimb.data.model.ClimbSession import com.atridad.openclimb.data.model.ClimbSession
@@ -95,80 +93,6 @@ fun ActiveSessionBanner(
} }
} }
@Composable
fun StartSessionButton(
gyms: List<Gym>,
onStartSession: (String) -> Unit
) {
var showGymSelection by remember { mutableStateOf(false) }
if (gyms.isEmpty()) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "No gyms available",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "Add a gym first to start a session",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
} else {
Button(
onClick = { showGymSelection = true },
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.PlayArrow, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Start Session")
}
}
if (showGymSelection) {
AlertDialog(
onDismissRequest = { showGymSelection = false },
title = { Text("Select Gym") },
text = {
Column {
gyms.forEach { gym ->
TextButton(
onClick = {
onStartSession(gym.id)
showGymSelection = false
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = gym.name,
modifier = Modifier.fillMaxWidth()
)
}
}
}
},
confirmButton = {
TextButton(onClick = { showGymSelection = false }) {
Text("Cancel")
}
}
)
}
}
private fun calculateDuration(startTimeString: String): String { private fun calculateDuration(startTimeString: String): String {
return try { return try {
val startTime = LocalDateTime.parse(startTimeString) val startTime = LocalDateTime.parse(startTimeString)
@@ -182,7 +106,7 @@ private fun calculateDuration(startTimeString: String): String {
remainingMinutes > 0 -> "${remainingMinutes}m" remainingMinutes > 0 -> "${remainingMinutes}m"
else -> "< 1m" else -> "< 1m"
} }
} catch (e: Exception) { } catch (_: Exception) {
"Active" "Active"
} }
} }

View File

@@ -50,7 +50,7 @@ fun FullscreenImageViewer(
LaunchedEffect(pagerState.currentPage) { LaunchedEffect(pagerState.currentPage) {
thumbnailListState.animateScrollToItem( thumbnailListState.animateScrollToItem(
index = pagerState.currentPage, index = pagerState.currentPage,
scrollOffset = -200 // Center the item scrollOffset = -200
) )
} }

View File

@@ -7,7 +7,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale

View File

@@ -1,6 +1,5 @@
package com.atridad.openclimb.ui.components package com.atridad.openclimb.ui.components
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@@ -20,7 +19,6 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.atridad.openclimb.utils.ImageUtils import com.atridad.openclimb.utils.ImageUtils
import java.io.File
@Composable @Composable
fun ImagePicker( fun ImagePicker(
@@ -41,7 +39,7 @@ fun ImagePicker(
val remainingSlots = maxImages - currentCount val remainingSlots = maxImages - currentCount
val urisToProcess = uris.take(remainingSlots) val urisToProcess = uris.take(remainingSlots)
// Process each selected image // Process images
val newImagePaths = mutableListOf<String>() val newImagePaths = mutableListOf<String>()
urisToProcess.forEach { uri -> urisToProcess.forEach { uri ->
val imagePath = ImageUtils.saveImageFromUri(context, uri) val imagePath = ImageUtils.saveImageFromUri(context, uri)

View File

@@ -8,6 +8,7 @@ import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@@ -47,32 +48,56 @@ fun AddEditGymScreen(
val isEditing = gymId != null val isEditing = gymId != null
// Calculate available difficulty systems based on selected climb types
val availableDifficultySystems = if (selectedClimbTypes.isEmpty()) {
emptyList()
} else {
selectedClimbTypes.flatMap { climbType ->
DifficultySystem.getSystemsForClimbType(climbType)
}.distinct()
}
// Reset selected difficulty systems when available systems change
LaunchedEffect(availableDifficultySystems) {
selectedDifficultySystems = selectedDifficultySystems.filter { it in availableDifficultySystems }.toSet()
}
// Load existing gym data for editing
LaunchedEffect(gymId) {
if (gymId != null) {
val existingGym = viewModel.getGymById(gymId).first()
existingGym?.let { gym ->
name = gym.name
location = gym.location ?: ""
notes = gym.notes ?: ""
selectedClimbTypes = gym.supportedClimbTypes.toSet()
selectedDifficultySystems = gym.difficultySystems.toSet()
}
}
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(if (isEditing) "Edit Gym" else "Add Gym") }, title = { Text(if (isEditing) "Edit Gym" else "Add Gym") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
TextButton( TextButton(
onClick = { onClick = {
val gym = if (isEditing) { val gym = Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
} else {
Gym.create(name, location, selectedClimbTypes.toList(), selectedDifficultySystems.toList(), notes = notes)
}
if (isEditing) { if (isEditing) {
viewModel.updateGym(gym) viewModel.updateGym(gym.copy(id = gymId))
} else { } else {
viewModel.addGym(gym) viewModel.addGym(gym)
} }
onNavigateBack() onNavigateBack()
}, },
enabled = name.isNotBlank() && selectedClimbTypes.isNotEmpty() enabled = name.isNotBlank() && selectedClimbTypes.isNotEmpty() && selectedDifficultySystems.isNotEmpty()
) { ) {
Text("Save") Text("Save")
} }
@@ -142,7 +167,7 @@ fun AddEditGymScreen(
onCheckedChange = null onCheckedChange = null
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() }) Text(climbType.getDisplayName())
} }
} }
} }
@@ -163,29 +188,38 @@ fun AddEditGymScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
DifficultySystem.entries.forEach { system -> if (selectedClimbTypes.isEmpty()) {
Row( Text(
verticalAlignment = Alignment.CenterVertically, text = "Select climb types first to see available difficulty systems",
modifier = Modifier style = MaterialTheme.typography.bodyMedium,
.fillMaxWidth() color = MaterialTheme.colorScheme.onSurfaceVariant,
.selectable( modifier = Modifier.padding(vertical = 8.dp)
selected = system in selectedDifficultySystems, )
onClick = { } else {
selectedDifficultySystems = if (system in selectedDifficultySystems) { availableDifficultySystems.forEach { system ->
selectedDifficultySystems - system Row(
} else { verticalAlignment = Alignment.CenterVertically,
selectedDifficultySystems + system modifier = Modifier
} .fillMaxWidth()
}, .selectable(
role = Role.Checkbox selected = system in selectedDifficultySystems,
onClick = {
selectedDifficultySystems = if (system in selectedDifficultySystems) {
selectedDifficultySystems - system
} else {
selectedDifficultySystems + system
}
},
role = Role.Checkbox
)
) {
Checkbox(
checked = system in selectedDifficultySystems,
onCheckedChange = null
) )
) { Spacer(modifier = Modifier.width(8.dp))
Checkbox( Text(system.getDisplayName())
checked = system in selectedDifficultySystems, }
onCheckedChange = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(system.name)
} }
} }
} }
@@ -244,6 +278,8 @@ fun AddEditProblemScreen(
notes = p.notes ?: "" notes = p.notes ?: ""
isActive = p.isActive isActive = p.isActive
imagePaths = p.imagePaths imagePaths = p.imagePaths
// Set the selected gym for the existing problem
selectedGym = gyms.find { it.id == p.gymId }
} }
} }
} }
@@ -254,8 +290,39 @@ fun AddEditProblemScreen(
} }
} }
val availableDifficultySystems = selectedGym?.difficultySystems ?: DifficultySystem.entries.toList()
val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList() val availableClimbTypes = selectedGym?.supportedClimbTypes ?: ClimbType.entries.toList()
val availableDifficultySystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
selectedGym?.difficultySystems?.contains(system) != false
}
// Auto-select climb type if there's only one available
LaunchedEffect(availableClimbTypes) {
if (availableClimbTypes.size == 1 && selectedClimbType != availableClimbTypes.first()) {
selectedClimbType = availableClimbTypes.first()
}
}
// Auto-select or reset difficulty system based on climb type
LaunchedEffect(selectedClimbType, availableDifficultySystems) {
when {
// If current system is not compatible, select the first available one
selectedDifficultySystem !in availableDifficultySystems -> {
selectedDifficultySystem = availableDifficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM
}
// If there's only one available system and nothing is selected, auto-select it
availableDifficultySystems.size == 1 && selectedDifficultySystem != availableDifficultySystems.first() -> {
selectedDifficultySystem = availableDifficultySystems.first()
}
}
}
// Reset grade when difficulty system changes (unless it's a valid grade for the new system)
LaunchedEffect(selectedDifficultySystem) {
val availableGrades = selectedDifficultySystem.getAvailableGrades()
if (availableGrades.isNotEmpty() && difficultyGrade !in availableGrades) {
difficultyGrade = ""
}
}
Scaffold( Scaffold(
topBar = { topBar = {
@@ -263,7 +330,7 @@ fun AddEditProblemScreen(
title = { Text(if (isEditing) "Edit Problem" else "Add Problem") }, title = { Text(if (isEditing) "Edit Problem" else "Add Problem") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
@@ -293,7 +360,7 @@ fun AddEditProblemScreen(
) )
if (isEditing) { if (isEditing) {
viewModel.updateProblem(problem.copy(id = problemId!!)) viewModel.updateProblem(problem.copy(id = problemId))
} else { } else {
viewModel.addProblem(problem) viewModel.addProblem(problem)
} }
@@ -437,7 +504,7 @@ fun AddEditProblemScreen(
availableClimbTypes.forEach { climbType -> availableClimbTypes.forEach { climbType ->
FilterChip( FilterChip(
onClick = { selectedClimbType = climbType }, onClick = { selectedClimbType = climbType },
label = { Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() }) }, label = { Text(climbType.getDisplayName()) },
selected = selectedClimbType == climbType selected = selectedClimbType == climbType
) )
} }
@@ -476,7 +543,7 @@ fun AddEditProblemScreen(
items(availableDifficultySystems) { system -> items(availableDifficultySystems) { system ->
FilterChip( FilterChip(
onClick = { selectedDifficultySystem = system }, onClick = { selectedDifficultySystem = system },
label = { Text(system.name) }, label = { Text(system.getDisplayName()) },
selected = selectedDifficultySystem == system selected = selectedDifficultySystem == system
) )
} }
@@ -484,23 +551,51 @@ fun AddEditProblemScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField( if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
value = difficultyGrade, OutlinedTextField(
onValueChange = { difficultyGrade = it }, value = difficultyGrade,
label = { Text("Grade *") }, onValueChange = { difficultyGrade = it },
modifier = Modifier.fillMaxWidth(), label = { Text("Grade *") },
singleLine = true, modifier = Modifier.fillMaxWidth(),
placeholder = { singleLine = true,
Text(when (selectedDifficultySystem) { placeholder = { Text("Enter custom grade") }
DifficultySystem.V_SCALE -> "e.g., V0, V4, V10" )
DifficultySystem.FONT -> "e.g., 3, 6A+, 8B" } else {
DifficultySystem.YDS -> "e.g., 5.8, 5.12a" var expanded by remember { mutableStateOf(false) }
DifficultySystem.FRENCH -> "e.g., 6a, 7c+" val availableGrades = selectedDifficultySystem.getAvailableGrades()
DifficultySystem.CUSTOM -> "Custom grade"
else -> "Enter grade" ExposedDropdownMenuBox(
}) expanded = expanded,
onExpandedChange = { expanded = !expanded },
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = difficultyGrade,
onValueChange = { },
readOnly = true,
label = { Text("Grade *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier
.menuAnchor()
.fillMaxWidth()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
availableGrades.forEach { grade ->
DropdownMenuItem(
text = { Text(grade) },
onClick = {
difficultyGrade = grade
expanded = false
}
)
}
}
} }
) }
} }
} }
} }
@@ -552,7 +647,7 @@ fun AddEditProblemScreen(
label = { Text("Tags (Optional)") }, label = { Text("Tags (Optional)") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
placeholder = { Text("e.g., overhang, crimpy, dynamic (comma-separated)") } placeholder = { Text("e.g., crimpy, dynamic (comma-separated)") }
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -617,6 +712,19 @@ fun AddEditSessionScreen(
var attempts by remember { mutableStateOf(listOf<AttemptInput>()) } var attempts by remember { mutableStateOf(listOf<AttemptInput>()) }
var showAddAttemptDialog by remember { mutableStateOf(false) } var showAddAttemptDialog by remember { mutableStateOf(false) }
// Load existing session data for editing
LaunchedEffect(sessionId) {
if (sessionId != null) {
val existingSession = viewModel.getSessionById(sessionId).first()
existingSession?.let { session ->
selectedGym = gyms.find { it.id == session.gymId }
sessionDate = session.date.split("T")[0] // Extract date part
duration = session.duration?.toString() ?: ""
sessionNotes = session.notes ?: ""
}
}
}
LaunchedEffect(gymId, gyms) { LaunchedEffect(gymId, gyms) {
if (gymId != null && selectedGym == null) { if (gymId != null && selectedGym == null) {
selectedGym = gyms.find { it.id == gymId } selectedGym = gyms.find { it.id == gymId }
@@ -629,7 +737,7 @@ fun AddEditSessionScreen(
title = { Text(if (isEditing) "Edit Session" else "Add Session") }, title = { Text(if (isEditing) "Edit Session" else "Add Session") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
@@ -642,7 +750,7 @@ fun AddEditSessionScreen(
) )
if (isEditing) { if (isEditing) {
viewModel.updateSession(session.copy(id = sessionId!!)) viewModel.updateSession(session.copy(id = sessionId))
} else { } else {
viewModel.addSession(session) viewModel.addSession(session)
@@ -830,7 +938,7 @@ fun AddEditSessionScreen(
problem?.difficulty?.let { difficulty -> problem?.difficulty?.let { difficulty ->
Text( Text(
text = "${difficulty.system.name}: ${difficulty.grade}", text = "${difficulty.system.getDisplayName()}: ${difficulty.grade}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@@ -840,8 +948,7 @@ fun AddEditSessionScreen(
text = "Result: ${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }}", text = "Result: ${attempt.result.name.lowercase().replaceFirstChar { it.uppercase() }}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = when (attempt.result) { color = when (attempt.result) {
AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.primary
AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.primary
else -> MaterialTheme.colorScheme.onSurfaceVariant else -> MaterialTheme.colorScheme.onSurfaceVariant
} }
) )
@@ -956,7 +1063,7 @@ fun AddAttemptDialog(
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
Text( Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}", text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )

View File

@@ -6,8 +6,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.atridad.openclimb.R
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
@Composable @Composable
@@ -26,11 +28,23 @@ fun AnalyticsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
item { item {
Text( Row(
text = "Analytics", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.headlineMedium, verticalAlignment = Alignment.CenterVertically,
fontWeight = FontWeight.Bold horizontalArrangement = Arrangement.spacedBy(12.dp)
) ) {
Icon(
painter = painterResource(id = R.drawable.ic_mountains),
contentDescription = "OpenClimb Logo",
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Analytics",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
} }
// Overall Stats // Overall Stats

View File

@@ -8,10 +8,9 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
@@ -60,7 +59,7 @@ fun SessionDetailScreen(
// Calculate stats // Calculate stats
val successfulAttempts = attempts.filter { val successfulAttempts = attempts.filter {
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT) it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
} }
val uniqueProblems = attempts.map { it.problemId }.distinct() val uniqueProblems = attempts.map { it.problemId }.distinct()
val attemptedProblems = problems.filter { it.id in uniqueProblems } val attemptedProblems = problems.filter { it.id in uniqueProblems }
@@ -72,9 +71,7 @@ fun SessionDetailScreen(
}.sortedByDescending { attempt -> }.sortedByDescending { attempt ->
// Sort by result priority, then by timestamp // Sort by result priority, then by timestamp
when (attempt.first.result) { when (attempt.first.result) {
AttemptResult.ONSIGHT -> 5 AttemptResult.FLASH -> 3
AttemptResult.FLASH -> 4
AttemptResult.REDPOINT -> 3
AttemptResult.SUCCESS -> 2 AttemptResult.SUCCESS -> 2
AttemptResult.FALL -> 1 AttemptResult.FALL -> 1
else -> 0 else -> 0
@@ -87,7 +84,7 @@ fun SessionDetailScreen(
title = { Text("Session Details") }, title = { Text("Session Details") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
@@ -131,8 +128,7 @@ fun SessionDetailScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
// Show FAB only for active sessions (those without duration) if (session?.status == SessionStatus.ACTIVE) {
if (session?.duration == null) {
FloatingActionButton( FloatingActionButton(
onClick = { showAddAttemptDialog = true } onClick = { showAddAttemptDialog = true }
) { ) {
@@ -403,7 +399,7 @@ fun ProblemDetailScreen(
val context = LocalContext.current val context = LocalContext.current
var showDeleteDialog by remember { mutableStateOf(false) } var showDeleteDialog by remember { mutableStateOf(false) }
var showImageViewer by remember { mutableStateOf(false) } var showImageViewer by remember { mutableStateOf(false) }
var selectedImageIndex by remember { mutableStateOf(0) } var selectedImageIndex by remember { mutableIntStateOf(0) }
val attempts by viewModel.getAttemptsByProblem(problemId).collectAsState(initial = emptyList()) val attempts by viewModel.getAttemptsByProblem(problemId).collectAsState(initial = emptyList())
val sessions by viewModel.sessions.collectAsState() val sessions by viewModel.sessions.collectAsState()
val gyms by viewModel.gyms.collectAsState() val gyms by viewModel.gyms.collectAsState()
@@ -419,7 +415,7 @@ fun ProblemDetailScreen(
// Calculate stats // Calculate stats
val successfulAttempts = attempts.filter { val successfulAttempts = attempts.filter {
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT) it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
} }
val successRate = if (attempts.isNotEmpty()) { val successRate = if (attempts.isNotEmpty()) {
(successfulAttempts.size.toDouble() / attempts.size * 100).toInt() (successfulAttempts.size.toDouble() / attempts.size * 100).toInt()
@@ -436,7 +432,7 @@ fun ProblemDetailScreen(
title = { Text("Problem Details") }, title = { Text("Problem Details") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
@@ -481,7 +477,7 @@ fun ProblemDetailScreen(
Column { Column {
problem?.let { p -> problem?.let { p ->
Text( Text(
text = "${p.difficulty.system.name}: ${p.difficulty.grade}", text = "${p.difficulty.system.getDisplayName()}: ${p.difficulty.grade}",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@@ -490,7 +486,7 @@ fun ProblemDetailScreen(
problem?.let { p -> problem?.let { p ->
Text( Text(
text = p.climbType.name.lowercase().replaceFirstChar { it.uppercase() }, text = p.climbType.getDisplayName(),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -751,7 +747,7 @@ fun GymDetailScreen(
} }
val successfulAttempts = gymAttempts.filter { val successfulAttempts = gymAttempts.filter {
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT) it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
} }
val successRate = if (gymAttempts.isNotEmpty()) { val successRate = if (gymAttempts.isNotEmpty()) {
@@ -770,7 +766,7 @@ fun GymDetailScreen(
title = { Text(gym?.name ?: "Gym Details") }, title = { Text(gym?.name ?: "Gym Details") },
navigationIcon = { navigationIcon = {
IconButton(onClick = onNavigateBack) { IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
}, },
actions = { actions = {
@@ -985,7 +981,7 @@ fun GymDetailScreen(
problems.sortedByDescending { it.createdAt }.take(5).forEach { problem -> problems.sortedByDescending { it.createdAt }.take(5).forEach { problem ->
val problemAttempts = gymAttempts.filter { it.problemId == problem.id } val problemAttempts = gymAttempts.filter { it.problemId == problem.id }
val problemSuccessful = problemAttempts.any { val problemSuccessful = problemAttempts.any {
it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT) it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH)
} }
Card( Card(
@@ -1087,7 +1083,7 @@ fun GymDetailScreen(
supportingContent = { supportingContent = {
val dateTime = try { val dateTime = try {
LocalDateTime.parse(session.date) LocalDateTime.parse(session.date)
} catch (e: Exception) { } catch (_: Exception) {
null null
} }
val formattedDate = dateTime?.format( val formattedDate = dateTime?.format(
@@ -1265,13 +1261,13 @@ fun AttemptHistoryCard(
@Composable @Composable
fun AttemptResultBadge(result: AttemptResult) { fun AttemptResultBadge(result: AttemptResult) {
val backgroundColor = when (result) { val backgroundColor = when (result) {
AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.primaryContainer AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.primaryContainer
AttemptResult.FALL -> MaterialTheme.colorScheme.secondaryContainer AttemptResult.FALL -> MaterialTheme.colorScheme.secondaryContainer
else -> MaterialTheme.colorScheme.surfaceVariant else -> MaterialTheme.colorScheme.surfaceVariant
} }
val textColor = when (result) { val textColor = when (result) {
AttemptResult.SUCCESS, AttemptResult.FLASH, AttemptResult.REDPOINT, AttemptResult.ONSIGHT -> MaterialTheme.colorScheme.onPrimaryContainer AttemptResult.SUCCESS, AttemptResult.FLASH -> MaterialTheme.colorScheme.onPrimaryContainer
AttemptResult.FALL -> MaterialTheme.colorScheme.onSecondaryContainer AttemptResult.FALL -> MaterialTheme.colorScheme.onSecondaryContainer
else -> MaterialTheme.colorScheme.onSurfaceVariant else -> MaterialTheme.colorScheme.onSurfaceVariant
} }
@@ -1314,7 +1310,7 @@ fun SessionAttemptCard(
) )
Text( Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}", text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
@@ -1348,42 +1344,11 @@ private fun formatDate(dateString: String): String {
val date = LocalDateTime.parse(dateString, formatter) val date = LocalDateTime.parse(dateString, formatter)
val displayFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy") val displayFormatter = DateTimeFormatter.ofPattern("MMM dd, yyyy")
date.format(displayFormatter) date.format(displayFormatter)
} catch (e: Exception) { } catch (_: Exception) {
dateString.take(10) // Fallback to just the date part dateString.take(10) // Fallback to just the date part
} }
} }
private fun formatTime(timeString: String): String {
return try {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
val time = LocalDateTime.parse(timeString, formatter)
val displayFormatter = DateTimeFormatter.ofPattern("h:mm a")
time.format(displayFormatter)
} catch (e: Exception) {
timeString.take(8) // Fallback to time part
}
}
private fun calculateSessionDuration(startTime: String, endTime: String): String {
return try {
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
val start = LocalDateTime.parse(startTime, formatter)
val end = LocalDateTime.parse(endTime, formatter)
val duration = java.time.Duration.between(start, end)
val hours = duration.toHours()
val minutes = duration.toMinutes() % 60
when {
hours > 0 -> "${hours}h ${minutes}m"
minutes > 0 -> "${minutes}m"
else -> "< 1m"
}
} catch (e: Exception) {
"Unknown"
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EnhancedAddAttemptDialog( fun EnhancedAddAttemptDialog(
@@ -1406,6 +1371,39 @@ fun EnhancedAddAttemptDialog(
var selectedClimbType by remember { mutableStateOf(ClimbType.BOULDER) } var selectedClimbType by remember { mutableStateOf(ClimbType.BOULDER) }
var selectedDifficultySystem by remember { mutableStateOf(gym.difficultySystems.firstOrNull() ?: DifficultySystem.V_SCALE) } var selectedDifficultySystem by remember { mutableStateOf(gym.difficultySystems.firstOrNull() ?: DifficultySystem.V_SCALE) }
// Auto-select climb type if there's only one available
LaunchedEffect(gym.supportedClimbTypes) {
if (gym.supportedClimbTypes.size == 1 && selectedClimbType != gym.supportedClimbTypes.first()) {
selectedClimbType = gym.supportedClimbTypes.first()
}
}
// Auto-select difficulty system if there's only one available for the selected climb type
LaunchedEffect(selectedClimbType, gym.difficultySystems) {
val availableSystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
gym.difficultySystems.contains(system)
}
when {
// If current system is not compatible, select the first available one
selectedDifficultySystem !in availableSystems -> {
selectedDifficultySystem = availableSystems.firstOrNull() ?: gym.difficultySystems.firstOrNull() ?: DifficultySystem.CUSTOM
}
// If there's only one available system, auto-select it
availableSystems.size == 1 && selectedDifficultySystem != availableSystems.first() -> {
selectedDifficultySystem = availableSystems.first()
}
}
}
// Reset grade when difficulty system changes
LaunchedEffect(selectedDifficultySystem) {
val availableGrades = selectedDifficultySystem.getAvailableGrades()
if (availableGrades.isNotEmpty() && newProblemGrade !in availableGrades) {
newProblemGrade = ""
}
}
Dialog(onDismissRequest = onDismiss) { Dialog(onDismissRequest = onDismiss) {
Card( Card(
modifier = Modifier modifier = Modifier
@@ -1509,7 +1507,7 @@ fun EnhancedAddAttemptDialog(
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "${problem.difficulty.system.name}: ${problem.difficulty.grade}", text = "${problem.difficulty.system.getDisplayName()}: ${problem.difficulty.grade}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) color = if (isSelected)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
@@ -1584,7 +1582,7 @@ fun EnhancedAddAttemptDialog(
onClick = { selectedClimbType = climbType }, onClick = { selectedClimbType = climbType },
label = { label = {
Text( Text(
climbType.name.lowercase().replaceFirstChar { it.uppercase() }, climbType.getDisplayName(),
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
}, },
@@ -1611,12 +1609,15 @@ fun EnhancedAddAttemptDialog(
LazyRow( LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(gym.difficultySystems) { system -> val availableSystems = DifficultySystem.getSystemsForClimbType(selectedClimbType).filter { system ->
gym.difficultySystems.contains(system)
}
items(availableSystems) { system ->
FilterChip( FilterChip(
onClick = { selectedDifficultySystem = system }, onClick = { selectedDifficultySystem = system },
label = { label = {
Text( Text(
system.name, system.getDisplayName(),
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
) )
}, },
@@ -1630,31 +1631,63 @@ fun EnhancedAddAttemptDialog(
} }
} }
OutlinedTextField( if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
value = newProblemGrade, OutlinedTextField(
onValueChange = { newProblemGrade = it }, value = newProblemGrade,
label = { Text("Grade *") }, onValueChange = { newProblemGrade = it },
placeholder = { label = { Text("Grade *") },
Text(when (selectedDifficultySystem) { placeholder = { Text("Enter custom grade") },
DifficultySystem.V_SCALE -> "e.g., V0, V4, V10" modifier = Modifier.fillMaxWidth(),
DifficultySystem.FONT -> "e.g., 3, 6A+, 8B" singleLine = true,
DifficultySystem.YDS -> "e.g., 5.8, 5.12a" colors = OutlinedTextFieldDefaults.colors(
DifficultySystem.FRENCH -> "e.g., 6a, 7c+" focusedBorderColor = MaterialTheme.colorScheme.primary,
DifficultySystem.CUSTOM -> "Custom grade" unfocusedBorderColor = MaterialTheme.colorScheme.outline
else -> "Enter grade" ),
}) isError = newProblemGrade.isBlank(),
}, supportingText = if (newProblemGrade.isBlank()) {
modifier = Modifier.fillMaxWidth(), { Text("Grade is required", color = MaterialTheme.colorScheme.error) }
singleLine = true, } else null
colors = OutlinedTextFieldDefaults.colors( )
focusedBorderColor = MaterialTheme.colorScheme.primary, } else {
unfocusedBorderColor = MaterialTheme.colorScheme.outline var expanded by remember { mutableStateOf(false) }
), val availableGrades = selectedDifficultySystem.getAvailableGrades()
isError = newProblemGrade.isBlank(),
supportingText = if (newProblemGrade.isBlank()) { ExposedDropdownMenuBox(
{ Text("Grade is required", color = MaterialTheme.colorScheme.error) } expanded = expanded,
} else null onExpandedChange = { expanded = !expanded },
) modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = newProblemGrade,
onValueChange = { },
readOnly = true,
label = { Text("Grade *") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
isError = newProblemGrade.isBlank(),
supportingText = if (newProblemGrade.isBlank()) {
{ Text("Grade is required", color = MaterialTheme.colorScheme.error) }
} else null
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
availableGrades.forEach { grade ->
DropdownMenuItem(
text = { Text(grade) },
onClick = {
newProblemGrade = grade
expanded = false
}
)
}
}
}
}
} }
} }
} }
@@ -1828,28 +1861,5 @@ fun EnhancedAddAttemptDialog(
} }
} }
} }
@Composable
fun StatisticItem(
label: String,
value: String,
valueColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.primary
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = value,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = valueColor
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
}
}

View File

@@ -3,15 +3,14 @@ package com.atridad.openclimb.ui.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.atridad.openclimb.R
import com.atridad.openclimb.data.model.Gym import com.atridad.openclimb.data.model.Gym
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
@@ -19,8 +18,7 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
@Composable @Composable
fun GymsScreen( fun GymsScreen(
viewModel: ClimbViewModel, viewModel: ClimbViewModel,
onNavigateToGymDetail: (String) -> Unit, onNavigateToGymDetail: (String) -> Unit
onNavigateToAddGym: () -> Unit
) { ) {
val gyms by viewModel.gyms.collectAsState() val gyms by viewModel.gyms.collectAsState()
@@ -29,11 +27,23 @@ fun GymsScreen(
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(16.dp)
) { ) {
Text( Row(
text = "Climbing Gyms", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.headlineMedium, verticalAlignment = Alignment.CenterVertically,
fontWeight = FontWeight.Bold horizontalArrangement = Arrangement.spacedBy(12.dp)
) ) {
Icon(
painter = painterResource(id = R.drawable.ic_mountains),
contentDescription = "OpenClimb Logo",
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Climbing Gyms",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -95,7 +105,7 @@ fun GymCard(
AssistChip( AssistChip(
onClick = { }, onClick = { },
label = { label = {
Text(climbType.name.lowercase().replaceFirstChar { it.uppercase() }) Text(climbType.getDisplayName())
}, },
modifier = Modifier.padding(end = 4.dp) modifier = Modifier.padding(end = 4.dp)
) )
@@ -105,7 +115,7 @@ fun GymCard(
if (gym.difficultySystems.isNotEmpty()) { if (gym.difficultySystems.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "Systems: ${gym.difficultySystems.joinToString(", ") { it.name }}", text = "Systems: ${gym.difficultySystems.joinToString(", ") { it.getDisplayName() }}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )

View File

@@ -2,16 +2,18 @@ package com.atridad.openclimb.ui.screens
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.atridad.openclimb.R
import com.atridad.openclimb.data.model.ClimbType
import com.atridad.openclimb.data.model.Gym
import com.atridad.openclimb.data.model.Problem import com.atridad.openclimb.data.model.Problem
import com.atridad.openclimb.ui.components.FullscreenImageViewer import com.atridad.openclimb.ui.components.FullscreenImageViewer
import com.atridad.openclimb.ui.components.ImageDisplay import com.atridad.openclimb.ui.components.ImageDisplay
@@ -21,38 +23,157 @@ import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
@Composable @Composable
fun ProblemsScreen( fun ProblemsScreen(
viewModel: ClimbViewModel, viewModel: ClimbViewModel,
onNavigateToProblemDetail: (String) -> Unit, onNavigateToProblemDetail: (String) -> Unit
onNavigateToAddProblem: (String?) -> Unit
) { ) {
val problems by viewModel.problems.collectAsState() val problems by viewModel.problems.collectAsState()
val gyms by viewModel.gyms.collectAsState() val gyms by viewModel.gyms.collectAsState()
var showImageViewer by remember { mutableStateOf(false) } var showImageViewer by remember { mutableStateOf(false) }
var selectedImagePaths by remember { mutableStateOf<List<String>>(emptyList()) } var selectedImagePaths by remember { mutableStateOf<List<String>>(emptyList()) }
var selectedImageIndex by remember { mutableStateOf(0) } var selectedImageIndex by remember { mutableIntStateOf(0) }
// Filter state
var selectedClimbType by remember { mutableStateOf<ClimbType?>(null) }
var selectedGym by remember { mutableStateOf<Gym?>(null) }
// Apply filters
val filteredProblems = problems.filter { problem ->
val climbTypeMatch = selectedClimbType?.let { it == problem.climbType } != false
val gymMatch = selectedGym?.let { it.id == problem.gymId } != false
climbTypeMatch && gymMatch
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(16.dp) .padding(16.dp)
) { ) {
Text( Row(
text = "Problems & Routes", modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.headlineMedium, verticalAlignment = Alignment.CenterVertically,
fontWeight = FontWeight.Bold horizontalArrangement = Arrangement.spacedBy(12.dp)
) ) {
Icon(
painter = painterResource(id = R.drawable.ic_mountains),
contentDescription = "OpenClimb Logo",
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Text(
text = "Problems & Routes",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (problems.isEmpty()) { // Filters Section
if (problems.isNotEmpty()) {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Filters",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(12.dp))
// Climb Type Filter
Text(
text = "Climb Type",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
FilterChip(
onClick = { selectedClimbType = null },
label = { Text("All Types") },
selected = selectedClimbType == null
)
}
items(ClimbType.entries) { climbType ->
FilterChip(
onClick = { selectedClimbType = climbType },
label = { Text(climbType.getDisplayName()) },
selected = selectedClimbType == climbType
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Gym Filter
Text(
text = "Gym",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(8.dp))
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
item {
FilterChip(
onClick = { selectedGym = null },
label = { Text("All Gyms") },
selected = selectedGym == null
)
}
items(gyms) { gym ->
FilterChip(
onClick = { selectedGym = gym },
label = { Text(gym.name) },
selected = selectedGym?.id == gym.id
)
}
}
// Filter result count
if (selectedClimbType != null || selectedGym != null) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "Showing ${filteredProblems.size} of ${problems.size} problems",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
if (filteredProblems.isEmpty()) {
EmptyStateMessage( EmptyStateMessage(
title = if (gyms.isEmpty()) "No Gyms Available" else "No Problems Yet", title = if (problems.isEmpty()) {
message = if (gyms.isEmpty()) "Add a gym first to start tracking problems and routes!" else "Start tracking your favorite problems and routes!", if (gyms.isEmpty()) "No Gyms Available" else "No Problems Yet"
} else {
"No Problems Match Filters"
},
message = if (problems.isEmpty()) {
if (gyms.isEmpty()) "Add a gym first to start tracking problems and routes!" else "Start tracking your favorite problems and routes!"
} else {
"Try adjusting your filters to see more problems."
},
onActionClick = { }, onActionClick = { },
actionText = "" actionText = ""
) )
} else { } else {
LazyColumn { LazyColumn {
items(problems) { problem -> items(filteredProblems) { problem ->
ProblemCard( ProblemCard(
problem = problem, problem = problem,
gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym", gymName = gyms.find { it.id == problem.gymId }?.name ?: "Unknown Gym",
@@ -124,7 +245,7 @@ fun ProblemCard(
) )
Text( Text(
text = problem.climbType.name.lowercase().replaceFirstChar { it.uppercase() }, text = problem.climbType.getDisplayName(),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )

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