From 20afdd833018a62e5aeac483ffb1b2c733ed4c27 Mon Sep 17 00:00:00 2001 From: "Darryl L. Pierce" Date: Sat, 12 Jul 2025 08:56:16 -0400 Subject: [PATCH] feat: add selecting and deleting comics [#66] * add a viewmodel list to contain the selected comic filenames * change how the list of comics is passed to the list view --- README.md | 2 + .../variant/android/view/HomeView.kt | 31 +++++- .../view/comics/ComicBookListItemView.kt | 37 ++++++- .../android/view/comics/ComicBookListView.kt | 11 +- .../android/view/comics/ComicBookView.kt | 95 ++++++++++++++++-- .../drawable-anydpi/ic_selection_mode_off.xml | 17 ++++ .../drawable-anydpi/ic_selection_mode_on.xml | 17 ++++ .../drawable-hdpi/ic_selection_mode_off.png | Bin 0 -> 532 bytes .../drawable-hdpi/ic_selection_mode_on.png | Bin 0 -> 520 bytes .../drawable-mdpi/ic_selection_mode_off.png | Bin 0 -> 355 bytes .../drawable-mdpi/ic_selection_mode_on.png | Bin 0 -> 351 bytes .../drawable-xhdpi/ic_selection_mode_off.png | Bin 0 -> 616 bytes .../drawable-xhdpi/ic_selection_mode_on.png | Bin 0 -> 625 bytes .../drawable-xxhdpi/ic_selection_mode_off.png | Bin 0 -> 900 bytes .../drawable-xxhdpi/ic_selection_mode_on.png | Bin 0 -> 896 bytes .../src/main/res/values/strings.xml | 3 + .../iosVariant/Assets.xcassets/Contents.json | 2 +- .../selection_mode_off.imageset/Contents.json | 21 ++++ ...eshot-icon-off-switch-TQVBLRMESW-1772e.svg | 6 ++ .../selection_mode_on.imageset/Contents.json | 21 ++++ ...reshot-icon-on-switch-S253U8PFKM-f8513.svg | 6 ++ iosVariant/iosVariant/HomeView.swift | 47 +++++++-- .../Views/Comics/ComicBookListItemView.swift | 34 +++++-- .../Views/Comics/ComicBookListView.swift | 11 +- .../Views/Comics/ComicBooksView.swift | 77 ++++++++++++-- .../variant/viewmodel/VariantViewModel.kt | 47 +++++++++ 26 files changed, 435 insertions(+), 50 deletions(-) create mode 100644 androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_off.xml create mode 100644 androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_on.xml create mode 100644 androidVariant/src/main/res/drawable-hdpi/ic_selection_mode_off.png create mode 100644 androidVariant/src/main/res/drawable-hdpi/ic_selection_mode_on.png create mode 100644 androidVariant/src/main/res/drawable-mdpi/ic_selection_mode_off.png create mode 100644 androidVariant/src/main/res/drawable-mdpi/ic_selection_mode_on.png create mode 100644 androidVariant/src/main/res/drawable-xhdpi/ic_selection_mode_off.png create mode 100644 androidVariant/src/main/res/drawable-xhdpi/ic_selection_mode_on.png create mode 100644 androidVariant/src/main/res/drawable-xxhdpi/ic_selection_mode_off.png create mode 100644 androidVariant/src/main/res/drawable-xxhdpi/ic_selection_mode_on.png create mode 100644 iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/Contents.json create mode 100644 iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/reshot-icon-off-switch-TQVBLRMESW-1772e.svg create mode 100644 iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/Contents.json create mode 100644 iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/reshot-icon-on-switch-S253U8PFKM-f8513.svg diff --git a/README.md b/README.md index e98673ca..30a60d23 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,5 @@ To achieve these goals, the project will: # Credits + +[Reshot](https://www.reshot.com/free-svg-icons/) - for toolbar icons. \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/HomeView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/HomeView.kt index ed04815d..183f7096 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/HomeView.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/HomeView.kt @@ -36,14 +36,20 @@ import org.comixedproject.variant.android.view.comics.ComicBookView import org.comixedproject.variant.android.view.reading.ReadingView import org.comixedproject.variant.android.view.server.ServerView import org.comixedproject.variant.android.view.settings.SettingsView +import org.comixedproject.variant.platform.Log import org.comixedproject.variant.viewmodel.VariantViewModel import org.koin.androidx.compose.koinViewModel +private const val TAG = "HomeView" + @Composable fun HomeView() { val variantViewModel: VariantViewModel = koinViewModel() var currentDestination by remember { mutableStateOf(AppDestination.COMICS) } val coroutineScope = rememberCoroutineScope() + val comicBookList by variantViewModel.comicBookList.collectAsState() + val selectionMode by variantViewModel.selectionMode.collectAsState() + val selectionList by variantViewModel.selectionList.collectAsState() val comicBook by variantViewModel.comicBook.collectAsState() Scaffold( @@ -72,8 +78,29 @@ fun HomeView() { ) } else { ComicBookView( - onReadComicBook = { comicBook -> - variantViewModel.readComicBook(comicBook) + comicBookList, + selectionMode, + selectionList, + onSetSelectionMode = { + Log.info(TAG, "Setting selection mode: ${it}") + variantViewModel.setSelectMode(it) + }, + onComicBookClicked = { comicBook -> + if (selectionMode) { + Log.info( + TAG, + "Toggling comic book selection: ${comicBook.path}" + ) + variantViewModel.updateSelectionList(comicBook.path) + } else { + Log.info(TAG, "Reading comic book: ${comicBook.filename}") + variantViewModel.readComicBook(comicBook) + } + }, + onDeleteComics = { + coroutineScope.launch(Dispatchers.IO) { + variantViewModel.deleteSelections() + } }, modifier = Modifier.padding(padding) ) diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListItemView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListItemView.kt index e60648ce..0fab71c9 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListItemView.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListItemView.kt @@ -1,8 +1,10 @@ package org.comixedproject.variant.android.view.comics import android.graphics.BitmapFactory +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.CardDefaults @@ -17,10 +19,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.comixedproject.variant.adaptor.ArchiveAPI @@ -33,23 +37,34 @@ import org.comixedproject.variant.platform.Log private val TAG = "ComicBookListItemView" +@OptIn(ExperimentalFoundationApi::class) @Composable fun ComicBookListItemView( comicBook: ComicBook, + selected: Boolean, onClick: (ComicBook) -> Unit, modifier: Modifier = Modifier ) { var coverContent by remember { mutableStateOf(null) } val coroutineScope = rememberCoroutineScope() + val borderWidth = when (selected) { + true -> 5.dp + false -> 0.dp + } ElevatedCard( colors = CardDefaults.cardColors(containerColor = colorScheme.surface), modifier = modifier .fillMaxWidth() + .border(borderWidth, Color.Red) ) { val title = MetadataAPI.displayableTitle(comicBook) - Column(modifier = Modifier.clickable(onClick = { onClick(comicBook) })) { + Column( + modifier = Modifier.combinedClickable( + onClick = { onClick(comicBook) } + ) + ) { comicBook.pages.firstOrNull()?.let { cover -> if (coverContent == null) { Image( @@ -91,5 +106,21 @@ fun ComicBookListItemView( @Composable @Preview fun ComicBookListItemViewPreview() { - VariantTheme { ComicBookListItemView(comicBook = COMIC_BOOK_LIST.get(0), onClick = {}) } + VariantTheme { + ComicBookListItemView( + comicBook = COMIC_BOOK_LIST.get(0), + false, + onClick = {}) + } +} + +@Composable +@Preview +fun ComicBookListItemViewSelectedPreview() { + VariantTheme { + ComicBookListItemView( + comicBook = COMIC_BOOK_LIST.get(0), + true, + onClick = {}) + } } \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListView.kt index 9aa73583..143a1825 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListView.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookListView.kt @@ -41,6 +41,7 @@ private val TAG = "ComicBookListView" @Composable fun ComicBookListView( comicBookList: List, + selectionList: List, onClick: (ComicBook) -> Unit, modifier: Modifier = Modifier ) { @@ -61,11 +62,13 @@ fun ComicBookListView( items(comicBookList) { comicBook -> ComicBookListItemView( comicBook, - onClick = { onClick(it) }, - modifier = Modifier.padding(padding) + selectionList.contains(comicBook.path), + onClick = { onClick(it) } ) } - }) + }, + modifier = modifier.padding(padding) + ) } }, modifier = modifier.padding(8.dp) ) @@ -75,6 +78,6 @@ fun ComicBookListView( @Preview fun ComicBookListViewPreview() { VariantTheme { - ComicBookListView(COMIC_BOOK_LIST, onClick = {}) + ComicBookListView(COMIC_BOOK_LIST, emptyList(), onClick = {}) } } \ No newline at end of file diff --git a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookView.kt b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookView.kt index 6b722b00..1ca9aa1f 100644 --- a/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookView.kt +++ b/androidVariant/src/main/java/org/comixedproject/variant/android/view/comics/ComicBookView.kt @@ -18,28 +18,105 @@ package org.comixedproject.variant.android.view.comics +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import org.comixedproject.variant.android.COMIC_BOOK_LIST +import org.comixedproject.variant.android.R import org.comixedproject.variant.android.VariantTheme import org.comixedproject.variant.model.library.ComicBook -import org.comixedproject.variant.viewmodel.VariantViewModel -import org.koin.androidx.compose.koinViewModel +import org.comixedproject.variant.platform.Log private val TAG = "ComicBookView" @Composable -fun ComicBookView(onReadComicBook: (ComicBook) -> Unit, modifier: Modifier = Modifier) { - val variantViewModel: VariantViewModel = koinViewModel() - val comicBookList by variantViewModel.comicBookList.collectAsState() +fun ComicBookView( + comicBookList: List, + selectionMode: Boolean, + selectionList: List, + onSetSelectionMode: (Boolean) -> Unit, + onComicBookClicked: (ComicBook) -> Unit, + onDeleteComics: () -> Unit, + modifier: Modifier = Modifier +) { + Scaffold( + content = { padding -> + ComicBookListView( + comicBookList, + selectionList, + onClick = { onComicBookClicked(it) }, + modifier = modifier.padding(padding) + ) + }, + bottomBar = { + BottomAppBar( + actions = { + if (selectionMode) { + IconButton(onClick = { onSetSelectionMode(false) }) { + Icon( + painterResource(id = R.drawable.ic_selection_mode_on), + contentDescription = stringResource(R.string.markReadLabel) + ) + } + } else { + IconButton(onClick = { onSetSelectionMode(true) }) { + Icon( + painterResource(id = R.drawable.ic_selection_mode_off), + contentDescription = stringResource(R.string.markReadLabel) + ) + } + } + if (!selectionList.isEmpty()) { + IconButton(enabled = !selectionList.isEmpty(), onClick = { + Log.info(TAG, "Deleting ${selectionList.size} comic book(s)") + onDeleteComics() + }) { + Icon( + Icons.Filled.Delete, + contentDescription = stringResource(R.string.deleteSelectionsLabel) + ) + } + } - ComicBookListView(comicBookList, onClick = { onReadComicBook(it) }, modifier = modifier) + } + ) + } + ) } @Composable @Preview fun ComicBookViewPreview() { - VariantTheme { ComicBookView(onReadComicBook = { }) } + VariantTheme { + ComicBookView( + COMIC_BOOK_LIST, + false, + emptyList(), + onSetSelectionMode = { _ -> }, + onComicBookClicked = { _ -> }, + onDeleteComics = { }) + } +} + +@Composable +@Preview +fun ComicBookViewWithSelectionsPreview() { + VariantTheme { + ComicBookView( + COMIC_BOOK_LIST, + true, + listOf(COMIC_BOOK_LIST.get(0).path), + onSetSelectionMode = { _ -> }, + onComicBookClicked = { _ -> }, + onDeleteComics = { }) + } } \ No newline at end of file diff --git a/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_off.xml b/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_off.xml new file mode 100644 index 00000000..0d86f6e1 --- /dev/null +++ b/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_off.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_on.xml b/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_on.xml new file mode 100644 index 00000000..04b64462 --- /dev/null +++ b/androidVariant/src/main/res/drawable-anydpi/ic_selection_mode_on.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/androidVariant/src/main/res/drawable-hdpi/ic_selection_mode_off.png b/androidVariant/src/main/res/drawable-hdpi/ic_selection_mode_off.png new file mode 100644 index 0000000000000000000000000000000000000000..6bb3eee86a8f4c6b6b596d2b9af6fd1896c3b5b3 GIT binary patch literal 532 zcmV+v0_**WP)woKaYZBr| zo126dMD7PalJ=haopVo5lUPhiNlEGNN;6G!zgR3jfoD($kHF->fhw6y&Y+O{QmORS z3#_*bg~Bnyhk1Wrg+xo7+z)i?Bb&{Ru}@!=&*xV-VNeXim=GT5+5xD@z`+l3U5)kX zFittkNy30mHG~hYBfKU9#nn=&)cCK0gdqop1;#tJ*Cnq70Uev_6oU`*ych*BPq5R{ z>GT}#s}k;bKWKIe!pUyBEJh;ifTOTo?|yV?Q+!!x;YO!%tP|!Iilqr+RxHH_DL(wKSjtbhp&Ix5g{wt5itLRXb zjiaLL`Xndx6CTD}-uuLxtzs;N=iyf|4HA! WT@JY~*r5La0000wcj4 zXFnpQRVtNcIC-EjO>+!gc;n)6gz+WTVdUU`6@j}L_mWVOU?i{VN^LtHnjXQo&zPH{jI!o->6Lu{Sb(o#0mGlYo_2cZ4e2)c-4xU01HrXQg< zD`tM|PVP?6X#&WRv2a#G>(YRMyU0Y73#w4B*B4kgD}!V6BF6=D4i{7bFXV`Yvl527 z((-ON0dboPs(}1s7S2k!i_)R{$iyiZQ~{e!u)w09mg@hO}%q|7W%$F7Z@s+%agpAgfMa<`8c*MUQ0`C@HOmu0L!vQ z<=Jzfib%~#z6kPK8pb^+`FvOd8!&jNqQps3p-`A`Gyb_EZ<9?71h#FDr|lFT(IV>% z9O&ACs$1x83!LWMUqz)-@lpHFvNDM2`7#ej!x@prk_ClY+5FY_p_ztd&geIUHASLMAdVXn4dmPGTYfp00 z%bfZC&OaSZtMo6W`@TO6g5Ws}!zVn~IdGP2+wK7P5=GG$8wKAS$GPJUI1TY+4SiCpHcv7NKV*N;DZ5WMt;pSmb6i_ zX#ecwO-6oXnr4ePc<&~QFpx4xWk9xBz5rs&Cd0emRD@G{!y%Pj*X?Tu4q6*-#$fm#3n002ovPDHLkV1frn BoB{v< literal 0 HcmV?d00001 diff --git a/androidVariant/src/main/res/drawable-mdpi/ic_selection_mode_on.png b/androidVariant/src/main/res/drawable-mdpi/ic_selection_mode_on.png new file mode 100644 index 0000000000000000000000000000000000000000..6047839d856911e8ddce89ddef089d74f1d6b587 GIT binary patch literal 351 zcmV-l0igbgP)pmfhZj)339hnWA z-bHHU0HZB9LIe;4*;-6aK%R612!^cP@cgWC=}0= z6?dm2We7Q649T->R^ca-0qW}vBvl!3gCp5heD~@f7Kq;g$5LVM5(ZF}ikUXOw`p2DhwH*wo2~!=002ovPDHLkV1j@DlVJb= literal 0 HcmV?d00001 diff --git a/androidVariant/src/main/res/drawable-xhdpi/ic_selection_mode_off.png b/androidVariant/src/main/res/drawable-xhdpi/ic_selection_mode_off.png new file mode 100644 index 0000000000000000000000000000000000000000..2334861c0761cf10ef6a2afbec8cac42bbd9c7c4 GIT binary patch literal 616 zcmV-u0+;=XP)A|eRoWa3{i-u38-U=%fe#E(Rc9t;{WD)MighoxDDB5m0g zNp_Nn(6>7~yS%r%)v^o>3=9km42BO0e(-ORN~Oka+ui|h3x&dKu~=*f@x2D#E+Wa?L5mmgQV7x0XyMC&+F)V7|lx{Zfit(ZA$)5MmOmu>}1;nWTm*FPAcPEa4ak#H{wo!gSxo^$T-O9W!d2ZP}2H? z^&Flx%|mqCBdD8YceO$QDUMBncdwr3e*YO#n8Wk0MFRr^0|Ntt2=N0}B$)3uNml>>0000<+Ip{G=w>2xv? zx}Zohue)BobibZtVlfK~3kwSi3kyMGW8sR9bUg_Y!=nR4O0Ba=H9~&nI}l zl1ilpcwXdy!8sUw^9=t*jK9VB3D$`OFmlWWgA#MsS+8rrRv1yYi7ugW7z@%X$z*b< zP$I*N>k^($eP$ULw1YwKJTVNV2|Yb&QjWJvXl|&U8^~lb z!!Z8BgLMeGglNhta?BJ_#F*;8j|b=y!Uvux$4nAm_F2n4lJ4_>CSPT_E05qX^EhsW9lqF^b)?>?J$kpiuUh8}de$%jIT8vuii9CK4RfC5kxm zL^)=X$aA3<(AY|@f#2*x$n}dRmbkz%Q-t4fUjZIa#CE$4sD3;6e9iH834c4P=Wj6a zhX(VfnsP?}&_L_lXSN$aV~-;7Gn)xZwZ9^PY%Mkwl$g86dXWG|a&CynDj0j8&1R=r zCvw2pg!^@msj3CxeI31b2d>J=$n$y`^0?&hM$@!~;oLGU6dZXj+4J&YHfQAgZy)NzTU|E=H4 z(6RiclSxcfFO&C!Z*r)Ydds}-s;-XbDT<;filQirq9}@@D9Qu`K~QhE+n3@vet^%X z_*edv%MocyGUqUbo!U20I6 z9q54Poai*de5=0i&$F-6f&X@duKC3Y+7b$N18yqpw-lf-S2-aUf}hXqyYL|BJDiA1 zp_ATo421>7InN1XArxuFj04+*BWz$$rX=N45uSV_VX_ zvO=4Fhf$Hr@&s9{(P->X0;<(&J15z1O(XcZXBJ}vvis#PFiq+%cBSWtnffl z#EH0q{U$>5yq3Xb9w>@95f`9A9sDbCEQ=mr#M&S@FZ<&T_tcVW8+EQkICkUC&@za`@xI@tH@!tu_;;6 zpgpF~u|cqRc|w?TuJ9N`bt7K{vhO64a5KnLt3EKteD9<$$=+A%^MNt$*Q4~c{6h%># at;8Sq0q=9CptNcL0000&{jlzP}CON=zr^X zLPw4tlbANSGk4PSgO9Y_lY1U>=gyg#6bgo67=~dOhG7_nVHk#yGg__I4d_J}hO0pk zyu;@*J|8z4jjL>XEI{IQ6ya^V-Tpm_VB3?T)oQg%Jl9l#RdJJ(KddmvPZa7L&z)k|*e;zV4*em&AxS>E3h2ITbG;zV4*zI_oE z&#tVXeI7W_0w>~#6A8Y(kkn>i|A7_soHKER&jsoYyK`RWOk9!R+Y9wiF%;+{55(d` zT)}>Q5el?Dv`Iq?@S2lx0sHku@rBBVMlsc!E^owwX zBp5mqT%L-3*X#9*Jf5Wr{J`u0c~Q5`#;$}_^GZ7peShQ{6FPjGsRcj#Q2&{WkcWr% ze}Q2sjUx<J*J)-Xpe2Q@9At>3IGN0zT_HX!KfT?K92OTnnfC9UX_)q_KLnf}_5A z&Jx)VI1v{@ahBM3UXZKmGAA@iVf%>KY3`8DTRK}#ZX8L^r(b(d14SI|c~11OlGXzA zT;a*53S5e7NOX;pA0*iJ@STMyil+NVoP$XC8SqMQoPP#v7=~dOhG7_nVHk#C9GO3w WukY!Xk?4s40000Go to the previous page. Go to the next page. Stop reading comic book. + Mark selected comics as read. + Mark selected comics as unread. + Delete selected comics from device. diff --git a/iosVariant/iosVariant/Assets.xcassets/Contents.json b/iosVariant/iosVariant/Assets.xcassets/Contents.json index 4aa7c535..73c00596 100644 --- a/iosVariant/iosVariant/Assets.xcassets/Contents.json +++ b/iosVariant/iosVariant/Assets.xcassets/Contents.json @@ -3,4 +3,4 @@ "author" : "xcode", "version" : 1 } -} \ No newline at end of file +} diff --git a/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/Contents.json b/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/Contents.json new file mode 100644 index 00000000..065f5aeb --- /dev/null +++ b/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reshot-icon-off-switch-TQVBLRMESW-1772e.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/reshot-icon-off-switch-TQVBLRMESW-1772e.svg b/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/reshot-icon-off-switch-TQVBLRMESW-1772e.svg new file mode 100644 index 00000000..78a9ca7f --- /dev/null +++ b/iosVariant/iosVariant/Assets.xcassets/selection_mode_off.imageset/reshot-icon-off-switch-TQVBLRMESW-1772e.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/Contents.json b/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/Contents.json new file mode 100644 index 00000000..c95ac2e8 --- /dev/null +++ b/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "reshot-icon-on-switch-S253U8PFKM-f8513.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/reshot-icon-on-switch-S253U8PFKM-f8513.svg b/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/reshot-icon-on-switch-S253U8PFKM-f8513.svg new file mode 100644 index 00000000..640b5431 --- /dev/null +++ b/iosVariant/iosVariant/Assets.xcassets/selection_mode_on.imageset/reshot-icon-on-switch-S253U8PFKM-f8513.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/iosVariant/iosVariant/HomeView.swift b/iosVariant/iosVariant/HomeView.swift index 332737e3..31e84202 100644 --- a/iosVariant/iosVariant/HomeView.swift +++ b/iosVariant/iosVariant/HomeView.swift @@ -28,10 +28,43 @@ struct HomeView: View { var body: some View { TabView(selection: $currentDestination) { - ComicBooksView(onReadComicBook: { comicBook in - variantViewModel.readComicBook(comicBook: comicBook) - currentDestination = .comics - }) + ComicBooksView( + comicBookList: variantViewModel.comicBookList, + selectionMode: variantViewModel.selectionMode, + selectionList: variantViewModel.selectionList, + onSetSelectionMode: { enable in + Log().info( + tag: TAG, + message: "Setting selection mode: \(enable)" + ) + variantViewModel.setSelectMode(enable: enable) + }, + onComicClicked: { comicBook in + if variantViewModel.selectionMode { + Log().info( + tag: TAG, + message: + "Toggling comic book select: \(comicBook.path)" + ) + variantViewModel.updateSelectionList( + filename: comicBook.path + ) + } else { + Log().info( + tag: TAG, + message: "Reading comic book: \(comicBook.filename)" + ) + variantViewModel.readComicBook(comicBook: comicBook) + currentDestination = .comics + } + }, + onDeleteComics: { + Log().info(tag: TAG, message: "Deleting \(variantViewModel.selectionList.count) comic book(s)") + Task { + try await variantViewModel.deleteSelections() + } + } + ) .tag(AppDestination.comics) .tabItem { Label("Comics", systemImage: "book.fill") } @@ -58,9 +91,3 @@ struct HomeView: View { } } } - -struct HomeViewPreviews: PreviewProvider { - static var previews: some View { - HomeView() - } -} diff --git a/iosVariant/iosVariant/Views/Comics/ComicBookListItemView.swift b/iosVariant/iosVariant/Views/Comics/ComicBookListItemView.swift index 08d6c456..893ce4eb 100644 --- a/iosVariant/iosVariant/Views/Comics/ComicBookListItemView.swift +++ b/iosVariant/iosVariant/Views/Comics/ComicBookListItemView.swift @@ -26,21 +26,32 @@ struct ComicBookListItemView: View { @ObservedObject var imageLoader: ImageLoader let comicBook: ComicBook + let selected: Bool - var onComicBookClicked: (ComicBook) -> Void + var onClick: (ComicBook) -> Void init( comicBook: ComicBook, - onComicBookClicked: @escaping (ComicBook) -> Void + selected: Bool, + onClick: @escaping (ComicBook) -> Void ) { self.comicBook = comicBook - self.onComicBookClicked = onComicBookClicked + self.selected = selected + self.onClick = onClick self.imageLoader = ImageLoader(comicBook: comicBook) } + var borderWidth: CGFloat { + if selected { + return 5 + } else { + return 0 + } + } + var body: some View { VStack(alignment: .leading) { - if (imageLoader.image != nil) { + if imageLoader.image != nil { Image(uiImage: imageLoader.image!) .resizable() .scaledToFit() @@ -59,14 +70,25 @@ struct ComicBookListItemView: View { } .onTapGesture { Log().debug(tag: TAG, message: "Comic book item tapped") - onComicBookClicked(self.comicBook) + onClick(self.comicBook) } + .padding() + .border(.red, width: borderWidth) } } #Preview { ComicBookListItemView( comicBook: COMIC_BOOK_LIST[0], - onComicBookClicked: { _ in } + selected: false, + onClick: { _ in } + ) +} + +#Preview("selected") { + ComicBookListItemView( + comicBook: COMIC_BOOK_LIST[0], + selected: true, + onClick: { _ in } ) } diff --git a/iosVariant/iosVariant/Views/Comics/ComicBookListView.swift b/iosVariant/iosVariant/Views/Comics/ComicBookListView.swift index 1fc42461..d4e307f0 100644 --- a/iosVariant/iosVariant/Views/Comics/ComicBookListView.swift +++ b/iosVariant/iosVariant/Views/Comics/ComicBookListView.swift @@ -24,10 +24,11 @@ private let TAG = "ComicBookListView" struct ComicBookListView: View { let comicBookList: [ComicBook] + let selectionList: [String] let columns = [GridItem(.adaptive(minimum: 128))] - var onComicBookClicked: (ComicBook) -> Void + var onClick: (ComicBook) -> Void var body: some View { NavigationStack { @@ -36,8 +37,9 @@ struct ComicBookListView: View { ForEach(comicBookList, id: \.path) { comicBook in ComicBookListItemView( comicBook: comicBook, - onComicBookClicked: { comicBook in - onComicBookClicked(comicBook) + selected: selectionList.contains(comicBook.path), + onClick: { comicBook in + onClick(comicBook) } ) } @@ -52,6 +54,7 @@ struct ComicBookListView: View { #Preview { ComicBookListView( comicBookList: COMIC_BOOK_LIST, - onComicBookClicked: { _ in } + selectionList: [COMIC_BOOK_LIST[0].path], + onClick: { _ in } ) } diff --git a/iosVariant/iosVariant/Views/Comics/ComicBooksView.swift b/iosVariant/iosVariant/Views/Comics/ComicBooksView.swift index 94a7f265..90eb6f92 100644 --- a/iosVariant/iosVariant/Views/Comics/ComicBooksView.swift +++ b/iosVariant/iosVariant/Views/Comics/ComicBooksView.swift @@ -23,25 +23,80 @@ import shared private let TAG = "ComicBooksView" struct ComicBooksView: View { - @EnvironmentViewModel var variantViewModel: VariantViewModel + let comicBookList: [ComicBook] + let selectionMode: Bool + let selectionList: [String] - var onReadComicBook: (ComicBook) -> Void + var onSetSelectionMode: (Bool) -> Void + var onComicClicked: (ComicBook) -> Void + var onDeleteComics: () -> Void var body: some View { - if (variantViewModel.comicBook != nil) { - ReadingView(comicBook: self.variantViewModel.comicBook!, onStopReading: { - Log().debug(tag: TAG, message: "Stop reading comic book") - variantViewModel.readComicBook(comicBook: nil) - }) - } else { + NavigationStack { ComicBookListView( - comicBookList: self.variantViewModel.comicBookList, - onComicBookClicked: { comicBook in onReadComicBook(comicBook) } + comicBookList: comicBookList, + selectionList: selectionList, + onClick: { comicBook in onComicClicked(comicBook) } ) + .toolbar { + ToolbarItemGroup(placement: .bottomBar) { + if selectionMode { + Button { + onSetSelectionMode(false) + } label: { + Image("selection_mode_on") + } + + Button { + onDeleteComics() + } label: { + Image(systemName: "trash.fill") + } + .disabled(selectionList.isEmpty) + } else { + Button { + onSetSelectionMode(true) + } label: { + Image("selection_mode_off") + } + } + + Spacer() + } + } } } } #Preview { - ComicBooksView(onReadComicBook: { _ in }) + ComicBooksView( + comicBookList: COMIC_BOOK_LIST, + selectionMode: false, + selectionList: [], + onSetSelectionMode: { _ in }, + onComicClicked: { _ in }, + onDeleteComics: { } + ) +} + +#Preview("selection mode on") { + ComicBooksView( + comicBookList: COMIC_BOOK_LIST, + selectionMode: true, + selectionList: [COMIC_BOOK_LIST[0].path], + onSetSelectionMode: { _ in }, + onComicClicked: { _ in }, + onDeleteComics: { } + ) +} + +#Preview("selection mode on no selections") { + ComicBooksView( + comicBookList: COMIC_BOOK_LIST, + selectionMode: true, + selectionList: [], + onSetSelectionMode: { _ in }, + onComicClicked: { _ in }, + onDeleteComics: { } + ) } diff --git a/shared/src/commonMain/kotlin/org/comixedproject/variant/viewmodel/VariantViewModel.kt b/shared/src/commonMain/kotlin/org/comixedproject/variant/viewmodel/VariantViewModel.kt index 30c84172..affa7977 100644 --- a/shared/src/commonMain/kotlin/org/comixedproject/variant/viewmodel/VariantViewModel.kt +++ b/shared/src/commonMain/kotlin/org/comixedproject/variant/viewmodel/VariantViewModel.kt @@ -137,6 +137,16 @@ open class VariantViewModel( @NativeCoroutinesState val comicBookList: StateFlow> = _comicBookList.asStateFlow() + private val _selectionMode = MutableStateFlow(viewModelScope, false) + + @NativeCoroutinesState + val selectionMode: StateFlow = _selectionMode.asStateFlow() + + private val _selectionList = MutableStateFlow>(viewModelScope, listOf()) + + @NativeCoroutinesState + val selectionList: StateFlow> = _selectionList.asStateFlow() + fun loadDirectory(path: String, reload: Boolean) { viewModelScope.launch(Dispatchers.Main) { Log.debug(TAG, "Loading directory: ${path}") @@ -255,6 +265,43 @@ open class VariantViewModel( } } + fun setSelectMode(enable: Boolean) { + viewModelScope.launch(Dispatchers.Main) { + Log.debug(TAG, "Setting selection mode: ${enable}") + _selectionMode.emit(enable) + if (!enable) { + Log.debug(TAG, "Clearing selections") + _selectionList.emit(emptyList()) + } + } + } + + fun updateSelectionList(filename: String) { + viewModelScope.launch(Dispatchers.Main) { + val selections = mutableListOf() + if (_selectionList.value.contains(filename)) { + Log.debug(TAG, "Removing selection: ${filename}") + selections.addAll(_selectionList.value.filter { !it.equals(filename) }.toList()) + } else { + Log.debug(TAG, "Adding selection: ${filename}") + selections.addAll(_selectionList.value) + selections.add(filename) + } + _selectionList.emit(selections) + } + } + + suspend fun deleteSelections() { + _selectionList.value.forEach { + val file = File(it) + Log.info(TAG, "Deleting file: ${it}") + file.delete() + } + + _selectionList.emit(emptyList()) + _selectionMode.emit(false) + } + private suspend fun loadLibraryContents() { Log.debug(TAG, "Loading library contents: ${_libraryDirectory}")