Move timer into notifications bar
This commit is contained in:
parent
47af169ca6
commit
6581b32afe
78
Alarm.tsx
78
Alarm.tsx
|
@ -1,78 +0,0 @@
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
import React, {useEffect, useState} from 'react';
|
|
||||||
import {NativeModules, StyleSheet, Text, View} from 'react-native';
|
|
||||||
import {Button, Modal, Portal} from 'react-native-paper';
|
|
||||||
|
|
||||||
export default function Alarm() {
|
|
||||||
const [show, setShow] = useState(false);
|
|
||||||
const [seconds, setSeconds] = useState(0);
|
|
||||||
const [minutes, setMinutes] = useState(0);
|
|
||||||
|
|
||||||
let intervalId: number;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!show) return;
|
|
||||||
(async () => {
|
|
||||||
const next = await AsyncStorage.getItem('nextAlarm');
|
|
||||||
if (!next) return;
|
|
||||||
const milliseconds = new Date(next).getTime() - new Date().getTime();
|
|
||||||
if (milliseconds <= 0) return;
|
|
||||||
let secondsLeft = milliseconds / 1000;
|
|
||||||
setSeconds(Math.floor(secondsLeft % 60));
|
|
||||||
setMinutes(Math.floor(secondsLeft / 60));
|
|
||||||
intervalId = setInterval(() => {
|
|
||||||
secondsLeft--;
|
|
||||||
if (secondsLeft <= 0) return clearInterval(intervalId);
|
|
||||||
setSeconds(Math.floor(secondsLeft % 60));
|
|
||||||
setMinutes(Math.floor(secondsLeft / 60));
|
|
||||||
}, 1000);
|
|
||||||
})();
|
|
||||||
return () => clearInterval(intervalId);
|
|
||||||
}, [show]);
|
|
||||||
|
|
||||||
const stop = async () => {
|
|
||||||
NativeModules.AlarmModule.stop();
|
|
||||||
clearInterval(intervalId);
|
|
||||||
setSeconds(0);
|
|
||||||
setMinutes(0);
|
|
||||||
await AsyncStorage.setItem('nextAlarm', '');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Portal>
|
|
||||||
<Modal
|
|
||||||
visible={show}
|
|
||||||
style={styles.center}
|
|
||||||
onDismiss={() => setShow(false)}>
|
|
||||||
<Text style={[styles.center, styles.title]}>Resting</Text>
|
|
||||||
<Text style={styles.center}>
|
|
||||||
{minutes}:{seconds}
|
|
||||||
</Text>
|
|
||||||
<View style={{flexDirection: 'row'}}>
|
|
||||||
<Button icon="close" onPress={() => setShow(false)}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button mode="contained" icon="stop" onPress={stop}>
|
|
||||||
Stop
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
|
||||||
</Portal>
|
|
||||||
<Button icon="time" onPress={() => setShow(true)}>
|
|
||||||
Time left
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
center: {
|
|
||||||
alignItems: 'center',
|
|
||||||
alignSelf: 'center',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 18,
|
|
||||||
},
|
|
||||||
});
|
|
7
Home.tsx
7
Home.tsx
|
@ -7,8 +7,7 @@ import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import {AnimatedFAB, Button, Searchbar} from 'react-native-paper';
|
import {AnimatedFAB, Searchbar} from 'react-native-paper';
|
||||||
import Alarm from './Alarm';
|
|
||||||
import {getDb} from './db';
|
import {getDb} from './db';
|
||||||
import EditSet from './EditSet';
|
import EditSet from './EditSet';
|
||||||
|
|
||||||
|
@ -56,10 +55,7 @@ export default function Home() {
|
||||||
const minutes = await AsyncStorage.getItem('minutes');
|
const minutes = await AsyncStorage.getItem('minutes');
|
||||||
const seconds = await AsyncStorage.getItem('seconds');
|
const seconds = await AsyncStorage.getItem('seconds');
|
||||||
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
const milliseconds = Number(minutes) * 60 * 1000 + Number(seconds) * 1000;
|
||||||
const when = new Date();
|
|
||||||
when.setTime(when.getTime() + milliseconds);
|
|
||||||
NativeModules.AlarmModule.timer(milliseconds);
|
NativeModules.AlarmModule.timer(milliseconds);
|
||||||
await AsyncStorage.setItem('nextAlarm', when.toISOString());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const next = async () => {
|
const next = async () => {
|
||||||
|
@ -87,7 +83,6 @@ export default function Home() {
|
||||||
onScrollEndDrag={next}
|
onScrollEndDrag={next}
|
||||||
/>
|
/>
|
||||||
<View style={styles.bottom}>
|
<View style={styles.bottom}>
|
||||||
<Alarm />
|
|
||||||
<EditSet
|
<EditSet
|
||||||
id={id}
|
id={id}
|
||||||
show={showEdit}
|
show={showEdit}
|
||||||
|
|
|
@ -56,8 +56,11 @@ export default function Settings({
|
||||||
value={alarmEnabled}
|
value={alarmEnabled}
|
||||||
onValueChange={setAlarmEnabled}
|
onValueChange={setAlarmEnabled}
|
||||||
/>
|
/>
|
||||||
<Button style={{alignSelf: 'flex-start'}} icon="trash" onPress={clear}>
|
<Button
|
||||||
Clear sets
|
style={{alignSelf: 'flex-start', marginTop: 'auto'}}
|
||||||
|
icon="trash"
|
||||||
|
onPress={clear}>
|
||||||
|
Delete all data
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -66,6 +69,7 @@ export default function Settings({
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
padding: 10,
|
padding: 10,
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
|
|
|
@ -29,7 +29,8 @@
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:exported="true" android:process=":remote" android:name=".AlarmActivity" />
|
<activity android:exported="true" android:process=":remote" android:name=".AlarmActivity" />
|
||||||
<receiver android:exported="true" android:process=":remote" android:name=".MyBroadcastReceiver" />
|
<service android:name=".StopTimer" android:exported="true" android:process=":remote" />
|
||||||
<service android:name=".AlarmService" android:exported="true" />
|
<service android:name=".AlarmService" android:exported="true" />
|
||||||
|
<service android:name=".TimerService" android:exported="true" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
package com.massive
|
package com.massive
|
||||||
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import com.facebook.react.bridge.ReactMethod
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import com.massive.MyBroadcastReceiver
|
|
||||||
import android.app.AlarmManager.AlarmClockInfo
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||||
|
import com.facebook.react.bridge.ReactMethod
|
||||||
|
|
||||||
// replace com.your-app-name with your app’s name
|
|
||||||
class AlarmModule internal constructor(context: ReactApplicationContext?) :
|
class AlarmModule internal constructor(context: ReactApplicationContext?) :
|
||||||
ReactContextBaseJavaModule(context) {
|
ReactContextBaseJavaModule(context) {
|
||||||
private var pendingIntent: PendingIntent? = null
|
|
||||||
private var alarmManager: AlarmManager? = null
|
|
||||||
override fun getName(): String {
|
override fun getName(): String {
|
||||||
return "AlarmModule"
|
return "AlarmModule"
|
||||||
}
|
}
|
||||||
|
@ -26,21 +19,8 @@ class AlarmModule internal constructor(context: ReactApplicationContext?) :
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
fun timer(milliseconds: Int) {
|
fun timer(milliseconds: Int) {
|
||||||
Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
|
Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
|
||||||
val intent = Intent(reactApplicationContext, MyBroadcastReceiver::class.java)
|
val intent = Intent(reactApplicationContext, TimerService::class.java)
|
||||||
pendingIntent = PendingIntent.getBroadcast(
|
intent.putExtra("milliseconds", milliseconds)
|
||||||
reactApplicationContext, 69, intent, PendingIntent.FLAG_IMMUTABLE
|
reactApplicationContext.startService(intent)
|
||||||
)
|
|
||||||
alarmManager =
|
|
||||||
reactApplicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
||||||
val info = AlarmClockInfo(System.currentTimeMillis() + milliseconds, pendingIntent)
|
|
||||||
alarmManager!!.setAlarmClock(info, pendingIntent)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@ReactMethod
|
|
||||||
fun stop() {
|
|
||||||
Log.d("AlarmModule", "Request to stop timer.")
|
|
||||||
alarmManager?.cancel(pendingIntent)
|
|
||||||
reactApplicationContext.stopService(Intent(reactApplicationContext, AlarmService::class.java))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,51 +0,0 @@
|
||||||
package com.massive
|
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import android.content.Intent
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import com.massive.MyBroadcastReceiver
|
|
||||||
import com.massive.AlarmService
|
|
||||||
import com.massive.AlarmActivity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.app.NotificationManagerCompat
|
|
||||||
|
|
||||||
class MyBroadcastReceiver : BroadcastReceiver() {
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.M)
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
Log.d("MyBroadcastReceiver", "Received intent for BroadcastReceiver.")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val importance = NotificationManager.IMPORTANCE_HIGH
|
|
||||||
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance)
|
|
||||||
channel.description = "Alarms for rest timings."
|
|
||||||
val notificationManager = context.getSystemService(
|
|
||||||
NotificationManager::class.java
|
|
||||||
)
|
|
||||||
notificationManager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
context.startService(Intent(context, AlarmService::class.java))
|
|
||||||
val contentIntent = Intent(context.applicationContext, AlarmActivity::class.java)
|
|
||||||
val pendingContent =
|
|
||||||
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_baseline_timer_24)
|
|
||||||
.setContentTitle("Rest")
|
|
||||||
.setContentText("Break times over!")
|
|
||||||
.setContentIntent(pendingContent)
|
|
||||||
.setAutoCancel(true)
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
notificationManager.notify(ALARM_ID, builder.build())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val CHANNEL_ID = "MassiveAlarm"
|
|
||||||
private const val ALARM_ID = 1
|
|
||||||
}
|
|
||||||
}
|
|
16
android/app/src/main/java/com/massive/StopTimer.kt
Normal file
16
android/app/src/main/java/com/massive/StopTimer.kt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package com.massive
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
|
||||||
|
class StopTimer : Service() {
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
applicationContext.stopService(Intent(applicationContext, TimerService::class.java))
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
92
android/app/src/main/java/com/massive/TimerService.kt
Normal file
92
android/app/src/main/java/com/massive/TimerService.kt
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package com.massive
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.CountDownTimer
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.NotificationManagerCompat
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
class TimerService : Service() {
|
||||||
|
private lateinit var notificationManager: NotificationManagerCompat
|
||||||
|
private lateinit var countdownTimer: CountDownTimer
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
Log.d("TimerService", "Started timer service.")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val importance = NotificationManager.IMPORTANCE_LOW
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_ID, importance)
|
||||||
|
channel.description = "Alarms for rest timings."
|
||||||
|
val notificationManager = applicationContext.getSystemService(
|
||||||
|
NotificationManager::class.java
|
||||||
|
)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentIntent = Intent(applicationContext, MainActivity::class.java)
|
||||||
|
val pendingContent =
|
||||||
|
PendingIntent.getActivity(applicationContext, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val actionIntent = Intent(applicationContext, StopTimer::class.java)
|
||||||
|
val pendingAction =
|
||||||
|
PendingIntent.getService(applicationContext, 0, actionIntent, PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24)
|
||||||
|
.setContentTitle("Resting")
|
||||||
|
.setContentIntent(pendingContent)
|
||||||
|
.addAction(R.drawable.ic_baseline_stop_24, "STOP", pendingAction)
|
||||||
|
|
||||||
|
val endMs = intent!!.extras!!.getInt("milliseconds")
|
||||||
|
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
|
countdownTimer = object : CountDownTimer(endMs.toLong(), 1000) {
|
||||||
|
override fun onTick(currentMs: Long) {
|
||||||
|
val seconds = floor((currentMs / 1000).toDouble() % 60)
|
||||||
|
.toInt().toString().padStart(2, '0')
|
||||||
|
val minutes = floor((currentMs / 1000).toDouble() / 60)
|
||||||
|
.toInt().toString().padStart(2, '0')
|
||||||
|
builder.setContentText("$minutes:$seconds")
|
||||||
|
.setAutoCancel(false)
|
||||||
|
.setDefaults(0)
|
||||||
|
.setProgress(endMs, currentMs.toInt(), false)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||||
|
.priority = NotificationCompat.PRIORITY_LOW
|
||||||
|
notificationManager.notify(ALARM_ID, builder.build())
|
||||||
|
}
|
||||||
|
override fun onFinish() {
|
||||||
|
builder.setContentText("Timer finished.")
|
||||||
|
.clearActions()
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setOngoing(false)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||||
|
.priority = NotificationCompat.PRIORITY_HIGH
|
||||||
|
notificationManager.notify(ALARM_ID, builder.build())
|
||||||
|
applicationContext.startService(Intent(applicationContext, AlarmService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countdownTimer.start()
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(p0: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
countdownTimer.cancel()
|
||||||
|
notificationManager.cancel(ALARM_ID)
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANNEL_ID = "MassiveAlarm"
|
||||||
|
private const val ALARM_ID = 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M18,22l-0.01,-6L14,12l3.99,-4.01L18,2H6v6l4,4l-4,3.99V22H18zM8,7.5V4h8v3.5l-4,4L8,7.5z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#000000"
|
||||||
|
android:viewportHeight="24" android:viewportWidth="24"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
|
||||||
|
</vector>
|
|
@ -1,3 +1,3 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">massive</string>
|
<string name="app_name">Massive</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -12,7 +12,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-env": "^7.1.6",
|
"@babel/preset-env": "^7.1.6",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.7",
|
"@react-native-async-storage/async-storage": "^1.17.7",
|
||||||
"@react-navigation/bottom-tabs": "^6.3.1",
|
|
||||||
"@react-navigation/material-top-tabs": "^6.2.1",
|
"@react-navigation/material-top-tabs": "^6.2.1",
|
||||||
"@react-navigation/native": "^6.0.10",
|
"@react-navigation/native": "^6.0.10",
|
||||||
"@react-navigation/native-stack": "^6.6.2",
|
"@react-navigation/native-stack": "^6.6.2",
|
||||||
|
|
|
@ -1618,15 +1618,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa"
|
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa"
|
||||||
integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==
|
integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==
|
||||||
|
|
||||||
"@react-navigation/bottom-tabs@^6.3.1":
|
|
||||||
version "6.3.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-6.3.1.tgz#1552ccdb789b6c9fc05af0877f8f3a50ab28870c"
|
|
||||||
integrity sha512-sL9F4WMhhR6I9bE7bpsPVHnK1cN9doaFHAuy5YmD+Sw6OyO0TAmNgQFx4xZWqboA5ZwSkN0tWcRCr6wGXaRRag==
|
|
||||||
dependencies:
|
|
||||||
"@react-navigation/elements" "^1.3.3"
|
|
||||||
color "^3.1.3"
|
|
||||||
warn-once "^0.1.0"
|
|
||||||
|
|
||||||
"@react-navigation/core@^6.2.1":
|
"@react-navigation/core@^6.2.1":
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.2.1.tgz#90459f9afd25b71a9471b0706ebea2cdd2534fc4"
|
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.2.1.tgz#90459f9afd25b71a9471b0706ebea2cdd2534fc4"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user