10 |
11 | But how do you rank these?
12 |
13 | You might be tempted to order them by preference, but this could quickly get overwhelming for long lists.
14 |
15 | A much easier method is to use pairwise comparisons, which shows you single head-to-head pairs, and has you choose which one you like best.
16 |
17 | After doing a small number of these matchups, Rank-My-Favs can confidently create a ranked list for you.
18 |
19 | Under the hood, Rank-My-Favs can sort by either the win-rate, or the more advanced Glicko rating system.
20 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 | -dontwarn okhttp3.internal.platform.**
23 | -dontwarn org.conscrypt.**
24 | -dontwarn org.bouncycastle.**
25 | -dontwarn org.openjsse.**
26 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/de/full_description.txt:
--------------------------------------------------------------------------------
1 | Führen Sie Listen mit Ihren Lieblingssachen wie Filmen, Büchern, Rezepten oder Musikalben? Sie könnten eine oder mehrere Listen führen, die wie folgt aussehen:
2 |
3 |
4 |
Network (1976)
5 |
Lone Star (1996)
6 |
Devils (1971)
7 |
Das siebte Siegel (1957)
8 |
... Viele weitere Filme_h
9 |
10 |
11 |
12 | Aber wie ordnen Sie diese ein?
13 |
14 | Sie könnten versucht sein, sie nach Ihren Wünschen zu sortieren, aber das könnte bei langen Listen schnell überwältigend werden.
15 |
16 | Eine viel einfachere Methode ist die Verwendung von pairwise comparisons, die Ihnen einzelne Kopf-an-Kopf-Paare anzeigen und anzeigen lassen Wählen Sie, welches Ihnen am besten gefällt.
17 |
18 | Nachdem Sie eine kleine Anzahl dieser Matchups durchgeführt haben, kann Rank-My-Favs getrost eine Rangliste für Sie erstellen.
19 |
20 | Unter der Haube kann Rank-My-Favs entweder nach der Gewinnrate oder dem fortschrittlicheren Glicko-Bewertungssystem sortiert werden.
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/app_icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.woodpecker.yml:
--------------------------------------------------------------------------------
1 | steps:
2 | prettier_markdown_check:
3 | image: jauderho/prettier:3.7.4-alpine
4 | commands:
5 | - prettier -c "**/*.md" "**/*.yml"
6 | when:
7 | - event: pull_request
8 |
9 | check_formatting:
10 | image: cimg/android:2025.12.1
11 | commands:
12 | - sudo chown -R circleci:circleci .
13 | - ./gradlew lintKotlin
14 | environment:
15 | GRADLE_USER_HOME: ".gradle"
16 | when:
17 | - event: pull_request
18 |
19 | check_android_lint:
20 | image: cimg/android:2025.12.1
21 | commands:
22 | - sudo chown -R circleci:circleci .
23 | - ./gradlew lint
24 | environment:
25 | GRADLE_USER_HOME: ".gradle"
26 | when:
27 | - event: pull_request
28 |
29 | build_project:
30 | image: cimg/android:2025.12.1
31 | commands:
32 | - sudo chown -R circleci:circleci .
33 | - ./gradlew assembleDebug
34 | environment:
35 | GRADLE_USER_HOME: ".gradle"
36 | when:
37 | - event: pull_request
38 |
39 | notify_success:
40 | image: alpine:3
41 | commands:
42 | - apk add curl
43 | - "curl -H'Title: ✔️ ${CI_REPO_NAME}/${CI_COMMIT_SOURCE_BRANCH}' -d'${CI_PIPELINE_URL}' ntfy.sh/rank_my_favs_ci"
44 | when:
45 | - event: pull_request
46 | status: [success]
47 |
48 | notify_failure:
49 | image: alpine:3
50 | commands:
51 | - apk add curl
52 | - "curl -H'Title: ❌ ${CI_REPO_NAME}/${CI_COMMIT_SOURCE_BRANCH}' -d'${CI_PIPELINE_URL}' ntfy.sh/rank_my_favs_ci"
53 | when:
54 | - event: pull_request
55 | status: [failure]
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/common/AppBars.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.common
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.rememberBasicTooltipState
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.TooltipAnchorPosition
12 | import androidx.compose.material3.TooltipDefaults
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.res.stringResource
15 | import com.dessalines.rankmyfavs.R
16 |
17 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
18 | @Composable
19 | fun BackButton(onBackClick: () -> Unit) {
20 | val tooltipPosition = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above)
21 | BasicTooltipBox(
22 | positionProvider = tooltipPosition,
23 | state = rememberBasicTooltipState(isPersistent = false),
24 | tooltip = {
25 | ToolTip(stringResource(R.string.go_back))
26 | },
27 | ) {
28 | IconButton(
29 | onClick = onBackClick,
30 | ) {
31 | Icon(
32 | Icons.AutoMirrored.Outlined.ArrowBack,
33 | contentDescription = stringResource(R.string.go_back),
34 | )
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=false
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | # Enables namespacing of each library's R class so that its R class includes only the
23 | # resources declared in the library itself and none from the library's dependencies,
24 | # thereby reducing the size of the R class for that library
25 | android.nonTransitiveRClass=true
26 | org.gradle.unsafe.configuration-cache=true
27 | android.nonFinalResIds=false
28 | org.gradle.daemon=false
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/app_icon.xml:
--------------------------------------------------------------------------------
1 |
7 |
14 |
21 |
28 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.platform.LocalContext
10 | import com.dessalines.rankmyfavs.db.AppSettings
11 | import com.dessalines.rankmyfavs.utils.ThemeColor
12 | import com.dessalines.rankmyfavs.utils.ThemeMode
13 |
14 | @Composable
15 | fun RankMyFavsTheme(
16 | settings: AppSettings?,
17 | content: @Composable () -> Unit,
18 | ) {
19 | val themeMode = ThemeMode.entries[settings?.theme ?: 0]
20 | val themeColor = ThemeColor.entries[settings?.themeColor ?: 0]
21 |
22 | val ctx = LocalContext.current
23 | val android12OrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
24 |
25 | // Dynamic schemes crash on lower than android 12
26 | val dynamicPair =
27 | if (android12OrLater) {
28 | Pair(dynamicLightColorScheme(ctx), dynamicDarkColorScheme(ctx))
29 | } else {
30 | pink()
31 | }
32 |
33 | val colorPair =
34 | when (themeColor) {
35 | ThemeColor.Dynamic -> dynamicPair
36 | ThemeColor.Green -> green()
37 | ThemeColor.Pink -> pink()
38 | }
39 |
40 | val systemTheme =
41 | if (!isSystemInDarkTheme()) {
42 | colorPair.first
43 | } else {
44 | colorPair.second
45 | }
46 |
47 | val colors =
48 | when (themeMode) {
49 | ThemeMode.System -> systemTheme
50 | ThemeMode.Light -> colorPair.first
51 | ThemeMode.Dark -> colorPair.second
52 | }
53 |
54 | MaterialTheme(
55 | colorScheme = colors,
56 | typography = Typography,
57 | shapes = Shapes,
58 | content = content,
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 🌟 Feature request
2 | description: Suggest a feature to improve the app
3 | labels: [feature]
4 | body:
5 | - type: textarea
6 | id: feature-description
7 | attributes:
8 | label: Describe your suggested feature
9 | description: How can the app be improved?
10 | placeholder: |
11 | Example:
12 | "It should work like this..."
13 | validations:
14 | required: true
15 |
16 | - type: textarea
17 | id: other-details
18 | attributes:
19 | label: Other details
20 | placeholder: Additional details and attachments.
21 |
22 | - type: checkboxes
23 | id: acknowledgements
24 | attributes:
25 | label: Acknowledgements
26 | description: >-
27 | Read this carefully, we will close and ignore your issue if you skimmed through this.
28 | options:
29 | - label: I have written a short but informative title.
30 | required: true
31 | - label: I have updated the app to **[the latest version](https://github.com/dessalines/rank-my-favs/releases/latest)**.
32 | required: true
33 | - label: I have checked through the app settings for my feature.
34 | required: true
35 | - label: >-
36 | I have searched the existing issues and this is a new one, **NOT** a
37 | duplicate or related to another open issue.
38 | required: true
39 | - label: >-
40 | This is a **single** feature request, in case of multiple feature
41 | requests I will open a separate issue for each one
42 | (they can always link to each other if related)
43 | required: true
44 | - label: >-
45 | This is not a question or a discussion, in which case I should have
46 | gone to [lemmy.ml/c/rankmyfavs](https://lemmy.ml/c/rankmyfavs)
47 | required: true
48 | - label: I have admitted that I am a clown by having checked this box, as I have not read these acknowledgements. 🤡
49 | - label: I will fill out all of the requested information in this form.
50 | required: true
51 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/db/Migrations.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.db
2 |
3 | import androidx.room.migration.Migration
4 | import androidx.sqlite.db.SupportSQLiteDatabase
5 |
6 | val MIGRATION_1_2 =
7 | object : Migration(1, 2) {
8 | override fun migrate(db: SupportSQLiteDatabase) {
9 | // Add min_confidence to settings
10 | db.execSQL(
11 | """
12 | ALTER TABLE AppSettings
13 | ADD COLUMN min_confidence
14 | INTEGER NOT NULL DEFAULT $DEFAULT_MIN_CONFIDENCE
15 | """.trimIndent(),
16 | )
17 |
18 | // Add match_count to favlistitem
19 | db.execSQL(
20 | """
21 | ALTER TABLE FavListItem
22 | ADD COLUMN match_count
23 | INTEGER NOT NULL DEFAULT 0
24 | """.trimIndent(),
25 | )
26 | }
27 | }
28 |
29 | val MIGRATION_2_3 =
30 | object : Migration(2, 3) {
31 | override fun migrate(db: SupportSQLiteDatabase) {
32 | // Add TierList table
33 | db.execSQL(
34 | """
35 | CREATE TABLE IF NOT EXISTS TierList (
36 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
37 | fav_list_id INTEGER NOT NULL,
38 | name TEXT NOT NULL,
39 | color INTEGER NOT NULL,
40 | tier_order INTEGER NOT NULL,
41 | FOREIGN KEY(fav_list_id) REFERENCES FavList(id) ON DELETE CASCADE
42 | )
43 | """.trimIndent(),
44 | )
45 |
46 | // Create an index on fav_list_id to optimize queries
47 | db.execSQL(
48 | """
49 | CREATE INDEX index_TierList_fav_list_id ON TierList(fav_list_id)
50 | """.trimIndent(),
51 | )
52 |
53 | // Create an index on tier_order to support ordering
54 | db.execSQL(
55 | """
56 | CREATE INDEX index_TierList_tier_order ON TierList(tier_order)
57 | """.trimIndent(),
58 | )
59 |
60 | // Add tier_list_initialized to FavList
61 | db.execSQL(
62 | """
63 | ALTER TABLE FavList
64 | ADD COLUMN tier_list_initialized
65 | INTEGER NOT NULL DEFAULT 0
66 | """.trimIndent(),
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlist/FavListForm.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlist
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.OutlinedTextField
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.saveable.rememberSaveable
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import com.dessalines.rankmyfavs.R
18 | import com.dessalines.rankmyfavs.db.FavList
19 | import com.dessalines.rankmyfavs.ui.components.common.SMALL_PADDING
20 | import com.dessalines.rankmyfavs.utils.nameIsValid
21 |
22 | @Composable
23 | fun FavListForm(
24 | favList: FavList? = null,
25 | onChange: (FavList) -> Unit,
26 | ) {
27 | var name by rememberSaveable {
28 | mutableStateOf(favList?.name.orEmpty())
29 | }
30 |
31 | var description by rememberSaveable {
32 | mutableStateOf(favList?.description.orEmpty())
33 | }
34 |
35 | Column(
36 | modifier = Modifier.padding(horizontal = SMALL_PADDING),
37 | verticalArrangement = Arrangement.spacedBy(SMALL_PADDING),
38 | ) {
39 | OutlinedTextField(
40 | label = { Text(stringResource(R.string.title)) },
41 | singleLine = true,
42 | modifier = Modifier.fillMaxWidth(),
43 | value = name,
44 | isError = !nameIsValid(name),
45 | onValueChange = {
46 | name = it
47 | onChange(
48 | FavList(
49 | id = favList?.id ?: 0,
50 | name,
51 | description,
52 | ),
53 | )
54 | },
55 | )
56 |
57 | OutlinedTextField(
58 | label = { Text(stringResource(R.string.description)) },
59 | modifier = Modifier.fillMaxWidth(),
60 | value = description,
61 | onValueChange = {
62 | description = it
63 | onChange(
64 | FavList(
65 | id = favList?.id ?: 0,
66 | name,
67 | description,
68 | ),
69 | )
70 | },
71 | )
72 | }
73 | }
74 |
75 | @Composable
76 | @Preview
77 | fun FavListFormPreview() {
78 | FavListForm(onChange = {})
79 | }
80 |
--------------------------------------------------------------------------------
/cliff.toml:
--------------------------------------------------------------------------------
1 | # git-cliff ~ configuration file
2 | # https://git-cliff.org/docs/configuration
3 |
4 | [remote.github]
5 | owner = "dessalines"
6 | repo = "rank-my-favs"
7 | # token = ""
8 |
9 | [changelog]
10 | # template for the changelog body
11 | # https://keats.github.io/tera/docs/#introduction
12 | body = """
13 | ## What's Changed
14 |
15 | {%- if version %} in {{ version }}{%- endif -%}
16 | {% for commit in commits %}
17 | {% if commit.remote.pr_title -%}
18 | {%- set commit_message = commit.remote.pr_title -%}
19 | {%- else -%}
20 | {%- set commit_message = commit.message -%}
21 | {%- endif -%}
22 | * {{ commit_message | split(pat="\n") | first | trim }}\
23 | {% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
24 | {% if commit.remote.pr_number %} in \
25 | [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
26 | {%- endif %}
27 | {%- endfor -%}
28 |
29 | {%- if github -%}
30 | {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
31 | {% raw %}\n{% endraw -%}
32 | ## New Contributors
33 | {%- endif %}\
34 | {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
35 | * @{{ contributor.username }} made their first contribution
36 | {%- if contributor.pr_number %} in \
37 | [#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
38 | {%- endif %}
39 | {%- endfor -%}
40 | {%- endif -%}
41 |
42 | {% if version %}
43 | {% if previous.version %}
44 | **Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
45 | {% endif %}
46 | {% else -%}
47 | {% raw %}\n{% endraw %}
48 | {% endif %}
49 |
50 | {%- macro remote_url() -%}
51 | https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
52 | {%- endmacro -%}
53 | """
54 | # remove the leading and trailing whitespace from the template
55 | trim = true
56 | # template for the changelog footer
57 | footer = """
58 |
59 | """
60 | # postprocessors
61 | postprocessors = []
62 |
63 | [git]
64 | # parse the commits based on https://www.conventionalcommits.org
65 | conventional_commits = false
66 | # filter out the commits that are not conventional
67 | filter_unconventional = true
68 | # process each line of a commit as an individual commit
69 | split_commits = false
70 | # regex for preprocessing the commit messages
71 | commit_preprocessors = [
72 | # remove issue numbers from commits
73 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
74 | ]
75 | commit_parsers = [
76 | { field = "author.name", pattern = "renovate", skip = true },
77 | { field = "message", pattern = "Upping version", skip = true },
78 | ]
79 | # filter out the commits that are not matched by commit parsers
80 | filter_commits = false
81 | # sort the tags topologically
82 | topo_order = false
83 | # sort the commits inside sections by oldest/newest order
84 | sort_commits = "newest"
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/db/AppDB.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.db
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE
6 | import androidx.room.Database
7 | import androidx.room.Room
8 | import androidx.room.RoomDatabase
9 | import androidx.sqlite.db.SupportSQLiteDatabase
10 | import com.dessalines.rankmyfavs.utils.TAG
11 | import java.util.concurrent.Executors
12 |
13 | @Database(
14 | version = 3,
15 | entities = [
16 | AppSettings::class, FavList::class, FavListItem::class, FavListMatch::class, TierList::class,
17 | ],
18 | exportSchema = true,
19 | )
20 | abstract class AppDB : RoomDatabase() {
21 | abstract fun appSettingsDao(): AppSettingsDao
22 |
23 | abstract fun favListDao(): FavListDao
24 |
25 | abstract fun favListItemDao(): FavListItemDao
26 |
27 | abstract fun tierListDao(): TierListDao
28 |
29 | abstract fun favListMatchDao(): FavListMatchDao
30 |
31 | companion object {
32 | @Volatile
33 | private var instance: AppDB? = null
34 |
35 | fun getDatabase(context: Context): AppDB {
36 | // if the INSTANCE is not null, then return it,
37 | // if it is, then create the database
38 | return instance ?: synchronized(this) {
39 | val i =
40 | Room
41 | .databaseBuilder(
42 | context.applicationContext,
43 | AppDB::class.java,
44 | TAG,
45 | ).allowMainThreadQueries()
46 | .addMigrations(
47 | MIGRATION_1_2,
48 | MIGRATION_2_3,
49 | )
50 | // Necessary because it can't insert data on creation
51 | .addCallback(
52 | object : Callback() {
53 | override fun onOpen(db: SupportSQLiteDatabase) {
54 | super.onCreate(db)
55 | Executors.newSingleThreadExecutor().execute {
56 | db.insert(
57 | "AppSettings",
58 | // Ensures it won't overwrite the existing data
59 | CONFLICT_IGNORE,
60 | ContentValues(2).apply {
61 | put("id", 1)
62 | },
63 | )
64 | }
65 | }
66 | },
67 | ).build()
68 | instance = i
69 | // return instance
70 | i
71 | }
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_report.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Issue report
2 | description: Report an issue or bug
3 | labels: [bug]
4 | body:
5 | - type: textarea
6 | id: reproduce-steps
7 | attributes:
8 | label: Steps to reproduce
9 | description: Provide an example of the issue.
10 | placeholder: |
11 | Example:
12 | 1. First step
13 | 2. Second step
14 | 3. Issue here
15 | validations:
16 | required: true
17 |
18 | - type: textarea
19 | id: expected-behavior
20 | attributes:
21 | label: Expected behavior
22 | description: Explain what you should expect to happen.
23 | placeholder: |
24 | Example:
25 | "This should happen..."
26 | validations:
27 | required: true
28 |
29 | - type: textarea
30 | id: actual-behavior
31 | attributes:
32 | label: Actual behavior
33 | description: Explain what actually happens.
34 | placeholder: |
35 | Example:
36 | "This happened instead..."
37 | validations:
38 | required: true
39 |
40 | - type: input
41 | id: version
42 | attributes:
43 | label: version of the program
44 | placeholder: >-
45 | Example: "2.6.14"
46 | validations:
47 | required: true
48 |
49 | - type: input
50 | id: android-version
51 | attributes:
52 | label: Android version
53 | description: You can find this somewhere in your Android settings.
54 | placeholder: |
55 | Example: "Android 14"
56 | validations:
57 | required: true
58 |
59 | - type: input
60 | id: device
61 | attributes:
62 | label: Device
63 | description: List your device and model.
64 | placeholder: |
65 | Example: "Google Pixel 8"
66 | validations:
67 | required: true
68 |
69 | - type: textarea
70 | id: other-details
71 | attributes:
72 | label: Other details
73 | placeholder: Additional details and attachments.
74 |
75 | - type: checkboxes
76 | id: acknowledgements
77 | attributes:
78 | label: Acknowledgements
79 | description: >-
80 | Read this carefully, I will close and ignore your issue if you skimmed through this.
81 | options:
82 | - label: I have written a short but informative title.
83 | required: true
84 | - label: I have updated the app to **[the latest version](https://github.com/dessalines/rank-my-favs/releases/latest)**.
85 | required: true
86 | - label: >-
87 | I have searched the existing issues and this is a new one, **NOT** a
88 | duplicate or related to another open issue.
89 | required: true
90 | - label: >-
91 | This is not a question or a discussion, in which case I should have
92 | gone to [lemmy.ml/c/rankmyfavs](https://lemmy.ml/c/rankmyfavs)
93 | required: true
94 | - label: >-
95 | This is a **single** bug report, in case of multiple bugs I will open
96 | a separate issue for each one
97 | (they can always link to each other if related)
98 | required: true
99 | - label: I have admitted that I am a clown by having checked this box, as I have not read these acknowledgements. 🤡
100 | - label: I have filled out all of the requested information in this form.
101 | required: true
102 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlistitem/FavListItemForm.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlistitem
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.material3.OutlinedTextField
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.saveable.rememberSaveable
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.tooling.preview.Preview
17 | import com.dessalines.rankmyfavs.R
18 | import com.dessalines.rankmyfavs.db.FavListItem
19 | import com.dessalines.rankmyfavs.ui.components.common.SMALL_PADDING
20 | import com.dessalines.rankmyfavs.utils.nameIsValid
21 |
22 | @Composable
23 | fun FavListItemForm(
24 | favListItem: FavListItem? = null,
25 | onChange: (FavListItem) -> Unit,
26 | ) {
27 | var name by rememberSaveable {
28 | mutableStateOf(favListItem?.name.orEmpty())
29 | }
30 |
31 | var description by rememberSaveable {
32 | mutableStateOf(favListItem?.description.orEmpty())
33 | }
34 |
35 | Column(
36 | modifier = Modifier.padding(horizontal = SMALL_PADDING),
37 | verticalArrangement = Arrangement.spacedBy(SMALL_PADDING),
38 | ) {
39 | OutlinedTextField(
40 | label = { Text(stringResource(R.string.title)) },
41 | singleLine = true,
42 | modifier = Modifier.fillMaxWidth(),
43 | value = name,
44 | isError = !nameIsValid(name),
45 | onValueChange = {
46 | name = it
47 | onChange(
48 | FavListItem(
49 | id = favListItem?.id ?: 0,
50 | favListId = favListItem?.favListId ?: 0,
51 | name = name,
52 | description = description,
53 | winRate = favListItem?.winRate ?: 0F,
54 | glickoRating = favListItem?.glickoRating ?: 0F,
55 | glickoDeviation = favListItem?.glickoDeviation ?: 0F,
56 | glickoVolatility = favListItem?.glickoVolatility ?: 0F,
57 | matchCount = favListItem?.matchCount ?: 0,
58 | ),
59 | )
60 | },
61 | )
62 |
63 | OutlinedTextField(
64 | label = { Text(stringResource(R.string.description)) },
65 | modifier = Modifier.fillMaxWidth(),
66 | value = description,
67 | onValueChange = {
68 | description = it
69 | onChange(
70 | FavListItem(
71 | id = favListItem?.id ?: 0,
72 | favListId = favListItem?.favListId ?: 0,
73 | name = name,
74 | description = description,
75 | winRate = favListItem?.winRate ?: 0F,
76 | glickoRating = favListItem?.glickoRating ?: 0F,
77 | glickoDeviation = favListItem?.glickoDeviation ?: 0F,
78 | glickoVolatility = favListItem?.glickoVolatility ?: 0F,
79 | matchCount = favListItem?.matchCount ?: 0,
80 | ),
81 | )
82 | },
83 | )
84 | }
85 | }
86 |
87 | @Composable
88 | @Preview
89 | fun FavListItemFormPreview() {
90 | FavListItemForm(onChange = {})
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Rank My Favs
3 | Lists
4 | Items
5 | Settings
6 | System
7 | Light
8 | Dark
9 | Dynamic
10 | Green
11 | Pink
12 | About
13 | What\'s New
14 | Version %1$s
15 | Releases
16 | Support
17 | Issue tracker
18 | Developer Matrix chatroom
19 | Donate to Rank My Favs
20 | Social
21 | Join c/rankmyfavs
22 | Follow me on Mastodon
23 | Open source
24 | Source code
25 | Rank-My-Favs is libre open-source software, licensed under the GNU Affero General Public License v3.0
26 | Theme
27 | Theme color
28 | Done
29 | Create List
30 | Edit List
31 | Save
32 | Title
33 | Description
34 | Delete
35 | Create Item
36 | Edit Item
37 | Go Back
38 | List Deleted
39 | Item Deleted
40 | Rate
41 | No more training necessary.
42 | Rating
43 | Deviation
44 | Win Rate
45 | No Items. Create one to get started.
46 | No lists. Create one to get started.
47 | What do these numbers mean?
48 | Clear Stats
49 | Import List
50 | Tier List
51 | Limit top X number of items (optional)
52 | Copy-Paste an existing list for an import.\n\nEach line should be a different item, of the form:\n- Name | Description\n\nThe starting - and | are optional.
53 | Export List as CSV
54 | Confidence
55 | Match Count
56 | Minimum Confidence: %1$s%%
57 | Higher requires more matches
58 | List already exists.
59 | Cancel
60 | Are you sure?
61 | Yes
62 | Skip
63 | Search
64 | Hide Search Bar
65 | More Actions
66 | Backup Database
67 | Restore Database
68 | Warning: This will clear out your current database
69 | Database Restored.
70 | Database Backed up.
71 | Export List as Markdown
72 | Pick a Color
73 | Enter New Tier Name
74 | Add New Tier
75 | Move Up
76 | Move Down
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlistitem/CreateFavListItemScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlistitem
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.imePadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.rememberBasicTooltipState
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Save
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TooltipAnchorPosition
20 | import androidx.compose.material3.TooltipDefaults
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.res.stringResource
25 | import androidx.navigation.NavController
26 | import com.dessalines.rankmyfavs.R
27 | import com.dessalines.rankmyfavs.db.FavListItem
28 | import com.dessalines.rankmyfavs.db.FavListItemInsert
29 | import com.dessalines.rankmyfavs.db.FavListItemViewModel
30 | import com.dessalines.rankmyfavs.ui.components.common.BackButton
31 | import com.dessalines.rankmyfavs.ui.components.common.ToolTip
32 | import com.dessalines.rankmyfavs.utils.nameIsValid
33 |
34 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
35 | @Composable
36 | fun CreateFavListItemScreen(
37 | navController: NavController,
38 | favListItemViewModel: FavListItemViewModel,
39 | favListId: Int,
40 | ) {
41 | val scrollState = rememberScrollState()
42 | val tooltipPosition = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above)
43 |
44 | var favListItem: FavListItem? = null
45 |
46 | Scaffold(
47 | topBar = {
48 | TopAppBar(
49 | title = { Text(stringResource(R.string.create_item)) },
50 | navigationIcon = {
51 | BackButton(
52 | onBackClick = { navController.navigateUp() },
53 | )
54 | },
55 | )
56 | },
57 | content = { padding ->
58 | Column(
59 | modifier =
60 | Modifier
61 | .padding(padding)
62 | .verticalScroll(scrollState)
63 | .imePadding(),
64 | ) {
65 | FavListItemForm(
66 | onChange = { favListItem = it },
67 | )
68 | }
69 | },
70 | floatingActionButton = {
71 | BasicTooltipBox(
72 | positionProvider = tooltipPosition,
73 | state = rememberBasicTooltipState(isPersistent = false),
74 | tooltip = {
75 | ToolTip(stringResource(R.string.save))
76 | },
77 | ) {
78 | FloatingActionButton(
79 | modifier = Modifier.imePadding(),
80 | onClick = {
81 | favListItem?.let {
82 | if (nameIsValid(it.name)) {
83 | val insert =
84 | FavListItemInsert(
85 | favListId = favListId,
86 | name = it.name,
87 | description = it.description,
88 | )
89 | favListItemViewModel.insert(insert)
90 | navController.navigateUp()
91 | }
92 | }
93 | },
94 | shape = CircleShape,
95 | ) {
96 | Icon(
97 | imageVector = Icons.Outlined.Save,
98 | contentDescription = stringResource(R.string.save),
99 | )
100 | }
101 | }
102 | },
103 | )
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/db/FavList.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.db
2 |
3 | import androidx.annotation.Keep
4 | import androidx.annotation.WorkerThread
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.ViewModelProvider
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.room.ColumnInfo
9 | import androidx.room.Dao
10 | import androidx.room.Delete
11 | import androidx.room.Entity
12 | import androidx.room.Index
13 | import androidx.room.Insert
14 | import androidx.room.OnConflictStrategy
15 | import androidx.room.PrimaryKey
16 | import androidx.room.Query
17 | import androidx.room.Update
18 | import kotlinx.coroutines.flow.Flow
19 | import kotlinx.coroutines.launch
20 |
21 | @Entity(
22 | indices = [Index(value = ["name"], unique = true)],
23 | )
24 | @Keep
25 | data class FavList(
26 | @PrimaryKey(autoGenerate = true) val id: Int,
27 | @ColumnInfo(
28 | name = "name",
29 | )
30 | val name: String,
31 | @ColumnInfo(
32 | name = "description",
33 | )
34 | val description: String? = null,
35 | @ColumnInfo(
36 | name = "tier_list_initialized",
37 | )
38 | val tierListInitialized: Boolean = false,
39 | )
40 |
41 | @Entity
42 | data class FavListInsert(
43 | @ColumnInfo(
44 | name = "name",
45 | )
46 | val name: String,
47 | @ColumnInfo(
48 | name = "description",
49 | )
50 | val description: String? = null,
51 | @ColumnInfo(
52 | name = "tier_list_initialized",
53 | )
54 | val tierListInitialized: Boolean = false,
55 | )
56 |
57 | @Entity
58 | data class FavListUpdate(
59 | val id: Int,
60 | @ColumnInfo(
61 | name = "name",
62 | )
63 | val name: String,
64 | @ColumnInfo(
65 | name = "description",
66 | )
67 | val description: String? = null,
68 | @ColumnInfo(
69 | name = "tier_list_initialized",
70 | )
71 | val tierListInitialized: Boolean = false,
72 | )
73 |
74 | private const val BY_ID_QUERY = "SELECT * FROM FavList where id = :id"
75 |
76 | @Dao
77 | interface FavListDao {
78 | @Query("SELECT * FROM FavList")
79 | fun getAll(): Flow>
80 |
81 | @Query(BY_ID_QUERY)
82 | fun getById(id: Int): Flow
83 |
84 | @Query(BY_ID_QUERY)
85 | fun getByIdSync(id: Int): FavList?
86 |
87 | @Insert(entity = FavList::class, onConflict = OnConflictStrategy.IGNORE)
88 | fun insert(favList: FavListInsert): Long
89 |
90 | @Update(entity = FavList::class)
91 | suspend fun update(favList: FavListUpdate)
92 |
93 | @Delete
94 | suspend fun delete(favList: FavList)
95 | }
96 |
97 | // Declares the DAO as a private property in the constructor. Pass in the DAO
98 | // instead of the whole database, because you only need access to the DAO
99 | class FavListRepository(
100 | private val favListDao: FavListDao,
101 | ) {
102 | // Room executes all queries on a separate thread.
103 | // Observed Flow will notify the observer when the data has changed.
104 | val getAll = favListDao.getAll()
105 |
106 | fun getById(id: Int) = favListDao.getById(id)
107 |
108 | fun getByIdSync(id: Int) = favListDao.getByIdSync(id)
109 |
110 | fun insert(favList: FavListInsert) = favListDao.insert(favList)
111 |
112 | @WorkerThread
113 | suspend fun update(favList: FavListUpdate) = favListDao.update(favList)
114 |
115 | @WorkerThread
116 | suspend fun delete(favList: FavList) = favListDao.delete(favList)
117 | }
118 |
119 | class FavListViewModel(
120 | private val repository: FavListRepository,
121 | ) : ViewModel() {
122 | val getAll = repository.getAll
123 |
124 | fun getById(id: Int) = repository.getById(id)
125 |
126 | fun getByIdSync(id: Int) = repository.getByIdSync(id)
127 |
128 | fun insert(favList: FavListInsert) = repository.insert(favList)
129 |
130 | fun update(favList: FavListUpdate) =
131 | viewModelScope.launch {
132 | repository.update(favList)
133 | }
134 |
135 | fun delete(favList: FavList) =
136 | viewModelScope.launch {
137 | repository.delete(favList)
138 | }
139 | }
140 |
141 | class FavListViewModelFactory(
142 | private val repository: FavListRepository,
143 | ) : ViewModelProvider.Factory {
144 | override fun create(modelClass: Class): T {
145 | if (modelClass.isAssignableFrom(FavListViewModel::class.java)) {
146 | @Suppress("UNCHECKED_CAST")
147 | return FavListViewModel(repository) as T
148 | }
149 | throw IllegalArgumentException("Unknown ViewModel class")
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlist/EditFavListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlist
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.imePadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.rememberBasicTooltipState
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Save
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TooltipAnchorPosition
20 | import androidx.compose.material3.TooltipDefaults
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.runtime.mutableStateOf
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.runtime.setValue
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.res.stringResource
29 | import androidx.navigation.NavController
30 | import com.dessalines.rankmyfavs.R
31 | import com.dessalines.rankmyfavs.db.FavListUpdate
32 | import com.dessalines.rankmyfavs.db.FavListViewModel
33 | import com.dessalines.rankmyfavs.ui.components.common.BackButton
34 | import com.dessalines.rankmyfavs.ui.components.common.ToolTip
35 | import com.dessalines.rankmyfavs.utils.nameIsValid
36 |
37 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
38 | @Composable
39 | fun EditFavListScreen(
40 | navController: NavController,
41 | favListViewModel: FavListViewModel,
42 | id: Int,
43 | ) {
44 | val scrollState = rememberScrollState()
45 | val tooltipPosition = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above)
46 |
47 | val favList = favListViewModel.getByIdSync(id)
48 |
49 | // Copy the favlist from the DB first
50 | var editedList by remember {
51 | mutableStateOf(favList)
52 | }
53 |
54 | Scaffold(
55 | topBar = {
56 | TopAppBar(
57 | title = { Text(stringResource(R.string.edit_list)) },
58 | navigationIcon = {
59 | BackButton(
60 | onBackClick = { navController.navigateUp() },
61 | )
62 | },
63 | )
64 | },
65 | content = { padding ->
66 | Column(
67 | modifier =
68 | Modifier
69 | .padding(padding)
70 | .verticalScroll(scrollState)
71 | .imePadding(),
72 | ) {
73 | FavListForm(
74 | favList = editedList,
75 | onChange = { editedList = it },
76 | )
77 | }
78 | },
79 | floatingActionButton = {
80 | BasicTooltipBox(
81 | positionProvider = tooltipPosition,
82 | state = rememberBasicTooltipState(isPersistent = false),
83 | tooltip = {
84 | ToolTip(stringResource(R.string.save))
85 | },
86 | ) {
87 | FloatingActionButton(
88 | modifier = Modifier.imePadding(),
89 | onClick = {
90 | editedList?.let {
91 | if (nameIsValid(it.name)) {
92 | val update =
93 | FavListUpdate(
94 | id = it.id,
95 | name = it.name,
96 | description = it.description,
97 | )
98 | favListViewModel.update(update)
99 | navController.navigateUp()
100 | }
101 | }
102 | },
103 | shape = CircleShape,
104 | ) {
105 | Icon(
106 | imageVector = Icons.Outlined.Save,
107 | contentDescription = stringResource(R.string.save),
108 | )
109 | }
110 | }
111 | },
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlistitem/EditFavListItemScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlistitem
2 |
3 | import androidx.compose.foundation.BasicTooltipBox
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.imePadding
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.rememberBasicTooltipState
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.shape.CircleShape
11 | import androidx.compose.foundation.verticalScroll
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.outlined.Save
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TooltipAnchorPosition
20 | import androidx.compose.material3.TooltipDefaults
21 | import androidx.compose.material3.TopAppBar
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.getValue
24 | import androidx.compose.runtime.mutableStateOf
25 | import androidx.compose.runtime.remember
26 | import androidx.compose.runtime.setValue
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.res.stringResource
29 | import androidx.navigation.NavController
30 | import com.dessalines.rankmyfavs.R
31 | import com.dessalines.rankmyfavs.db.FavListItemUpdateNameAndDesc
32 | import com.dessalines.rankmyfavs.db.FavListItemViewModel
33 | import com.dessalines.rankmyfavs.ui.components.common.BackButton
34 | import com.dessalines.rankmyfavs.ui.components.common.ToolTip
35 | import com.dessalines.rankmyfavs.utils.nameIsValid
36 |
37 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
38 | @Composable
39 | fun EditFavListItemScreen(
40 | navController: NavController,
41 | favListItemViewModel: FavListItemViewModel,
42 | id: Int,
43 | ) {
44 | val scrollState = rememberScrollState()
45 | val tooltipPosition = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above)
46 |
47 | val favListItem = favListItemViewModel.getByIdSync(id)
48 |
49 | // Copy the favlist from the DB first
50 | var editedItem by remember {
51 | mutableStateOf(favListItem)
52 | }
53 |
54 | Scaffold(
55 | topBar = {
56 | TopAppBar(
57 | title = { Text(stringResource(R.string.edit_item)) },
58 | navigationIcon = {
59 | BackButton(
60 | onBackClick = { navController.navigateUp() },
61 | )
62 | },
63 | )
64 | },
65 | content = { padding ->
66 | Column(
67 | modifier =
68 | Modifier
69 | .padding(padding)
70 | .verticalScroll(scrollState)
71 | .imePadding(),
72 | ) {
73 | FavListItemForm(
74 | favListItem = favListItem,
75 | onChange = { editedItem = it },
76 | )
77 | }
78 | },
79 | floatingActionButton = {
80 | BasicTooltipBox(
81 | positionProvider = tooltipPosition,
82 | state = rememberBasicTooltipState(isPersistent = false),
83 | tooltip = {
84 | ToolTip(stringResource(R.string.save))
85 | },
86 | ) {
87 | FloatingActionButton(
88 | modifier = Modifier.imePadding(),
89 | onClick = {
90 | editedItem?.let {
91 | if (nameIsValid(it.name)) {
92 | val update =
93 | FavListItemUpdateNameAndDesc(
94 | id = it.id,
95 | name = it.name,
96 | description = it.description,
97 | )
98 | favListItemViewModel.updateNameAndDesc(update)
99 | navController.navigateUp()
100 | }
101 | }
102 | },
103 | shape = CircleShape,
104 | ) {
105 | Icon(
106 | imageVector = Icons.Outlined.Save,
107 | contentDescription = stringResource(R.string.save),
108 | )
109 | }
110 | }
111 | },
112 | )
113 | }
114 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/utils/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.utils
2 | import android.content.ContentResolver
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.PackageInfo
6 | import android.content.pm.PackageManager
7 | import android.graphics.Bitmap
8 | import android.net.Uri
9 | import android.os.Build
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.graphics.lerp
12 | import androidx.core.net.toUri
13 | import com.dessalines.rankmyfavs.db.FavListItem
14 | import com.dessalines.rankmyfavs.db.TierList
15 | import java.io.IOException
16 | import java.io.OutputStream
17 | import java.util.Random
18 |
19 | const val TAG = "com.rank-my-favs"
20 |
21 | const val GITHUB_URL = "https://github.com/dessalines/rank-my-favs"
22 | const val MATRIX_CHAT_URL = "https://matrix.to/#/#rank-my-favs:matrix.org"
23 | const val DONATE_URL = "https://liberapay.com/dessalines"
24 | const val LEMMY_URL = "https://lemmy.ml/c/rankmyfavs"
25 | const val MASTODON_URL = "https://mastodon.social/@dessalines"
26 | const val GLICKO_WIKI_URL = "https://en.m.wikipedia.org/wiki/Glicko_rating_system"
27 |
28 | val TIER_COLORS =
29 | mapOf(
30 | "S" to Color(0XFFFF7F7F),
31 | "A" to Color(0XFFFFBF7F),
32 | "B" to Color(0XFFFFDF7F),
33 | "C" to Color(0XFFFFFF7F),
34 | "D" to Color(0XFFBFFF7F),
35 | )
36 |
37 | fun openLink(
38 | url: String,
39 | ctx: Context,
40 | ) {
41 | val intent = Intent(Intent.ACTION_VIEW, url.toUri())
42 | ctx.startActivity(intent)
43 | }
44 |
45 | fun Context.getPackageInfo(): PackageInfo =
46 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
47 | packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0))
48 | } else {
49 | packageManager.getPackageInfo(packageName, 0)
50 | }
51 |
52 | fun Context.getVersionCode(): Int =
53 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
54 | getPackageInfo().longVersionCode.toInt()
55 | } else {
56 | @Suppress("DEPRECATION")
57 | getPackageInfo().versionCode
58 | }
59 |
60 | fun numToString(
61 | num: Float,
62 | decimalPlaces: Int,
63 | ): String = String.format("%.${decimalPlaces}f", num)
64 |
65 | fun writeData(
66 | ctx: Context,
67 | uri: Uri,
68 | data: String,
69 | ) {
70 | ctx.contentResolver.openOutputStream(uri)?.use {
71 | val bytes = data.toByteArray()
72 | it.write(bytes)
73 | }
74 | }
75 |
76 | fun writeBitmap(
77 | contentResolver: ContentResolver,
78 | uri: Uri,
79 | bitmap: Bitmap,
80 | ) {
81 | try {
82 | val outputStream: OutputStream? = contentResolver.openOutputStream(uri)
83 | outputStream?.use {
84 | bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
85 | } ?: throw IOException("Failed to get output stream")
86 | } catch (e: IOException) {
87 | e.printStackTrace()
88 | }
89 | }
90 |
91 | fun nameIsValid(name: String): Boolean = name.isNotEmpty()
92 |
93 | fun convertFavlistToMarkdown(
94 | title: String,
95 | favListItems: List,
96 | ): String {
97 | val items = favListItems.joinToString(separator = "\n") { "1. ${it.name}" }
98 | return "# $title\n\n$items"
99 | }
100 |
101 | fun generateRandomColor(): Color {
102 | val rnd = Random()
103 | return Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
104 | }
105 |
106 | fun assignTiersToItems(
107 | tiers: List,
108 | items: List,
109 | limit: Int? = null,
110 | ): Map> {
111 | if (tiers.isEmpty()) {
112 | return emptyMap()
113 | }
114 |
115 | // Sort items by glickoRating in descending order
116 | val sortedItems = items.sortedByDescending { it.glickoRating }
117 |
118 | // Apply limit if provided
119 | val limitedItems = limit?.let { sortedItems.take(it) } ?: sortedItems
120 |
121 | // Calculate tier thresholds
122 | val tierMap = mutableMapOf>()
123 |
124 | if (items.isNotEmpty()) {
125 | tiers.sortedBy { it.tierOrder }.forEachIndexed { index, tier ->
126 |
127 | val lowerBoundIndex =
128 | (limitedItems.size * index / tiers.size).coerceAtMost(limitedItems.size - 1)
129 | val upperBoundIndex =
130 | (limitedItems.size * (index + 1) / tiers.size).coerceAtMost(limitedItems.size)
131 |
132 | tierMap[tier] = limitedItems.subList(lowerBoundIndex, upperBoundIndex)
133 | }
134 | }
135 |
136 | return tierMap
137 | }
138 |
139 | fun Color.tint(factor: Float): Color = lerp(this, Color.White, factor)
140 |
141 | sealed interface SelectionVisibilityState {
142 | object NoSelection : SelectionVisibilityState
143 |
144 | data class ShowSelection(
145 | val selectedItem: Item,
146 | ) : SelectionVisibilityState
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/ui/components/favlist/CreateFavListScreen.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.ui.components.favlist
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.BasicTooltipBox
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.imePadding
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.rememberBasicTooltipState
10 | import androidx.compose.foundation.rememberScrollState
11 | import androidx.compose.foundation.shape.CircleShape
12 | import androidx.compose.foundation.verticalScroll
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.outlined.Save
15 | import androidx.compose.material3.ExperimentalMaterial3Api
16 | import androidx.compose.material3.FloatingActionButton
17 | import androidx.compose.material3.Icon
18 | import androidx.compose.material3.Scaffold
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TooltipAnchorPosition
21 | import androidx.compose.material3.TooltipDefaults
22 | import androidx.compose.material3.TopAppBar
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.res.stringResource
27 | import androidx.navigation.NavController
28 | import com.dessalines.rankmyfavs.R
29 | import com.dessalines.rankmyfavs.db.FavList
30 | import com.dessalines.rankmyfavs.db.FavListInsert
31 | import com.dessalines.rankmyfavs.db.FavListViewModel
32 | import com.dessalines.rankmyfavs.ui.components.common.BackButton
33 | import com.dessalines.rankmyfavs.ui.components.common.ToolTip
34 | import com.dessalines.rankmyfavs.utils.nameIsValid
35 |
36 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
37 | @Composable
38 | fun CreateFavListScreen(
39 | navController: NavController,
40 | favListViewModel: FavListViewModel,
41 | ) {
42 | val scrollState = rememberScrollState()
43 | val tooltipPosition = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above)
44 | val ctx = LocalContext.current
45 | val listAlreadyExistsStr = stringResource(R.string.list_already_exists)
46 |
47 | var favList: FavList? = null
48 |
49 | Scaffold(
50 | topBar = {
51 | TopAppBar(
52 | title = { Text(stringResource(R.string.create_list)) },
53 | navigationIcon = {
54 | BackButton(
55 | onBackClick = { navController.navigateUp() },
56 | )
57 | },
58 | )
59 | },
60 | content = { padding ->
61 | Column(
62 | modifier =
63 | Modifier
64 | .padding(padding)
65 | .verticalScroll(scrollState)
66 | .imePadding(),
67 | ) {
68 | FavListForm(
69 | onChange = { favList = it },
70 | )
71 | }
72 | },
73 | floatingActionButton = {
74 | BasicTooltipBox(
75 | positionProvider = tooltipPosition,
76 | state = rememberBasicTooltipState(isPersistent = false),
77 | tooltip = {
78 | ToolTip(stringResource(R.string.save))
79 | },
80 | ) {
81 | FloatingActionButton(
82 | modifier = Modifier.imePadding(),
83 | onClick = {
84 | favList?.let {
85 | if (nameIsValid(it.name)) {
86 | val insert =
87 | FavListInsert(name = it.name, description = it.description)
88 | val insertedId = favListViewModel.insert(insert)
89 |
90 | // The id is -1 if its a failed insert
91 | if (insertedId != -1L) {
92 | navController.navigate("favLists?favListId=$insertedId") {
93 | popUpTo("favLists")
94 | }
95 | } else {
96 | Toast
97 | .makeText(
98 | ctx,
99 | listAlreadyExistsStr,
100 | Toast.LENGTH_SHORT,
101 | ).show()
102 | }
103 | }
104 | }
105 | },
106 | shape = CircleShape,
107 | ) {
108 | Icon(
109 | imageVector = Icons.Outlined.Save,
110 | contentDescription = stringResource(R.string.save),
111 | )
112 | }
113 | }
114 | },
115 | )
116 | }
117 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/db/AppSettings.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.db
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.annotation.WorkerThread
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.ViewModelProvider
8 | import androidx.lifecycle.viewModelScope
9 | import androidx.room.ColumnInfo
10 | import androidx.room.Dao
11 | import androidx.room.Entity
12 | import androidx.room.PrimaryKey
13 | import androidx.room.Query
14 | import androidx.room.Update
15 | import com.dessalines.rankmyfavs.utils.TAG
16 | import kotlinx.coroutines.Dispatchers
17 | import kotlinx.coroutines.flow.Flow
18 | import kotlinx.coroutines.flow.MutableStateFlow
19 | import kotlinx.coroutines.flow.asStateFlow
20 | import kotlinx.coroutines.launch
21 | import kotlinx.coroutines.withContext
22 |
23 | const val DEFAULT_THEME = 0
24 | const val DEFAULT_THEME_COLOR = 0
25 | const val DEFAULT_MIN_CONFIDENCE = 85
26 | const val MIN_CONFIDENCE_BOUND = 80
27 |
28 | @Entity
29 | data class AppSettings(
30 | @PrimaryKey(autoGenerate = true) val id: Int,
31 | @ColumnInfo(
32 | name = "theme",
33 | defaultValue = DEFAULT_THEME.toString(),
34 | )
35 | val theme: Int,
36 | @ColumnInfo(
37 | name = "theme_color",
38 | defaultValue = DEFAULT_THEME_COLOR.toString(),
39 | )
40 | val themeColor: Int,
41 | @ColumnInfo(
42 | name = "last_version_code_viewed",
43 | defaultValue = "0",
44 | )
45 | val lastVersionCodeViewed: Int,
46 | @ColumnInfo(
47 | name = "min_confidence",
48 | defaultValue = DEFAULT_MIN_CONFIDENCE.toString(),
49 | )
50 | val minConfidence: Int,
51 | )
52 |
53 | data class SettingsUpdate(
54 | val id: Int,
55 | @ColumnInfo(
56 | name = "theme",
57 | )
58 | val theme: Int,
59 | @ColumnInfo(
60 | name = "theme_color",
61 | )
62 | val themeColor: Int,
63 | @ColumnInfo(
64 | name = "min_confidence",
65 | defaultValue = DEFAULT_MIN_CONFIDENCE.toString(),
66 | )
67 | val minConfidence: Int,
68 | )
69 |
70 | @Dao
71 | interface AppSettingsDao {
72 | @Query("SELECT * FROM AppSettings limit 1")
73 | fun getSettings(): Flow
74 |
75 | @Update(entity = AppSettings::class)
76 | suspend fun updateSettings(settings: SettingsUpdate)
77 |
78 | @Query("UPDATE AppSettings SET last_version_code_viewed = :versionCode")
79 | suspend fun updateLastVersionCode(versionCode: Int)
80 | }
81 |
82 | // Declares the DAO as a private property in the constructor. Pass in the DAO
83 | // instead of the whole database, because you only need access to the DAO
84 | class AppSettingsRepository(
85 | private val appSettingsDao: AppSettingsDao,
86 | ) {
87 | private val _changelog = MutableStateFlow("")
88 | val changelog = _changelog.asStateFlow()
89 |
90 | // Room executes all queries on a separate thread.
91 | // Observed Flow will notify the observer when the data has changed.
92 | val appSettings = appSettingsDao.getSettings()
93 |
94 | @WorkerThread
95 | suspend fun updateSettings(settings: SettingsUpdate) {
96 | appSettingsDao.updateSettings(settings)
97 | }
98 |
99 | @WorkerThread
100 | suspend fun updateLastVersionCodeViewed(versionCode: Int) {
101 | appSettingsDao.updateLastVersionCode(versionCode)
102 | }
103 |
104 | @WorkerThread
105 | suspend fun updateChangelog(ctx: Context) {
106 | withContext(Dispatchers.IO) {
107 | try {
108 | val releasesStr =
109 | ctx.assets
110 | .open("RELEASES.md")
111 | .bufferedReader()
112 | .use { it.readText() }
113 | _changelog.value = releasesStr
114 | } catch (e: Exception) {
115 | Log.e(TAG, "Failed to load changelog: $e")
116 | }
117 | }
118 | }
119 | }
120 |
121 | class AppSettingsViewModel(
122 | private val repository: AppSettingsRepository,
123 | ) : ViewModel() {
124 | val appSettings = repository.appSettings
125 | val changelog = repository.changelog
126 |
127 | fun updateSettings(settings: SettingsUpdate) =
128 | viewModelScope.launch {
129 | repository.updateSettings(settings)
130 | }
131 |
132 | fun updateLastVersionCodeViewed(versionCode: Int) =
133 | viewModelScope.launch {
134 | repository.updateLastVersionCodeViewed(versionCode)
135 | }
136 |
137 | fun updateChangelog(ctx: Context) =
138 | viewModelScope.launch {
139 | repository.updateChangelog(ctx)
140 | }
141 | }
142 |
143 | class AppSettingsViewModelFactory(
144 | private val repository: AppSettingsRepository,
145 | ) : ViewModelProvider.Factory {
146 | override fun create(modelClass: Class): T {
147 | if (modelClass.isAssignableFrom(AppSettingsViewModel::class.java)) {
148 | @Suppress("UNCHECKED_CAST")
149 | return AppSettingsViewModel(repository) as T
150 | }
151 | throw IllegalArgumentException("Unknown ViewModel class")
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/app/src/main/java/com/dessalines/rankmyfavs/db/FavListMatch.kt:
--------------------------------------------------------------------------------
1 | package com.dessalines.rankmyfavs.db
2 |
3 | import androidx.annotation.Keep
4 | import androidx.annotation.WorkerThread
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.ViewModelProvider
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.room.ColumnInfo
9 | import androidx.room.Dao
10 | import androidx.room.Delete
11 | import androidx.room.Entity
12 | import androidx.room.ForeignKey
13 | import androidx.room.Index
14 | import androidx.room.Insert
15 | import androidx.room.OnConflictStrategy
16 | import androidx.room.PrimaryKey
17 | import androidx.room.Query
18 | import kotlinx.coroutines.launch
19 |
20 | @Entity(
21 | foreignKeys = [
22 | ForeignKey(
23 | entity = FavListItem::class,
24 | parentColumns = arrayOf("id"),
25 | childColumns = arrayOf("item_id_1"),
26 | onDelete = ForeignKey.CASCADE,
27 | ),
28 | ForeignKey(
29 | entity = FavListItem::class,
30 | parentColumns = arrayOf("id"),
31 | childColumns = arrayOf("item_id_2"),
32 | onDelete = ForeignKey.CASCADE,
33 | ),
34 | ForeignKey(
35 | entity = FavListItem::class,
36 | parentColumns = arrayOf("id"),
37 | childColumns = arrayOf("winner_id"),
38 | onDelete = ForeignKey.CASCADE,
39 | ),
40 | ],
41 | indices = [
42 | Index(value = ["item_id_1"], unique = false),
43 | Index(value = ["item_id_2"], unique = false),
44 | Index(value = ["winner_id"], unique = false),
45 | ],
46 | )
47 | @Keep
48 | data class FavListMatch(
49 | @PrimaryKey(autoGenerate = true) val id: Int,
50 | @ColumnInfo(
51 | name = "item_id_1",
52 | )
53 | val itemId1: Int,
54 | @ColumnInfo(
55 | name = "item_id_2",
56 | )
57 | val itemId2: Int,
58 | /**
59 | * This can be either 1 or 2
60 | */
61 | @ColumnInfo(
62 | name = "winner_id",
63 | )
64 | val winnerId: Int,
65 | )
66 |
67 | @Entity
68 | data class FavListMatchInsert(
69 | @ColumnInfo(
70 | name = "item_id_1",
71 | )
72 | val itemId1: Int,
73 | @ColumnInfo(
74 | name = "item_id_2",
75 | )
76 | val itemId2: Int,
77 | @ColumnInfo(
78 | name = "winner_id",
79 | )
80 | val winnerId: Int,
81 | )
82 |
83 | @Dao
84 | interface FavListMatchDao {
85 | // TODO do this in SQL, not in code
86 | @Query("SELECT * FROM FavListMatch where item_id_1 = :itemId or item_id_2 = :itemId")
87 | fun getMatchups(itemId: Int): List
88 |
89 | @Insert(entity = FavListMatch::class, onConflict = OnConflictStrategy.IGNORE)
90 | fun insert(match: FavListMatchInsert): Long
91 |
92 | @Delete
93 | suspend fun delete(match: FavListMatch)
94 |
95 | @Query(
96 | """
97 | DELETE FROM FavListMatch
98 | WHERE item_id_1 in ( select id from FavListItem WHERE fav_list_id = :favListId)
99 | or item_id_2 in ( select id from FavListItem WHERE fav_list_id = :favListId)
100 | """,
101 | )
102 | suspend fun deleteMatchesForList(favListId: Int)
103 |
104 | @Query(
105 | """
106 | DELETE FROM FavListMatch
107 | WHERE item_id_1 = :itemId
108 | or item_id_2 = :itemId
109 | """,
110 | )
111 | suspend fun deleteMatchesForItem(itemId: Int)
112 | }
113 |
114 | // Declares the DAO as a private property in the constructor. Pass in the DAO
115 | // instead of the whole database, because you only need access to the DAO
116 | class FavListMatchRepository(
117 | private val favListDao: FavListMatchDao,
118 | ) {
119 | // Room executes all queries on a separate thread.
120 | // Observed Flow will notify the observer when the data has changed.
121 | fun getMatchups(itemId: Int) = favListDao.getMatchups(itemId)
122 |
123 | fun insert(match: FavListMatchInsert) = favListDao.insert(match)
124 |
125 | @WorkerThread
126 | suspend fun delete(match: FavListMatch) = favListDao.delete(match)
127 |
128 | @WorkerThread
129 | suspend fun deleteMatchesForList(favListId: Int) = favListDao.deleteMatchesForList(favListId)
130 |
131 | @WorkerThread
132 | suspend fun deleteMatchesForItem(itemId: Int) = favListDao.deleteMatchesForItem(itemId)
133 | }
134 |
135 | class FavListMatchViewModel(
136 | private val repository: FavListMatchRepository,
137 | ) : ViewModel() {
138 | fun getMatchups(itemId: Int) = repository.getMatchups(itemId)
139 |
140 | fun insert(match: FavListMatchInsert) = repository.insert(match)
141 |
142 | fun delete(match: FavListMatch) =
143 | viewModelScope.launch {
144 | repository.delete(match)
145 | }
146 |
147 | fun deleteMatchesForList(favListId: Int) =
148 | viewModelScope.launch {
149 | repository.deleteMatchesForList(favListId)
150 | }
151 |
152 | fun deleteMatchesForItem(itemId: Int) =
153 | viewModelScope.launch {
154 | repository.deleteMatchesForItem(itemId)
155 | }
156 | }
157 |
158 | class FavListMatchViewModelFactory(
159 | private val repository: FavListMatchRepository,
160 | ) : ViewModelProvider.Factory {
161 | override fun create(modelClass: Class): T {
162 | if (modelClass.isAssignableFrom(FavListMatchViewModel::class.java)) {
163 | @Suppress("UNCHECKED_CAST")
164 | return FavListMatchViewModel(repository) as T
165 | }
166 | throw IllegalArgumentException("Unknown ViewModel class")
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |