Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ fun GeneralSettingsScreen(
var gridAdaptiveHeight by remember { mutableStateOf(AppPrefs.isGridAdaptiveHeight(context)) }
// v1.0.56 T3: 双指捏放行高(实验室, 默认关)
var gridPinchZoom by remember { mutableStateOf(AppPrefs.isGridPinchZoom(context)) }
var autoNextWeek by remember { mutableStateOf(AppPrefs.isAutoNextWeek(context)) }
// v1.0.56 T4: 语言折叠卡展开态 — 默认收起; 选择语言即 recreate 重建, 会话态足够
var languageExpanded by remember { mutableStateOf(false) }
var eveningStart by remember { mutableStateOf(AppPrefs.getGridEveningStart(context)) }
Expand Down Expand Up @@ -806,6 +807,17 @@ fun GeneralSettingsScreen(
)
}
}
HorizontalDivider(color = colors.outlineVariant.copy(alpha = SleepyTheme.Alpha.hairline))
SettingToggleRow(
label = stringResource(R.string.settings_auto_next_week),
subtitle = stringResource(R.string.settings_auto_next_week_sub),
checked = autoNextWeek,
onCheckedChange = {
autoNextWeek = it
AppPrefs.setAutoNextWeek(context, it)
refreshWidgets()
}
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ import com.lingion.sleepy.util.AppPrefs
import com.lingion.sleepy.util.DateUtils
import com.lingion.sleepy.util.HolidayManager
import com.lingion.sleepy.util.TimeTableUtils
import com.lingion.sleepy.util.WeekDisplayContext
import com.lingion.sleepy.util.WeekDisplayResolver
import com.lingion.sleepy.util.WeekDisplayStatus

// 非 private: MainActivity(AppRoot 会话层)需以本类型注入 viewMode —
// 会话内切视图/编辑课程 overlay 往返/切 tab 往返都不丢(启动默认仍由 AppRoot 初始化时读 AppPrefs)。
Expand Down Expand Up @@ -156,6 +159,7 @@ fun ScheduleScreen(
currentWeek = state.selectedWeek,
maxWeek = state.currentTable?.maxWeek ?: 20,
startDate = state.currentTable?.startDate ?: "",
displayContext = state.weekDisplayContext,
onSwitchTable = { showTableSwitcher = true },
onUndo = {
undoScope.launch {
Expand Down Expand Up @@ -413,6 +417,7 @@ private fun TopBar(
currentWeek: Int,
maxWeek: Int,
startDate: String,
displayContext: WeekDisplayContext?,
onSwitchTable: () -> Unit,
onUndo: () -> Unit,
scaleUncommitted: Boolean,
Expand All @@ -427,12 +432,16 @@ private fun TopBar(
) {
val colors = SleepyTheme.colors
// 实时计算当前实际周(不依赖 state.currentWeek — 用户可能切到了别的周)
val actualWeek = remember(startDate) {
val actualWeek = displayContext?.actualWeek ?: remember(startDate) {
if (startDate.isBlank()) 1 else DateUtils.currentWeek(startDate)
}
var menuOpen by remember { mutableStateOf(false) }
val isOnActual = currentWeek == actualWeek
val semesterStatus = DateUtils.semesterStatus(startDate, maxWeek)
val semesterStatus = displayContext?.semesterStatus
?: DateUtils.semesterStatus(startDate, maxWeek)
val displayStatus = displayContext?.let {
WeekDisplayResolver.statusForSelectedWeek(it, currentWeek)
} ?: WeekDisplayStatus.NORMAL

Row(
modifier = Modifier
Expand Down Expand Up @@ -493,9 +502,14 @@ private fun TopBar(
else -> 0
}
Text(
text = if (statusRes == 0)
stringResource(R.string.schedule_current_week, currentWeek)
else "${stringResource(statusRes)} · ${stringResource(R.string.schedule_week_prefix, currentWeek)}",
text = when (displayStatus) {
WeekDisplayStatus.NEXT_WEEK -> stringResource(R.string.schedule_next_week)
WeekDisplayStatus.WEEKEND_CURRENT -> stringResource(R.string.schedule_weekend)
WeekDisplayStatus.CURRENT_ENDED -> stringResource(R.string.schedule_week_ended)
WeekDisplayStatus.NORMAL -> if (statusRes == 0)
stringResource(R.string.schedule_current_week, currentWeek)
else "${stringResource(statusRes)} · ${stringResource(R.string.schedule_week_prefix, currentWeek)}"
},
style = MaterialTheme.typography.labelLarge.copy(fontWeight = FontWeight.SemiBold),
color = if (isOnActual) colors.onPrimaryContainer else colors.primary,
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,34 @@ import com.lingion.sleepy.SleepyApp
import com.lingion.sleepy.data.entity.CourseEntity
import com.lingion.sleepy.data.entity.TimeTableEntity
import com.lingion.sleepy.data.repository.ScheduleRepository
import com.lingion.sleepy.util.AppPrefs
import com.lingion.sleepy.util.DateUtils
import com.lingion.sleepy.util.WeekDisplayContext
import com.lingion.sleepy.util.WeekDisplayResolver
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.LocalDateTime

data class ScheduleState(
val tables: List<TimeTableEntity> = emptyList(),
val selectedTableId: Long? = null,
val courses: List<CourseEntity> = emptyList(),
val currentWeek: Int = 1,
val selectedWeek: Int = 1,
val weekDisplayContext: WeekDisplayContext? = null,
/** true = 用户手动选周;false = 跟随自动展示周。 */
val weekSelectionManual: Boolean = false,
/** false=首次加载(本周), true=用户/系统已选定周 — 课程变更时 selectedWeek 不再被重置 */
val initialWeekSettled: Boolean = false,
val nodesPerDay: Int = 12,
Expand Down Expand Up @@ -77,6 +87,17 @@ class ScheduleViewModel : ViewModel() {

init {
loadTables()
viewModelScope.launch {
AppPrefs.changeBus
.filter { it == AppPrefs.KEY_AUTO_NEXT_WEEK }
.collect { recalculateWeekDisplay() }
}
viewModelScope.launch {
while (isActive) {
delay(60_000)
recalculateWeekDisplay()
}
}
}

private fun loadTables() {
Expand All @@ -89,7 +110,14 @@ class ScheduleViewModel : ViewModel() {
if (tables.isEmpty()) {
// 没有课表就老实空着,不强行造占位表。
// selectedTableId = null,UI 走空态。
_state.update { it.copy(tables = emptyList(), selectedTableId = null) }
_state.update {
it.copy(
tables = emptyList(),
selectedTableId = null,
weekDisplayContext = null,
weekSelectionManual = false
)
}
return@collect
}
val selectedId = _state.value.selectedTableId
Expand Down Expand Up @@ -119,14 +147,29 @@ class ScheduleViewModel : ViewModel() {
val rawTable = st.tables.find { it.id == tableId }
// 水合: 绑定存在时 nodesPerDay/timeJson/smartConfigJson 以时间节次表为准
val table = rawTable?.hydratedWith(periodTable)
val week = table?.let { DateUtils.currentWeek(it.startDate) } ?: 1
val displayContext = table?.let {
WeekDisplayResolver.resolve(
startDate = it.startDate,
maxWeek = it.maxWeek,
now = LocalDateTime.now(),
courses = courses,
timeJson = it.timeJson,
enabled = AppPrefs.isAutoNextWeek(SleepyApp.get())
)
}
val week = displayContext?.actualWeek ?: 1
// v7.10.16s: 只更新真实周(currentWeek, 供"回到本周"), 不再重置 selectedWeek —
// 用户在第 x 周编辑/删课, 保存回来仍停在 x 周(此前被拽回真实周=跳回第一周体验)。
// 首次加载(initial=true)仍落真实周, 保持原行为
st.copy(
courses = courses,
currentWeek = week,
selectedWeek = if (st.initialWeekSettled) st.selectedWeek else week,
selectedWeek = when {
!st.initialWeekSettled -> displayContext?.displayWeek ?: week
!st.weekSelectionManual -> displayContext?.displayWeek ?: week
else -> st.selectedWeek
},
weekDisplayContext = displayContext,
initialWeekSettled = true,
nodesPerDay = table?.nodesPerDay ?: 12,
effectivePeriodTable = periodTable
Expand All @@ -145,7 +188,14 @@ class ScheduleViewModel : ViewModel() {
fun selectTable(id: Long) {
manualSelectDone = true
// 切表 = 新学期语境, 周选择回到该表真实周(initialWeekSettled 复位, loadCourses 重新落周)
_state.update { it.copy(selectedTableId = id, initialWeekSettled = false) }
_state.update {
it.copy(
selectedTableId = id,
initialWeekSettled = false,
weekSelectionManual = false,
weekDisplayContext = null
)
}
loadCourses(id)
// 切表后同步数据库 isDefault,使小组件严格跟随 App 当前选中表(widget 按默认表解析)
viewModelScope.launch {
Expand Down Expand Up @@ -366,7 +416,31 @@ class ScheduleViewModel : ViewModel() {
// 防呆: 下限 1, 上限 maxWeek — 之前只有下限, 右箭头可以无限翻出学期范围外
val maxWeek = _state.value.currentTable?.maxWeek ?: 20
if (week < 1 || week > maxWeek) return
_state.update { it.copy(selectedWeek = week) }
_state.update { it.copy(selectedWeek = week, weekSelectionManual = true) }
}

private fun recalculateWeekDisplay() {
val current = _state.value
val table = current.effectiveCurrentTable ?: return
val context = WeekDisplayResolver.resolve(
startDate = table.startDate,
maxWeek = table.maxWeek,
now = LocalDateTime.now(),
courses = current.courses,
timeJson = table.timeJson,
enabled = AppPrefs.isAutoNextWeek(SleepyApp.get())
)
_state.update {
val selected = if (it.weekSelectionManual) it.selectedWeek else context.displayWeek
if (it.currentWeek == context.actualWeek &&
it.weekDisplayContext == context &&
it.selectedWeek == selected
) it else it.copy(
currentWeek = context.actualWeek,
weekDisplayContext = context,
selectedWeek = selected
)
}
}

/**
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/java/com/lingion/sleepy/util/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ object AppPrefs {
const val KEY_GRID_ROW_SCALE = "grid_row_scale" // float default 1.0 — 双指行高缩放确认值(相对基座; 顶栏 tick 落盘, 撤回回退)
const val KEY_GRID_PINCH_ZOOM = "grid_pinch_zoom" // bool default false — 实验室: 网格视图双指捏放行高(v1.0.56 默认关, 关=手势不挂; 存量缩放值不清)
const val DEFAULT_GRID_PINCH_ZOOM = false
const val KEY_AUTO_NEXT_WEEK = "auto_next_week" // bool default false — 实验室: 当周无剩余课程时自动显示下周
const val DEFAULT_AUTO_NEXT_WEEK = false
const val KEY_WEEK_SCALE = "week_scale" // float 0.7~1.3 default 1.0 — 周视图整体缩放(与网格视图互相独立, issue#8)
const val KEY_GRID_CORNER_RATIO = "grid_corner_ratio" // float 0.0~2.0 default 1.0 — 网格/周视图圆角比例系数(乘基准 12/16dp, issue#8)
const val KEY_WEEK_TWO_COLUMN = "week_two_column" // bool default false — 周视图两栏开关, issue#8
Expand Down Expand Up @@ -534,6 +536,14 @@ object AppPrefs {
_changeBus.tryEmit(KEY_GRID_PINCH_ZOOM)
}

fun isAutoNextWeek(ctx: Context): Boolean =
sp(ctx).getBoolean(KEY_AUTO_NEXT_WEEK, DEFAULT_AUTO_NEXT_WEEK)

fun setAutoNextWeek(ctx: Context, v: Boolean) {
sp(ctx).edit().putBoolean(KEY_AUTO_NEXT_WEEK, v).apply()
_changeBus.tryEmit(KEY_AUTO_NEXT_WEEK)
}

fun getGridEveningStart(ctx: Context): String =
sp(ctx).getString(KEY_GRID_EVENING_START, "18:00") ?: "18:00"

Expand Down
121 changes: 121 additions & 0 deletions app/src/main/java/com/lingion/sleepy/util/WeekDisplayResolver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.lingion.sleepy.util

import com.lingion.sleepy.data.entity.CourseEntity
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime

/**
* 展示周次的单一事实来源。
*
* 这不是课程数据的重排器:课程仍以课表里保存的 day/startWeek/endWeek 为准。
* 它只决定在“当周没有剩余课程”时,默认展示当前周还是下一周。
*/
enum class WeekDisplayStatus {
NORMAL,
NEXT_WEEK,
WEEKEND_CURRENT,
CURRENT_ENDED
}

data class WeekDisplayContext(
val actualWeek: Int,
val displayWeek: Int,
val actualWeekEnded: Boolean,
val status: WeekDisplayStatus,
val semesterStatus: DateUtils.SemesterStatus,
val today: LocalDate,
val enabled: Boolean
)

object WeekDisplayResolver {
fun resolve(
startDate: String,
maxWeek: Int,
now: LocalDateTime,
courses: List<CourseEntity>,
timeJson: String,
enabled: Boolean
): WeekDisplayContext {
val safeMaxWeek = maxWeek.coerceAtLeast(1)
val today = now.toLocalDate()
val semesterStatus = DateUtils.semesterStatus(startDate, safeMaxWeek, today)
val actualWeek = DateUtils.currentWeek(startDate, today).coerceIn(1, safeMaxWeek)
val ended = semesterStatus == DateUtils.SemesterStatus.IN_RANGE &&
!hasRemainingCourse(actualWeek, today, now.toLocalTime(), courses, timeJson)
val canAdvance = enabled &&
semesterStatus == DateUtils.SemesterStatus.IN_RANGE &&
ended && actualWeek < safeMaxWeek
val displayWeek = if (canAdvance) actualWeek + 1 else actualWeek
val status = when {
canAdvance -> WeekDisplayStatus.NEXT_WEEK
enabled && ended && today.dayOfWeek.value >= 6 -> WeekDisplayStatus.WEEKEND_CURRENT
enabled && ended -> WeekDisplayStatus.CURRENT_ENDED
else -> WeekDisplayStatus.NORMAL
}
return WeekDisplayContext(
actualWeek = actualWeek,
displayWeek = displayWeek,
actualWeekEnded = ended,
status = status,
semesterStatus = semesterStatus,
today = today,
enabled = enabled
)
}

/**
* 给主界面/可手动导航的小组件计算当前选中周的标题状态。
* 只有自动切换产生的下一周和已结束的真实周显示特殊标题,其他手动周次保持普通周次标题。
*/
fun statusForSelectedWeek(
context: WeekDisplayContext,
selectedWeek: Int
): WeekDisplayStatus = when {
!context.enabled -> WeekDisplayStatus.NORMAL
context.status == WeekDisplayStatus.NEXT_WEEK && selectedWeek == context.displayWeek ->
WeekDisplayStatus.NEXT_WEEK
selectedWeek == context.actualWeek && context.actualWeekEnded &&
context.today.dayOfWeek.value >= 6 -> WeekDisplayStatus.WEEKEND_CURRENT
selectedWeek == context.actualWeek && context.actualWeekEnded ->
WeekDisplayStatus.CURRENT_ENDED
else -> WeekDisplayStatus.NORMAL
}

/** 下周模式下,今日/两日小组件从下一周周一开始,而不是把周末和下周拼在一起。 */
fun dateOfDisplayWeek(startDate: String, displayWeek: Int, dayOfWeek: Int): LocalDate =
DateUtils.dateOfWeek(startDate, displayWeek, dayOfWeek)

private fun hasRemainingCourse(
actualWeek: Int,
today: LocalDate,
now: LocalTime,
courses: List<CourseEntity>,
timeJson: String
): Boolean {
val todayDow = today.dayOfWeek.value
return courses.asSequence()
.filter { it.inWeek(actualWeek) }
.any { course ->
when {
course.day > todayDow -> true
course.day < todayDow -> false
else -> courseEndMinutes(course, timeJson)?.let { it > now.hour * 60 + now.minute }
?: true
}
}
}

private fun courseEndMinutes(course: CourseEntity, timeJson: String): Int? {
val effective = TimeTableUtils.effectiveCourseTime(
course.isIrregularTime || course.ownTime,
course.startTime,
course.endTime,
course.startNode,
course.step,
timeJson
) ?: return null
val end = runCatching { LocalTime.parse(effective.second) }.getOrNull() ?: return null
return end.hour * 60 + end.minute
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ object TodayDateNavStore {
return LocalDate.ofEpochDay(epoch)
}

/** true 表示用户曾经手动翻过日期;自动周末切换不得覆盖这类导航态。 */
fun hasNavigation(context: Context, widgetId: Int): Boolean =
read(context, widgetId) != null

/** 把日期推进 ±deltaDays (锚点刷新为 today); 钳制在 Core.MAX_ABS_OFFSET_DAYS 内。 */
fun shift(context: Context, widgetId: Int, today: LocalDate, deltaDays: Long) {
synchronized(writeLock) {
Expand Down
Loading