[core, android] Initial playtime implementation #2535
24 changed files with 590 additions and 44 deletions
|
@ -206,6 +206,17 @@ object NativeLibrary {
|
||||||
ErrorUnknown
|
ErrorUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* playtime tracking
|
||||||
|
*/
|
||||||
|
external fun playTimeManagerInit()
|
||||||
|
external fun playTimeManagerStart()
|
||||||
|
external fun playTimeManagerStop()
|
||||||
|
external fun playTimeManagerGetPlayTime(programId: String): Long
|
||||||
|
external fun playTimeManagerGetCurrentTitleId(): Long
|
||||||
|
external fun playTimeManagerResetProgramPlayTime(programId: String)
|
||||||
|
external fun playTimeManagerSetPlayTime(programId: String, playTimeSeconds: Long)
|
||||||
|
|
||||||
var coreErrorAlertResult = false
|
var coreErrorAlertResult = false
|
||||||
val coreErrorAlertLock = Object()
|
val coreErrorAlertLock = Object()
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ class YuzuApplication : Application() {
|
||||||
application = this
|
application = this
|
||||||
documentsTree = DocumentsTree()
|
documentsTree = DocumentsTree()
|
||||||
DirectoryInitialization.start()
|
DirectoryInitialization.start()
|
||||||
|
NativeLibrary.playTimeManagerInit()
|
||||||
GpuDriverHelper.initializeDriverParameters()
|
GpuDriverHelper.initializeDriverParameters()
|
||||||
NativeInput.reloadInputDevices()
|
NativeInput.reloadInputDevices()
|
||||||
NativeLibrary.logDeviceInfo()
|
NativeLibrary.logDeviceInfo()
|
||||||
|
|
|
@ -61,6 +61,7 @@ import org.yuzu.yuzu_emu.utils.ThemeHelper
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import org.yuzu.yuzu_emu.utils.ForegroundService
|
import org.yuzu.yuzu_emu.utils.ForegroundService
|
||||||
|
import androidx.core.os.BundleCompat
|
||||||
|
|
||||||
class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
private lateinit var binding: ActivityEmulationBinding
|
private lateinit var binding: ActivityEmulationBinding
|
||||||
|
@ -322,6 +323,11 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
|
|
||||||
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
|
override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
NativeLibrary.playTimeManagerStop()
|
||||||
|
}
|
||||||
|
|
||||||
private fun enableFullscreenImmersive() {
|
private fun enableFullscreenImmersive() {
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
|
@ -526,6 +532,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
|
|
||||||
fun onEmulationStarted() {
|
fun onEmulationStarted() {
|
||||||
emulationViewModel.setEmulationStarted(true)
|
emulationViewModel.setEmulationStarted(true)
|
||||||
|
NativeLibrary.playTimeManagerStart()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEmulationStopped(status: Int) {
|
fun onEmulationStopped(status: Int) {
|
||||||
|
|
|
@ -1635,6 +1635,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
Log.debug("[EmulationFragment] Pausing emulation.")
|
Log.debug("[EmulationFragment] Pausing emulation.")
|
||||||
|
|
||||||
NativeLibrary.pauseEmulation()
|
NativeLibrary.pauseEmulation()
|
||||||
|
NativeLibrary.playTimeManagerStop()
|
||||||
|
|
||||||
state = State.PAUSED
|
state = State.PAUSED
|
||||||
} else {
|
} else {
|
||||||
|
@ -1725,6 +1726,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
State.PAUSED -> {
|
State.PAUSED -> {
|
||||||
Log.debug("[EmulationFragment] Resuming emulation.")
|
Log.debug("[EmulationFragment] Resuming emulation.")
|
||||||
NativeLibrary.unpauseEmulation()
|
NativeLibrary.unpauseEmulation()
|
||||||
|
NativeLibrary.playTimeManagerStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
|
else -> Log.debug("[EmulationFragment] Bug, run called while already running.")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// SPDX-FileCopyrightText: 2025 Eden Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
package org.yuzu.yuzu_emu.fragments
|
package org.yuzu.yuzu_emu.fragments
|
||||||
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
import org.yuzu.yuzu_emu.HomeNavigationDirections
|
||||||
|
import org.yuzu.yuzu_emu.NativeLibrary
|
||||||
import org.yuzu.yuzu_emu.R
|
import org.yuzu.yuzu_emu.R
|
||||||
import org.yuzu.yuzu_emu.YuzuApplication
|
import org.yuzu.yuzu_emu.YuzuApplication
|
||||||
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
|
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
|
||||||
|
@ -104,6 +105,8 @@ class GamePropertiesFragment : Fragment() {
|
||||||
binding.title.text = args.game.title
|
binding.title.text = args.game.title
|
||||||
binding.title.marquee()
|
binding.title.marquee()
|
||||||
|
|
||||||
|
getPlayTime()
|
||||||
|
|
||||||
binding.buttonStart.setOnClickListener {
|
binding.buttonStart.setOnClickListener {
|
||||||
LaunchGameDialogFragment.newInstance(args.game)
|
LaunchGameDialogFragment.newInstance(args.game)
|
||||||
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
|
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
|
||||||
|
@ -128,6 +131,109 @@ class GamePropertiesFragment : Fragment() {
|
||||||
gamesViewModel.reloadGames(true)
|
gamesViewModel.reloadGames(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPlayTime() {
|
||||||
|
binding.playtime.text = buildString {
|
||||||
|
val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId)
|
||||||
|
|
||||||
|
val hours = playTimeSeconds / 3600
|
||||||
|
val minutes = (playTimeSeconds % 3600) / 60
|
||||||
|
val seconds = playTimeSeconds % 60
|
||||||
|
|
||||||
|
val readablePlayTime = when {
|
||||||
|
hours > 0 -> "${hours}h ${minutes}m ${seconds}s"
|
||||||
|
minutes > 0 -> "${minutes}m ${seconds}s"
|
||||||
|
else -> "${seconds}s"
|
||||||
|
}
|
||||||
|
|
||||||
|
append(getString(R.string.playtime))
|
||||||
|
append(readablePlayTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.playtime.setOnClickListener {
|
||||||
|
showEditPlaytimeDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showEditPlaytimeDialog() {
|
||||||
|
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_playtime, null)
|
||||||
|
val hoursLayout =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_hours)
|
||||||
|
val minutesLayout =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_minutes)
|
||||||
|
val secondsLayout =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputLayout>(R.id.layout_seconds)
|
||||||
|
val hoursInput =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_hours)
|
||||||
|
val minutesInput =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_minutes)
|
||||||
|
val secondsInput =
|
||||||
|
dialogView.findViewById<com.google.android.material.textfield.TextInputEditText>(R.id.input_seconds)
|
||||||
|
|
||||||
|
val playTimeSeconds = NativeLibrary.playTimeManagerGetPlayTime(args.game.programId)
|
||||||
|
val hours = playTimeSeconds / 3600
|
||||||
|
val minutes = (playTimeSeconds % 3600) / 60
|
||||||
|
val seconds = playTimeSeconds % 60
|
||||||
|
|
||||||
|
hoursInput.setText(hours.toString())
|
||||||
|
minutesInput.setText(minutes.toString())
|
||||||
|
secondsInput.setText(seconds.toString())
|
||||||
|
|
||||||
|
val dialog = com.google.android.material.dialog.MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.edit_playtime)
|
||||||
|
.setView(dialogView)
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
dialog.setOnShowListener {
|
||||||
|
val positiveButton = dialog.getButton(android.app.AlertDialog.BUTTON_POSITIVE)
|
||||||
|
positiveButton.setOnClickListener {
|
||||||
|
hoursLayout.error = null
|
||||||
|
minutesLayout.error = null
|
||||||
|
secondsLayout.error = null
|
||||||
|
|
||||||
|
val hoursText = hoursInput.text.toString()
|
||||||
|
val minutesText = minutesInput.text.toString()
|
||||||
|
val secondsText = secondsInput.text.toString()
|
||||||
|
|
||||||
|
val hoursValue = hoursText.toLongOrNull() ?: 0
|
||||||
|
val minutesValue = minutesText.toLongOrNull() ?: 0
|
||||||
|
val secondsValue = secondsText.toLongOrNull() ?: 0
|
||||||
|
|
||||||
|
var hasError = false
|
||||||
|
|
||||||
|
// normally cant be above 9999
|
||||||
|
if (hoursValue < 0 || hoursValue > 9999) {
|
||||||
|
hoursLayout.error = getString(R.string.hours_must_be_between_0_and_9999)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutesValue < 0 || minutesValue > 59) {
|
||||||
|
minutesLayout.error = getString(R.string.minutes_must_be_between_0_and_59)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondsValue < 0 || secondsValue > 59) {
|
||||||
|
secondsLayout.error = getString(R.string.seconds_must_be_between_0_and_59)
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasError) {
|
||||||
|
val totalSeconds = hoursValue * 3600 + minutesValue * 60 + secondsValue
|
||||||
|
NativeLibrary.playTimeManagerSetPlayTime(args.game.programId, totalSeconds)
|
||||||
|
getPlayTime()
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
R.string.playtime_updated_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
private fun reloadList() {
|
private fun reloadList() {
|
||||||
_binding ?: return
|
_binding ?: return
|
||||||
|
|
||||||
|
@ -272,6 +378,31 @@ class GamePropertiesFragment : Fragment() {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (NativeLibrary.playTimeManagerGetPlayTime(args.game.programId) > 0) {
|
||||||
|
add(
|
||||||
|
SubmenuProperty(
|
||||||
|
R.string.reset_playtime,
|
||||||
|
R.string.reset_playtime_description,
|
||||||
|
R.drawable.ic_delete
|
||||||
|
) {
|
||||||
|
MessageDialogFragment.newInstance(
|
||||||
|
requireActivity(),
|
||||||
|
titleId = R.string.reset_playtime,
|
||||||
|
descriptionId = R.string.reset_playtime_warning_description,
|
||||||
|
positiveAction = {
|
||||||
|
NativeLibrary.playTimeManagerResetProgramPlayTime( args.game.programId)
|
||||||
|
Toast.makeText(
|
||||||
|
YuzuApplication.appContext,
|
||||||
|
R.string.playtime_reset_successfully,
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
getPlayTime()
|
||||||
|
homeViewModel.reloadPropertiesList(true)
|
||||||
|
}
|
||||||
|
).show(parentFragmentManager, MessageDialogFragment.TAG)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.listProperties.apply {
|
binding.listProperties.apply {
|
||||||
|
@ -284,6 +415,7 @@ class GamePropertiesFragment : Fragment() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
driverViewModel.updateDriverNameForGame(args.game)
|
driverViewModel.updateDriverNameForGame(args.game)
|
||||||
|
getPlayTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInsets() =
|
private fun setInsets() =
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
#include "common/scope_exit.h"
|
#include "common/scope_exit.h"
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
|
#include "frontend_common/play_time_manager.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/cpu_manager.h"
|
#include "core/cpu_manager.h"
|
||||||
#include "core/crypto/key_manager.h"
|
#include "core/crypto/key_manager.h"
|
||||||
|
@ -85,6 +86,9 @@ std::atomic<int> g_battery_percentage = {100};
|
||||||
std::atomic<bool> g_is_charging = {false};
|
std::atomic<bool> g_is_charging = {false};
|
||||||
std::atomic<bool> g_has_battery = {true};
|
std::atomic<bool> g_has_battery = {true};
|
||||||
|
|
||||||
|
// playtime
|
||||||
|
std::unique_ptr<PlayTime::PlayTimeManager> play_time_manager;
|
||||||
|
|
||||||
EmulationSession::EmulationSession() {
|
EmulationSession::EmulationSession() {
|
||||||
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
|
m_vfs = std::make_shared<FileSys::RealVfsFilesystem>();
|
||||||
}
|
}
|
||||||
|
@ -733,6 +737,56 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerInit(JNIEnv* env, jobject obj) {
|
||||||
|
// for some reason the full user directory isnt initialized in Android, so we need to create it
|
||||||
|
const auto play_time_dir = Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir);
|
||||||
|
if (!Common::FS::IsDir(play_time_dir)) {
|
||||||
|
if (!Common::FS::CreateDir(play_time_dir)) {
|
||||||
|
LOG_WARNING(Frontend, "Failed to create play time directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStart(JNIEnv* env, jobject obj) {
|
||||||
|
if (play_time_manager) {
|
||||||
|
play_time_manager->SetProgramId(EmulationSession::GetInstance().System().GetApplicationProcessProgramID());
|
||||||
|
play_time_manager->Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerStop(JNIEnv* env, jobject obj) {
|
||||||
|
play_time_manager->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetPlayTime(JNIEnv* env, jobject obj,
|
||||||
|
jstring jprogramId) {
|
||||||
|
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
return play_time_manager->GetPlayTime(program_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
jlong Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerGetCurrentTitleId(JNIEnv* env,
|
||||||
|
jobject obj) {
|
||||||
|
return EmulationSession::GetInstance().System().GetApplicationProcessProgramID();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerResetProgramPlayTime(JNIEnv* env, jobject obj,
|
||||||
|
jstring jprogramId) {
|
||||||
|
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
if (play_time_manager) {
|
||||||
|
play_time_manager->ResetProgramPlayTime(program_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Java_org_yuzu_yuzu_1emu_NativeLibrary_playTimeManagerSetPlayTime(JNIEnv* env, jobject obj,
|
||||||
|
jstring jprogramId, jlong playTimeSeconds) {
|
||||||
|
u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
|
||||||
|
if (play_time_manager) {
|
||||||
|
play_time_manager->SetPlayTime(program_id, static_cast<u64>(playTimeSeconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz,
|
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getAppletLaunchPath(JNIEnv* env, jclass clazz,
|
||||||
jlong jid) {
|
jlong jid) {
|
||||||
auto bis_system =
|
auto bis_system =
|
||||||
|
|
|
@ -105,6 +105,16 @@
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
tools:text="deko_basic" />
|
tools:text="deko_basic" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/playtime"
|
||||||
|
style="?attr/textAppearanceBodyMedium"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="Game Playtime" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||||
|
|
59
src/android/app/src/main/res/layout/dialog_edit_playtime.xml
Normal file
59
src/android/app/src/main/res/layout/dialog_edit_playtime.xml
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_hours"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:hint="@string/hours"
|
||||||
|
app:boxBackgroundMode="outline">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_hours"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="4" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_minutes"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:hint="@string/minutes"
|
||||||
|
app:boxBackgroundMode="outline">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_minutes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="2" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/layout_seconds"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="@string/seconds"
|
||||||
|
app:boxBackgroundMode="outline">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/input_seconds"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:maxLength="2" />
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -74,12 +74,22 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="2dp"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:requiresFadingEdge="horizontal"
|
android:requiresFadingEdge="horizontal"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
tools:text="deko_basic" />
|
tools:text="deko_basic" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/playtime"
|
||||||
|
style="?attr/textAppearanceBodyMedium"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
tools:text="Game Playtime" />
|
||||||
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/list_properties"
|
android:id="@+id/list_properties"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -757,6 +757,18 @@
|
||||||
<string name="copy_details">Copy details</string>
|
<string name="copy_details">Copy details</string>
|
||||||
<string name="add_ons">Add-ons</string>
|
<string name="add_ons">Add-ons</string>
|
||||||
<string name="add_ons_description">Toggle mods, updates and DLC</string>
|
<string name="add_ons_description">Toggle mods, updates and DLC</string>
|
||||||
|
<string name="playtime">Playtime:</string>
|
||||||
|
<string name="reset_playtime">Clear Playtime</string>
|
||||||
|
<string name="reset_playtime_description">Reset the current game\'s playtime back to 0 seconds</string>
|
||||||
|
<string name="reset_playtime_warning_description">This will clear the current game\'s playtime data. Are you sure?</string>
|
||||||
|
<string name="playtime_reset_successfully">Playtime has been reset</string>
|
||||||
|
<string name="edit_playtime">Edit Playtime</string>
|
||||||
|
<string name="hours">Hours</string>
|
||||||
|
<string name="minutes">Minutes</string>
|
||||||
|
<string name="hours_must_be_between_0_and_9999">Hours must be between 0 and 9999</string>
|
||||||
|
<string name="minutes_must_be_between_0_and_59">Minutes must be between 0 and 59</string>
|
||||||
|
<string name="seconds_must_be_between_0_and_59">Seconds must be between 0 and 59</string>
|
||||||
|
<string name="playtime_updated_successfully">Playtime updated successfully</string>
|
||||||
<string name="clear_shader_cache">Clear shader cache</string>
|
<string name="clear_shader_cache">Clear shader cache</string>
|
||||||
<string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
|
<string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
|
||||||
<string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string>
|
<string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string>
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
|
||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
@ -7,6 +10,8 @@ add_library(frontend_common STATIC
|
||||||
content_manager.h
|
content_manager.h
|
||||||
firmware_manager.h
|
firmware_manager.h
|
||||||
firmware_manager.cpp
|
firmware_manager.cpp
|
||||||
|
play_time_manager.cpp
|
||||||
|
play_time_manager.h
|
||||||
)
|
)
|
||||||
|
|
||||||
create_target_directory_groups(frontend_common)
|
create_target_directory_groups(frontend_common)
|
||||||
|
|
|
@ -11,7 +11,10 @@
|
||||||
#include "common/settings.h"
|
#include "common/settings.h"
|
||||||
#include "common/thread.h"
|
#include "common/thread.h"
|
||||||
#include "core/hle/service/acc/profile_manager.h"
|
#include "core/hle/service/acc/profile_manager.h"
|
||||||
#include "yuzu/play_time_manager.h"
|
#include "play_time_manager.h"
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace PlayTime {
|
namespace PlayTime {
|
||||||
|
|
||||||
|
@ -22,19 +25,13 @@ struct PlayTimeElement {
|
||||||
PlayTime play_time;
|
PlayTime play_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
|
std::optional<std::filesystem::path> GetCurrentUserPlayTimePath() {
|
||||||
const Service::Account::ProfileManager& manager) {
|
|
||||||
const auto uuid = manager.GetUser(static_cast<s32>(Settings::values.current_user));
|
|
||||||
if (!uuid.has_value()) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir) /
|
return Common::FS::GetEdenPath(Common::FS::EdenPath::PlayTimeDir) /
|
||||||
uuid->RawString().append(".bin");
|
"playtime.bin";
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db,
|
[[nodiscard]] bool ReadPlayTimeFile(PlayTimeDatabase& out_play_time_db) {
|
||||||
const Service::Account::ProfileManager& manager) {
|
const auto filename = GetCurrentUserPlayTimePath();
|
||||||
const auto filename = GetCurrentUserPlayTimePath(manager);
|
|
||||||
|
|
||||||
if (!filename.has_value()) {
|
if (!filename.has_value()) {
|
||||||
LOG_ERROR(Frontend, "Failed to get current user path");
|
LOG_ERROR(Frontend, "Failed to get current user path");
|
||||||
|
@ -69,9 +66,8 @@ std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db,
|
[[nodiscard]] bool WritePlayTimeFile(const PlayTimeDatabase& play_time_db) {
|
||||||
const Service::Account::ProfileManager& manager) {
|
const auto filename = GetCurrentUserPlayTimePath();
|
||||||
const auto filename = GetCurrentUserPlayTimePath(manager);
|
|
||||||
|
|
||||||
if (!filename.has_value()) {
|
if (!filename.has_value()) {
|
||||||
LOG_ERROR(Frontend, "Failed to get current user path");
|
LOG_ERROR(Frontend, "Failed to get current user path");
|
||||||
|
@ -100,9 +96,9 @@ std::optional<std::filesystem::path> GetCurrentUserPlayTimePath(
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
PlayTimeManager::PlayTimeManager(Service::Account::ProfileManager& profile_manager)
|
PlayTimeManager::PlayTimeManager()
|
||||||
: manager{profile_manager} {
|
: running_program_id() {
|
||||||
if (!ReadPlayTimeFile(database, manager)) {
|
if (!ReadPlayTimeFile(database)) {
|
||||||
LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
|
LOG_ERROR(Frontend, "Failed to read play time database! Resetting to default.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +143,7 @@ void PlayTimeManager::AutoTimestamp(std::stop_token stop_token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void PlayTimeManager::Save() {
|
void PlayTimeManager::Save() {
|
||||||
if (!WritePlayTimeFile(database, manager)) {
|
if (!WritePlayTimeFile(database)) {
|
||||||
LOG_ERROR(Frontend, "Failed to update play time database!");
|
LOG_ERROR(Frontend, "Failed to update play time database!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,24 +157,47 @@ u64 PlayTimeManager::GetPlayTime(u64 program_id) const {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PlayTimeManager::SetPlayTime(u64 program_id, u64 play_time) {
|
||||||
|
database[program_id] = play_time;
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
|
||||||
void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
|
void PlayTimeManager::ResetProgramPlayTime(u64 program_id) {
|
||||||
database.erase(program_id);
|
database.erase(program_id);
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ReadablePlayTime(qulonglong time_seconds) {
|
std::string PlayTimeManager::GetReadablePlayTime(u64 time_seconds) {
|
||||||
if (time_seconds == 0) {
|
if (time_seconds == 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
const auto time_minutes = (std::max)(static_cast<double>(time_seconds) / 60, 1.0);
|
|
||||||
const auto time_hours = static_cast<double>(time_seconds) / 3600;
|
|
||||||
const bool is_minutes = time_minutes < 60;
|
|
||||||
const char* unit = is_minutes ? "m" : "h";
|
|
||||||
const auto value = is_minutes ? time_minutes : time_hours;
|
|
||||||
|
|
||||||
return QStringLiteral("%L1 %2")
|
const auto time_minutes = std::max(static_cast<double>(time_seconds) / 60.0, 1.0);
|
||||||
.arg(value, 0, 'f', !is_minutes && time_seconds % 60 != 0)
|
const auto time_hours = static_cast<double>(time_seconds) / 3600.0;
|
||||||
.arg(QString::fromUtf8(unit));
|
const bool is_minutes = time_minutes < 60.0;
|
||||||
|
|
||||||
|
if (is_minutes) {
|
||||||
|
return fmt::format("{:.0f} m", time_minutes);
|
||||||
|
} else {
|
||||||
|
const bool has_remainder = time_seconds % 60 != 0;
|
||||||
|
if (has_remainder) {
|
||||||
|
return fmt::format("{:.1f} h", time_hours);
|
||||||
|
} else {
|
||||||
|
return fmt::format("{:.0f} h", time_hours);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PlayTimeManager::GetPlayTimeHours(u64 time_seconds) {
|
||||||
|
return fmt::format("{}", time_seconds / 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PlayTimeManager::GetPlayTimeMinutes(u64 time_seconds) {
|
||||||
|
return fmt::format("{}", (time_seconds % 3600) / 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PlayTimeManager::GetPlayTimeSeconds(u64 time_seconds) {
|
||||||
|
return fmt::format("{}", time_seconds % 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace PlayTime
|
} // namespace PlayTime
|
|
@ -6,8 +6,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QString>
|
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "common/common_funcs.h"
|
#include "common/common_funcs.h"
|
||||||
|
@ -27,7 +25,7 @@ using PlayTimeDatabase = std::map<ProgramId, PlayTime>;
|
||||||
|
|
||||||
class PlayTimeManager {
|
class PlayTimeManager {
|
||||||
public:
|
public:
|
||||||
explicit PlayTimeManager(Service::Account::ProfileManager& profile_manager);
|
explicit PlayTimeManager();
|
||||||
~PlayTimeManager();
|
~PlayTimeManager();
|
||||||
|
|
||||||
YUZU_NON_COPYABLE(PlayTimeManager);
|
YUZU_NON_COPYABLE(PlayTimeManager);
|
||||||
|
@ -36,9 +34,15 @@ public:
|
||||||
u64 GetPlayTime(u64 program_id) const;
|
u64 GetPlayTime(u64 program_id) const;
|
||||||
void ResetProgramPlayTime(u64 program_id);
|
void ResetProgramPlayTime(u64 program_id);
|
||||||
void SetProgramId(u64 program_id);
|
void SetProgramId(u64 program_id);
|
||||||
|
void SetPlayTime(u64 program_id, u64 play_time);
|
||||||
void Start();
|
void Start();
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
|
static std::string GetReadablePlayTime(u64 time_seconds);
|
||||||
|
static std::string GetPlayTimeHours(u64 time_seconds);
|
||||||
|
static std::string GetPlayTimeMinutes(u64 time_seconds);
|
||||||
|
static std::string GetPlayTimeSeconds(u64 time_seconds);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void AutoTimestamp(std::stop_token stop_token);
|
void AutoTimestamp(std::stop_token stop_token);
|
||||||
void Save();
|
void Save();
|
||||||
|
@ -46,9 +50,7 @@ private:
|
||||||
PlayTimeDatabase database;
|
PlayTimeDatabase database;
|
||||||
u64 running_program_id;
|
u64 running_program_id;
|
||||||
std::jthread play_time_thread;
|
std::jthread play_time_thread;
|
||||||
Service::Account::ProfileManager& manager;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
QString ReadablePlayTime(qulonglong time_seconds);
|
|
||||||
|
|
||||||
} // namespace PlayTime
|
} // namespace PlayTime
|
|
@ -27,7 +27,6 @@ add_library(qt_common STATIC
|
||||||
qt_rom_util.h qt_rom_util.cpp
|
qt_rom_util.h qt_rom_util.cpp
|
||||||
qt_applet_util.h qt_applet_util.cpp
|
qt_applet_util.h qt_applet_util.cpp
|
||||||
qt_progress_dialog.h qt_progress_dialog.cpp
|
qt_progress_dialog.h qt_progress_dialog.cpp
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
create_target_directory_groups(qt_common)
|
create_target_directory_groups(qt_common)
|
||||||
|
|
|
@ -198,11 +198,11 @@ add_executable(yuzu
|
||||||
multiplayer/state.cpp
|
multiplayer/state.cpp
|
||||||
multiplayer/state.h
|
multiplayer/state.h
|
||||||
multiplayer/validation.h
|
multiplayer/validation.h
|
||||||
play_time_manager.cpp
|
|
||||||
play_time_manager.h
|
|
||||||
precompiled_headers.h
|
precompiled_headers.h
|
||||||
startup_checks.cpp
|
startup_checks.cpp
|
||||||
startup_checks.h
|
startup_checks.h
|
||||||
|
set_play_time_dialog.cpp
|
||||||
|
set_play_time_dialog.h
|
||||||
util/clickable_label.cpp
|
util/clickable_label.cpp
|
||||||
util/clickable_label.h
|
util/clickable_label.h
|
||||||
util/controller_navigation.cpp
|
util/controller_navigation.cpp
|
||||||
|
|
|
@ -557,13 +557,15 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
|
||||||
QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
|
QAction* remove_update = remove_menu->addAction(tr("Remove Installed Update"));
|
||||||
QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC"));
|
QAction* remove_dlc = remove_menu->addAction(tr("Remove All Installed DLC"));
|
||||||
QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration"));
|
QAction* remove_custom_config = remove_menu->addAction(tr("Remove Custom Configuration"));
|
||||||
QAction* remove_play_time_data = remove_menu->addAction(tr("Remove Play Time Data"));
|
|
||||||
QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage"));
|
QAction* remove_cache_storage = remove_menu->addAction(tr("Remove Cache Storage"));
|
||||||
QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache"));
|
QAction* remove_gl_shader_cache = remove_menu->addAction(tr("Remove OpenGL Pipeline Cache"));
|
||||||
QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache"));
|
QAction* remove_vk_shader_cache = remove_menu->addAction(tr("Remove Vulkan Pipeline Cache"));
|
||||||
remove_menu->addSeparator();
|
remove_menu->addSeparator();
|
||||||
QAction* remove_shader_cache = remove_menu->addAction(tr("Remove All Pipeline Caches"));
|
QAction* remove_shader_cache = remove_menu->addAction(tr("Remove All Pipeline Caches"));
|
||||||
QAction* remove_all_content = remove_menu->addAction(tr("Remove All Installed Contents"));
|
QAction* remove_all_content = remove_menu->addAction(tr("Remove All Installed Contents"));
|
||||||
|
QMenu* play_time_menu = context_menu.addMenu(tr("Manage Play Time"));
|
||||||
|
QAction* set_play_time = play_time_menu->addAction(tr("Edit Play Time Data"));
|
||||||
crueter marked this conversation as resolved
|
|||||||
|
QAction* remove_play_time_data = play_time_menu->addAction(tr("Remove Play Time Data"));
|
||||||
QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS"));
|
QMenu* dump_romfs_menu = context_menu.addMenu(tr("Dump RomFS"));
|
||||||
QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS"));
|
QAction* dump_romfs = dump_romfs_menu->addAction(tr("Dump RomFS"));
|
||||||
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
|
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
|
||||||
|
@ -629,6 +631,8 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
|
||||||
connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
|
connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() {
|
||||||
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
|
emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path);
|
||||||
});
|
});
|
||||||
|
connect(set_play_time, &QAction::triggered,
|
||||||
|
[this, program_id]() { emit SetPlayTimeRequested(program_id); });
|
||||||
connect(remove_play_time_data, &QAction::triggered,
|
connect(remove_play_time_data, &QAction::triggered,
|
||||||
[this, program_id]() { emit RemovePlayTimeRequested(program_id); });
|
[this, program_id]() { emit RemovePlayTimeRequested(program_id); });
|
||||||
connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
|
connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
#include "qt_common/uisettings.h"
|
#include "qt_common/uisettings.h"
|
||||||
#include "qt_common/qt_game_util.h"
|
#include "qt_common/qt_game_util.h"
|
||||||
#include "yuzu/compatibility_list.h"
|
#include "yuzu/compatibility_list.h"
|
||||||
#include "yuzu/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
class System;
|
class System;
|
||||||
|
@ -104,6 +104,7 @@ signals:
|
||||||
void RemoveFileRequested(u64 program_id, QtCommon::Game::GameListRemoveTarget target,
|
void RemoveFileRequested(u64 program_id, QtCommon::Game::GameListRemoveTarget target,
|
||||||
const std::string& game_path);
|
const std::string& game_path);
|
||||||
void RemovePlayTimeRequested(u64 program_id);
|
void RemovePlayTimeRequested(u64 program_id);
|
||||||
|
void SetPlayTimeRequested(u64 program_id);
|
||||||
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
|
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
|
||||||
void VerifyIntegrityRequested(const std::string& game_path);
|
void VerifyIntegrityRequested(const std::string& game_path);
|
||||||
void CopyTIDRequested(u64 program_id);
|
void CopyTIDRequested(u64 program_id);
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/string_util.h"
|
#include "common/string_util.h"
|
||||||
#include "yuzu/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
#include "qt_common/uisettings.h"
|
#include "qt_common/uisettings.h"
|
||||||
#include "yuzu/util/util.h"
|
#include "yuzu/util/util.h"
|
||||||
|
|
||||||
|
@ -241,7 +241,7 @@ public:
|
||||||
|
|
||||||
void setData(const QVariant& value, int role) override {
|
void setData(const QVariant& value, int role) override {
|
||||||
qulonglong time_seconds = value.toULongLong();
|
qulonglong time_seconds = value.toULongLong();
|
||||||
GameListItem::setData(PlayTime::ReadablePlayTime(time_seconds), Qt::DisplayRole);
|
GameListItem::setData(QString::fromStdString(PlayTime::PlayTimeManager::GetReadablePlayTime(time_seconds)), Qt::DisplayRole);
|
||||||
GameListItem::setData(value, PlayTimeRole);
|
GameListItem::setData(value, PlayTimeRole);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
#include "core/file_sys/registered_cache.h"
|
#include "core/file_sys/registered_cache.h"
|
||||||
#include "qt_common/uisettings.h"
|
#include "qt_common/uisettings.h"
|
||||||
#include "yuzu/compatibility_list.h"
|
#include "yuzu/compatibility_list.h"
|
||||||
#include "yuzu/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
|
|
||||||
namespace Core {
|
namespace Core {
|
||||||
class System;
|
class System;
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include "set_play_time_dialog.h"
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
#include <unistd.h> // for chdir
|
#include <unistd.h> // for chdir
|
||||||
#endif
|
#endif
|
||||||
|
@ -163,7 +165,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
|
||||||
#include "yuzu/install_dialog.h"
|
#include "yuzu/install_dialog.h"
|
||||||
#include "yuzu/loading_screen.h"
|
#include "yuzu/loading_screen.h"
|
||||||
#include "yuzu/main.h"
|
#include "yuzu/main.h"
|
||||||
#include "yuzu/play_time_manager.h"
|
#include "frontend_common/play_time_manager.h"
|
||||||
#include "yuzu/startup_checks.h"
|
#include "yuzu/startup_checks.h"
|
||||||
#include "qt_common/uisettings.h"
|
#include "qt_common/uisettings.h"
|
||||||
#include "yuzu/util/clickable_label.h"
|
#include "yuzu/util/clickable_label.h"
|
||||||
|
@ -447,7 +449,7 @@ GMainWindow::GMainWindow(bool has_broken_vulkan)
|
||||||
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
|
SetDiscordEnabled(UISettings::values.enable_discord_presence.GetValue());
|
||||||
discord_rpc->Update();
|
discord_rpc->Update();
|
||||||
|
|
||||||
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>(QtCommon::system->GetProfileManager());
|
play_time_manager = std::make_unique<PlayTime::PlayTimeManager>();
|
||||||
|
|
||||||
Network::Init();
|
Network::Init();
|
||||||
|
|
||||||
|
@ -1574,6 +1576,8 @@ void GMainWindow::ConnectWidgetEvents() {
|
||||||
connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
|
connect(game_list, &GameList::RemoveFileRequested, this, &GMainWindow::OnGameListRemoveFile);
|
||||||
connect(game_list, &GameList::RemovePlayTimeRequested, this,
|
connect(game_list, &GameList::RemovePlayTimeRequested, this,
|
||||||
&GMainWindow::OnGameListRemovePlayTimeData);
|
&GMainWindow::OnGameListRemovePlayTimeData);
|
||||||
|
connect(game_list, &GameList::SetPlayTimeRequested, this,
|
||||||
|
&GMainWindow::OnGameListSetPlayTime);
|
||||||
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
|
connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS);
|
||||||
connect(game_list, &GameList::VerifyIntegrityRequested, this,
|
connect(game_list, &GameList::VerifyIntegrityRequested, this,
|
||||||
&GMainWindow::OnGameListVerifyIntegrity);
|
&GMainWindow::OnGameListVerifyIntegrity);
|
||||||
|
@ -2634,6 +2638,19 @@ void GMainWindow::OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GMainWindow::OnGameListSetPlayTime(u64 program_id) {
|
||||||
|
const u64 current_play_time = play_time_manager->GetPlayTime(program_id);
|
||||||
crueter marked this conversation as resolved
crueter
commented
We can probably make this a .ui file We can probably make this a .ui file
|
|||||||
|
|
||||||
|
SetPlayTimeDialog dialog(this, current_play_time);
|
||||||
|
|
||||||
|
if (dialog.exec() == QDialog::Accepted) {
|
||||||
|
const u64 total_seconds = dialog.GetTotalSeconds();
|
||||||
|
play_time_manager->SetPlayTime(program_id, total_seconds);
|
||||||
crueter marked this conversation as resolved
crueter
commented
These can be spin boxes These can be spin boxes
|
|||||||
|
game_list->PopulateAsync(UISettings::values.game_dirs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
crueter marked this conversation as resolved
crueter
commented
And if we do go with QSpinBox you can just set the range and this won't be necessary And if we do go with QSpinBox you can just set the range and this won't be necessary
|
|||||||
|
|
||||||
void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
|
void GMainWindow::OnGameListRemovePlayTimeData(u64 program_id) {
|
||||||
if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
|
if (QMessageBox::question(this, tr("Remove Play Time Data"), tr("Reset play time?"),
|
||||||
QMessageBox::Yes | QMessageBox::No,
|
QMessageBox::Yes | QMessageBox::No,
|
||||||
|
|
|
@ -345,6 +345,7 @@ private slots:
|
||||||
void OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListRemoveTarget target,
|
void OnGameListRemoveFile(u64 program_id, QtCommon::Game::GameListRemoveTarget target,
|
||||||
const std::string& game_path);
|
const std::string& game_path);
|
||||||
void OnGameListRemovePlayTimeData(u64 program_id);
|
void OnGameListRemovePlayTimeData(u64 program_id);
|
||||||
|
void OnGameListSetPlayTime(u64 program_id);
|
||||||
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
|
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
|
||||||
void OnGameListVerifyIntegrity(const std::string& game_path);
|
void OnGameListVerifyIntegrity(const std::string& game_path);
|
||||||
void OnGameListCopyTID(u64 program_id);
|
void OnGameListCopyTID(u64 program_id);
|
||||||
|
|
49
src/yuzu/set_play_time_dialog.cpp
Normal file
49
src/yuzu/set_play_time_dialog.cpp
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "yuzu/set_play_time_dialog.h"
|
||||||
|
#include "frontend_common/play_time_manager.h"
|
||||||
|
#include "ui_set_play_time_dialog.h"
|
||||||
|
|
||||||
|
SetPlayTimeDialog::SetPlayTimeDialog(QWidget* parent, u64 current_play_time)
|
||||||
|
: QDialog(parent), ui{std::make_unique<Ui::SetPlayTimeDialog>()} {
|
||||||
|
ui->setupUi(this);
|
||||||
|
|
||||||
|
ui->hoursSpinBox->setValue(
|
||||||
|
QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeHours(current_play_time)).toInt());
|
||||||
|
ui->minutesSpinBox->setValue(
|
||||||
|
QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeMinutes(current_play_time)).toInt());
|
||||||
|
ui->secondsSpinBox->setValue(
|
||||||
|
QString::fromStdString(PlayTime::PlayTimeManager::GetPlayTimeSeconds(current_play_time)).toInt());
|
||||||
|
|
||||||
|
connect(ui->hoursSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||||
|
&SetPlayTimeDialog::OnValueChanged);
|
||||||
|
connect(ui->minutesSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||||
|
&SetPlayTimeDialog::OnValueChanged);
|
||||||
|
connect(ui->secondsSpinBox, QOverload<int>::of(&QSpinBox::valueChanged), this,
|
||||||
|
&SetPlayTimeDialog::OnValueChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetPlayTimeDialog::~SetPlayTimeDialog() = default;
|
||||||
|
|
||||||
|
u64 SetPlayTimeDialog::GetTotalSeconds() const {
|
||||||
|
const u64 hours = static_cast<u64>(ui->hoursSpinBox->value());
|
||||||
|
const u64 minutes = static_cast<u64>(ui->minutesSpinBox->value());
|
||||||
|
const u64 seconds = static_cast<u64>(ui->secondsSpinBox->value());
|
||||||
|
|
||||||
|
return hours * 3600 + minutes * 60 + seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetPlayTimeDialog::OnValueChanged() {
|
||||||
|
if (ui->errorLabel->isVisible()) {
|
||||||
|
ui->errorLabel->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const u64 total_seconds = GetTotalSeconds();
|
||||||
|
constexpr u64 max_reasonable_time = 9999ULL * 3600;
|
||||||
|
|
||||||
|
if (total_seconds > max_reasonable_time) {
|
||||||
|
ui->errorLabel->setText(tr("Total play time reached maximum."));
|
||||||
|
ui->errorLabel->setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
27
src/yuzu/set_play_time_dialog.h
Normal file
27
src/yuzu/set_play_time_dialog.h
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <memory>
|
||||||
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class SetPlayTimeDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SetPlayTimeDialog : public QDialog {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SetPlayTimeDialog(QWidget* parent, u64 current_play_time);
|
||||||
|
~SetPlayTimeDialog() override;
|
||||||
|
|
||||||
|
u64 GetTotalSeconds() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnValueChanged();
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::SetPlayTimeDialog> ui;
|
||||||
|
};
|
123
src/yuzu/set_play_time_dialog.ui
Normal file
123
src/yuzu/set_play_time_dialog.ui
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SetPlayTimeDialog</class>
|
||||||
|
<widget class="QDialog" name="SetPlayTimeDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>400</width>
|
||||||
|
<height>150</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Set Play Time Data</string>
|
||||||
|
</property>
|
||||||
|
<property name="modal">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="inputLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelHours">
|
||||||
|
<property name="text">
|
||||||
|
<string>Hours:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="hoursSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>9999</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelMinutes">
|
||||||
|
<property name="text">
|
||||||
|
<string>Minutes:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="minutesSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>59</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="labelSeconds">
|
||||||
|
<property name="text">
|
||||||
|
<string>Seconds:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSpinBox" name="secondsSpinBox">
|
||||||
|
<property name="maximum">
|
||||||
|
<number>59</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="errorLabel">
|
||||||
|
<property name="styleSheet">
|
||||||
|
<string notr="true">QLabel { color : red; }</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="visible">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>SetPlayTimeDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>129</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>74</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>SetPlayTimeDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>129</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>199</x>
|
||||||
|
<y>74</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
Loading…
Add table
Add a link
Reference in a new issue
"Edit" would work better here