1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/MingYueChunQiu-RecorderManager

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
README.md 33 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 28.11.2024 15:18 478d984

RecorderManager

Потому что в проекте часто требуется использовать аудио- и видеозапись, мы написали общую библиотеку RecorderManager. Добро пожаловать в её использование!

Версия 0.4.0-beta.5:

  • обновление зависимостей;
  • удаление EasyPermissions и устаревших методов, использование нового API registerForActivityResult (требуется Java 1.8 или выше);
  • рефакторинг фреймворка, оптимизация кода;
  • частичная настройка вызовов библиотеки, подробности см. в документации ниже;
  • приветствуем отзывы и предложения по улучшению функционала.

Версия 0.3.2:

  • удаление app_name из strings.xml;
  • обновление Kotlin.

Обновление версии 0.3.1: подробности см. в документации.

  • добавление настройки минимальной длительности записи RecordVideoOption.setMinDuration (минимальная длительность записи в секундах, минимум 1 секунда, автоматически корректируется, чтобы не превышать максимальную длительность записи).
  • оптимизация кода.

Обновление версии 0.3-beta.2:

  • реорганизация проекта, переписаны некоторые функции на Kotlin;
  • удаление библиотеки rxjava, уменьшение зависимостей;
  • обновление последних SDK;
  • добавлена функция вспышки, добавлен текст подсказки перед отсчётом времени;
  • добавлены поддержка интернационализации, английский и китайский языки;
  • исправлены известные проблемы, оптимизирован код;
  • для внешних пользователей изменения в API минимальны, в основном это внутренние настройки, см. документацию ниже, приветствуем тестирование и обратную связь для улучшения функционала.

Обновление версии 0.2.29:

  • добавлена настройка круглой кнопки прогресса;
  • добавлена возможность указать переднюю и заднюю камеры;
  • оптимизирован код, скорректированы настройки запуска видеозахвата.

Обновление версии 0.2.28:

  • оптимизированы способы получения результатов видеосъёмки;
  • оптимизирован код.

Обновление версии 0.2.27:

  • в интерфейсе видеосъёмки RecordVideoRequestOption добавлены конфигурации RecorderOption и hideFlipCameraButton;
  • оптимизирован код.

Обновление версии 0.2.26:

  • проект перенесён на AndroidX, внедрён Kotlin.

Обновление версии 0.2.25:

  • улучшено автоматическое разрешение на запись видео, можно автоматически запускать интерфейс видеосъёмки;
  • стандартизировано именование ресурсов изображений.

Эффект демонстрации

Эмуляция интерфейса видеосъёмки WeChat: вставьте изображение здесь

Интерфейс аудиозаписи проще, поэтому изображение не приводится.

Ссылка

  1. Добавьте в свой корневой build.gradle в конце репозиториев:
allprojects {
        repositories {
            ...
            maven { url 'https://jitpack.io' }
        }
    }
  1. Добавьте зависимость:
dependencies {
            implementation 'com.github.MingYueChunQiu:RecorderManager:0.3.2'
    }

Использование

1. Аудиозапись

Используйте конфигурацию по умолчанию для записи:

mRecorderManager.recordAudio(mFilePath);

Записывайте с пользовательскими параметрами конфигурации:

mRecorderManager.recordAudio(new RecorderOption.Builder()
                    .setAudioSource(MediaRecorder.AudioSource.MIC)
                    .setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
                    .setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
                    .setAudioSamplingRate(44100)
                    .setBitRate(96000)
                    .setFilePath(path)
                    .build());

2. Видеозапись

(1). Можно напрямую использовать RecordVideoActivity, реализуя интерфейс съёмки в стиле WeChat.

С версии 0.2.18 используется следующий метод:

RecorderManagerFactory.getRecordVideoRequest().startRecordVideo(MainActivity.this, 0);

Начиная с версии 0.4.0-бета, используется метод registerForActivityResult, поэтому передаётся результат обратного вызова:

      RecorderManagerProvider.getRecordVideoRequester().startRecordVideo(MainActivity.this, new RMRecordVideoResultCallback() {
            @Override
            public void onResponseRecordVideoResult(@NonNull RecordVideoResultInfo info) {
                Log.e("MainActivity", "onActivityResult: " + info.getDuration() + " " + info.getFilePath());
                Toast.makeText(MainActivity.this, info.getDuration() + " " + info.getFilePath(), Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onFailure(@NonNull RecorderManagerException e) {
                Log.e("MainActivity", "onActivityResult: " + e.getErrorCode() + " " + e.getMessage());
            }
        });

С версии 0.4.0-бета RecorderManagerFactory переименован в RecorderManagerProvider. В RecorderManagerProvider можно получить IRecordVideoPageRequester, который находится в интерфейсе IRecordVideoPageRequester:

/**
     * 以默认配置打开 интерфейс видеозаписи
     *
     * @param activity    Activity
     * @param requestCode 请求码
     */
    void startRecordVideo(@NonNull FragmentActivity activity, int requestCode);

    /**
     * 以默认配置 открыть интерфейс видеозаписи
     *
     * @param fragment    Fragment
     * @param requestCode 请求码
     */
    void startRecordVideo(@NonNull Fragment fragment, int requestCode);

    /**
     * Открыть интерфейс видеозаписи
     *
     * @param activity    Activity
     * @param requestCode 请求码
     * @param option      Видеозапись запроса конфигурации информации класса
     */
    void startRecordVideo(@NonNull FragmentActivity activity, int requestCode, @Nullable RecordVideoRequestOption option);

    /**
     * Открыть интерфейс видеозаписи
     *
     * @param fragment    Fragment
     * @param requestCode 请求码
     * @param option      Видеозапись запроса конфигурации информации класса
     */
    void startRecordVideo(@NonNull Fragment fragment, int requestCode, @Nullable RecordVideoRequestOption
``` **Вариант)**

Начиная с версии 0.4.0-beta:

public interface IRecordVideoPageRequester extends IRMRequester {

    /**
     * Открыть интерфейс записи видео по умолчанию
     *
     * @param activity Activity
     * @param callback Результат обратного вызова при записи видео
     */
    void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

    /**
     * Открыть интерфейс записи видео по умолчанию
     *
    * @param fragment Fragment
    * @param callback Результат обратного вызова при записи видео
    */
    void startRecordVideo(@NonNull Fragment fragment, @NonNull RMRecordVideoResultCallback callback);

    /**
    * Открыть интерфейс записи видео
    *
    * @param activity Activity
    * @param option  Информация о конфигурации запроса на запись видео
    * @param callback Результат обратного вызова при записи видео
    */
    void startRecordVideo(@NonNull FragmentActivity activity, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);

    /**
    * Открыть интерфейс записи видео
    *
    * @param fragment Fragment
    * @param option  Информация о конфигурации запроса на запись видео
    * @param callback Результат обратного вызова при записи видео
    */
    void startRecordVideo(@NonNull Fragment fragment, @Nullable RecordVideoRequestOption option, @NonNull RMRecordVideoResultCallback callback);
}

RecordVideoRequestOption позволяет настроить максимальную продолжительность (в секундах) и путь сохранения файла.

public class RecordVideoRequestOption implements Parcelable {

        private String filePath;//путь сохранения файла
        private int maxDuration;//максимальная продолжительность записи (в секундах)
        private RecordVideoOption recordVideoOption;//информация о конфигурации записи видео (значения filePath и maxDuration из этого класса будут перекрывать значения из внешнего класса)
}

В RecordVideoActivity уже настроены параметры по умолчанию, которые можно использовать напрямую. Затем в onActivityResult можно получить возвращаемое значение пути к видео. Возвращаемое значение имеет тип RecordVideoResultInfo.

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == Activity.RESULT_OK && requestCode == 0) {
        RecordVideoResultInfo info = data.getParcelableExtra(EXTRA_RECORD_VIDEO_RESULT_INFO);

//С версии 0.2.28 можно использовать следующий способ, который более безопасен и гибок, а также обладает лучшей совместимостью
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResult(data);
//с версии 0.3
RecordVideoResultInfo info = RecorderManagerFactory.getRecordVideoResultParser().parseRecordVideoResult(data);

if (info != null) {
            Log.e("MainActivity", "onActivityResult: " + " "
                    + info.getDuration() + " " + info.getFilePath());
        }
    }
}

С версии 0.4.0-beta.1: Поскольку используется Android API registerForActivityResult, методы startActivityForResult и подобные были отменены, и соответствующие обратные вызовы будут передаваться напрямую через RMRecordVideoResultCallback.

interface RMRecordVideoResultCallback {

    fun onResponseRecordVideoResult(info: RecordVideoResultInfo)

    fun onFailure(e: RecorderManagerException)
}

Через соответствующие методы IRecordVideoPageRequester, вызывайте их одновременно с передачей обратного результата.
void startRecordVideo(@NonNull FragmentActivity activity, @NonNull RMRecordVideoResultCallback callback);

(2).Если вы хотите настроить стиль некоторых элементов интерфейса, вы можете наследовать от RecordVideoActivity. В этом классе есть несколько защищенных методов, которые позволяют получить доступ к некоторым элементам интерфейса.

/**
 * Получить элемент управления таймером
 *
 * @return возвращает AppCompatTextView
 */
protected AppCompatTextView getTimingView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getTimingView();
}

/**
* Получить элемент управления круглым индикатором прогресса
*
* @return возвращает CircleProgressButton
*/
protected CircleProgressButton getCircleProgressButton() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getCircleProgressButton();
}

/**
* Получить элемент управления для переключения камеры
*
* @return возвращает AppCompatImageView
*/
public AppCompatImageView getFlipCameraView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getFlipCameraView();
}

/**
* Получить элемент управления воспроизведением
*
* @return возвращает AppCompatImageView
*/
protected AppCompatImageView getPlayView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getPlayView();
}

/**
* Получить элемент управления отменой
*
* @return возвращает AppCompatImageView
*/
protected AppCompatImageView getCancelView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getCancelView();
}

/**
* Получить элемент подтверждения
*
* @return возвращает AppCompatImageView
*/
protected AppCompatImageView getConfirmView() {
    return mRecordVideoFg

``` **null ? null : mRecordVideoFg.getConfirmView();**

**/**
* Получение возвращаемого компонента
*
* @return возвращает AppCompatImageView
*/
protected AppCompatImageView getBackView() {
    return mRecordVideoFg == null ? null : mRecordVideoFg.getBackView();
}

Если вы хотите заменить ресурсы значков, предоставьте следующие имена изображений:

rm_record_video_flip_camera.png
rm_record_video_cancel.png
rm_record_video_confirm.png
rm_record_video_play.png
rm_record_video_pull_down.png
rm_record_video_flashlight_turn_off.png
rm_record_video_flashlight_turn_on.png

#### (3). Одновременно предоставляется соответствующий RecordVideoFragment, реализующий ту же функцию, что и RecordVideoActivity. Фактически RecordVideoActivity — это оболочка для RecordVideoFragment.
1. Создание RecordVideoFragment

/**
 * Получение экземпляра фрагмента записи видео (с использованием конфигурации по умолчанию)
 *
 * @param filePath путь к файлу хранения
 * @return возвращает RecordVideoFragment
 */
public static RecordVideoFragment newInstance(@Nullable String filePath) {
    return newInstance(filePath, 30);
}

/**
 * Получение экземпляра фрагмента записи видео (с использованием конфигурации по умолчанию)
 *
 * @param filePath    путь к файлу хранения
 * @param maxDuration максимальная продолжительность (в секундах)
 * @return возвращает RecordVideoFragment
 */
public static RecordVideoFragment newInstance(@Nullable String filePath, int maxDuration) {
    return newInstance(new RecordVideoOption.Builder()
            .setRecorderOption(new RecorderOption.Builder().buildDefaultVideoBean(filePath))
            .setMaxDuration(maxDuration)
            .build());
}

/**
 * Получение экземпляра фрагмента записи видео
 *
 * @param option объект информации о конфигурации записи
 * @return возвращает RecordVideoFragment
 */
public static RecordVideoFragment newInstance(@Nullable RecordVideoOption option) {
    RecordVideoFragment fragment = new RecordVideoFragment();
    Bundle args = new Bundle();
    args.putParcelable(BUNDLE_EXTRA_RECORD_VIDEO_OPTION, option == null ? new RecordVideoOption() : option);
    fragment.setArguments(args);
    return fragment;
}
2. Затем добавьте RecordVideoFragment в нужное место.
3. Можно установить OnRecordVideoListener и получить обратные вызовы для каждого события.

public class RecordVideoOption:

    private RecorderOption recorderOption;//информация о конфигурации записи
        private RecordVideoButtonOption recordVideoButtonOption;//класс конфигурации кнопки записи видео
    private int minDuration;//минимальная продолжительность записи (в секундах, минимум 1, будет автоматически отрегулирована, чтобы не превышать максимальную продолжительность), может использоваться вместе с timingHint
        private int maxDuration;//максимальная продолжительность
        private RecorderManagerConstants.CameraType cameraType;//тип камеры
        private boolean hideFlipCameraButton;//скрыть кнопку возврата для переключения камеры
        private boolean hideFlashlightButton;//скрыть кнопку фонарика
        private String timingHint;//подсказка над кнопкой записи (по умолчанию: 0:%s), будет отображаться перед отсчётом времени
        private String errorToastMsg;//сообщение Toast при возникновении ошибки записи (по умолчанию: запись менее 1 секунды, пожалуйста, повторите попытку)

OnRecordVideoListener теперь заменён на RMOnRecordVideoListener, и он был удалён из RecordVideoOption, в основном используется пользователями для реализации этого интерфейса в их собственной activity или фрагменте для переноса RecordVideoFragment и получения обратных вызовов для соответствующих шагов.

interface RMOnRecordVideoListener {

/**
 * Когда запись завершена, вызывается обратный вызов
 *
 * @param filePath      путь к видеофайлу
 * @param videoDuration продолжительность видео (в миллисекундах)
 */
fun onCompleteRecordVideo(filePath: String?, videoDuration: Int)

/**
 * Вызывается обратный вызов, когда пользователь нажимает кнопку подтверждения результата записи
 *
 * @param filePath      путь к видеофайлу
 * @param videoDuration продолжительность видео (в миллисекундах)
 */
fun onClickConfirm(filePath: String?, videoDuration: Int)

/**
 * Вызывается обратный вызов, когда пользователь нажимает кнопку отмены
 *
 * @param filePath      путь к видеофайлу
 * @param videoDuration продолжительность видео (в миллисекундах)
 */
fun onClickCancel(filePath: String?, videoDuration: Int)

/**
 * Вызывается обратный вызов, когда пользователь нажимает кнопку «Назад»
 */
fun onClickBack()

} 4. RecordVideoButtonOption — класс конфигурации круглой кнопки прогресса.

    private @ColorInt
        int idleCircleColor;//цвет внутреннего круга в состоянии простоя
        private @ColorInt
        int pressedCircleColor;//цвет внутреннего круга при нажатии
        private @ColorInt
        int releasedCircleColor;//цвет внутреннего круга после отпускания
        private @ColorInt
        int idleRingColor;//цвет внешнего кольца в состоянии простоя
        private @ColorInt
        int pressedRingColor;//цвет внешнего кольца при нажатии
        private @ColorInt
        int releasedRingColor;//цвет внешнего кольца после отпускания
        private int idleRingWidth;//ширина внешнего кольца в состоянии простоя
        private int pressedRingWidth;//ширина внешнего кольца при нажатии
        private int releasedRingWidth;//ширина внешнего кольца после отпускания
        private int idleInnerPadding;//внутренний отступ между внешним кольцом и внутренним кругом в состоянии простоя
        private int pressedInnerPadding;//внутренний отступ между внешним кольцом и внутренним кругом при нажатии **releasedInnerPadding** — освобождает состояние внешнего кольца и внутреннего круга, задавая между ними поля.

**idleRingVisible** — определяет видимость внешнего кольца в состоянии простоя.

**pressedRingVisible** — указывает на видимость внешнего кольца при нажатии.

**releasedRingVisible** — показывает видимость внешнего кольца после освобождения.

5. **RecorderOption** — это класс для настройки конкретных параметров записи.

    * **audioSource** — источник аудиосигнала.
    * **videoSource** — источник видеосигнала.
    * **outputFormat** — формат вывода.
    * **audioEncoder** — формат кодирования аудио.
    * **videoEncoder** — формат кодирования видео.
    * **audioSamplingRate** — частота дискретизации аудио (обычно 44100).
    * **bitRate** — битрейт видеокодирования.
    * **frameRate** — кадровая частота видео.
    * **videoWidth** и **videoHeight** — ширина и высота видео.
    * **maxDuration** — максимальная продолжительность.
    * **maxFileSize** — максимальный размер файла.
    * **filePath** — путь к файлу.
    * **orientationHint** — направление угла видеосъёмки.

#### 4. Если вы хотите создать собственный интерфейс, можно использовать класс RecorderManagerable напрямую:

1. Получите экземпляр IRecorderManager через RecorderManagerFactory или RecorderManagerProvider. С версии 0.4.0-beta RecorderManagerFactory переименован в RecorderManagerProvider:

    ```
public final class RecorderManagerProvider {

    private RecorderManagerProvider() {
    }

    /**
     * Создаёт экземпляр класса управления записью (используя стандартный класс записи).
     *
     * @return Возвращает экземпляр класса управления записью.
     */
    @NonNull
    public static IRecorderManager newInstance() {
        return newInstance(new RecorderHelper());
    }

    /**
     * Создаёт экземпляр класса управления записью (используя стандартный класс записи).
     *
     * @param intercept Перехватчик класса управления записью.
     * @return Возвращает экземпляр класса управления записью.
     */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderManagerInterceptor intercept) {
        return newInstance(new RecorderHelper(), intercept);
    }

    /**
     * Создаёт экземпляр класса управления записью.
     *
     * @param helper Фактический класс записи.
     * @return Возвращает экземпляр класса управления записью.
     */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderHelper helper) {
        return newInstance(helper, null);
    }

    /**
     * Создаёт экземпляр класса управления записью.
     *
     * @param helper    Фактический класс записи.
     * @param intercept Перехватчик класса управления записью.
     * @return Возвращает экземпляр класса управления записью.
     */
    @NonNull
    public static IRecorderManager newInstance(@NonNull IRecorderHelper helper, @Nullable IRecorderManagerInterceptor intercept) {
        return new RecorderManager(helper, intercept);
    }

    @NonNull
    public static IRecordVideoPageRequester getRecordVideoRequester() {
        return new RecordVideoPageRequester();
    }

    @NonNull
    public static IRecordVideoResultParser getRecordVideoResultParser() {
        return new RecordVideoResultParser();
    }
}

Они возвращают экземпляры интерфейса IRecorderManager. RecorderManager является стандартным классом реализации, который содержит фактический класс записи RecorderHelper.

public interface IRecorderManager extends IRecorderHelper {

    /**
     * Устанавливает объект записи.
     *
     * @param helper Экземпляр класса записи.
     */
    void setRecorderHelper(@NonNull IRecorderHelper helper);

    /**
     * Получает объект записи.
     *
     * @return Экземпляр класса записи.
     */
    @NonNull
    IRecorderHelper getRecorderHelper();

    /**
     * Инициализирует объект камеры.
     *
     * @param holder Держатель поверхности.
     * @return Инициализированный объект камеры.
     */
    @Nullable
    Camera initCamera(@NonNull SurfaceHolder holder);

    /**
     * Инициализирует объект камеры.
     *
     * @param cameraType Указанный тип камеры.
     * @param holder    Держатель поверхности.
     * @return Инициализированный объект камеры.
     */
    @Nullable
    Camera initCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

    /**
     * Включает или выключает вспышку.
     *
     * @param turnOn true означает включение, false — выключение.
     */
    boolean switchFlashlight(boolean turnOn);

    /**
     * Переворачивает камеру.
     *
     * @param holder Держатель поверхности.
     * @return Перевёрнутый и инициализированный объект камеры.
     */
    @Nullable
    Camera flipCamera(@NonNull SurfaceHolder holder);

    /**
     * Переводит камеру в указанный тип.
     *
     * @param cameraType Тип камеры.
     * @param holder     Держатель поверхности.
     * @return Перевёрнутый и инициализированный объект камеры.
     */
    @Nullable
    Camera flipCamera(@NonNull RecorderManagerConstants.CameraType cameraType, @NonNull SurfaceHolder holder);

    /**
     * Получает текущий тип камеры.
     *
     * @return Тип камеры.
     */
    @NonNull
    RecorderManagerConstants.CameraType getCameraType();

    /**
     * Освобождает ресурсы камеры.
     */
    void releaseCamera();
}

RecorderManagerIntercept реализует интерфейс IRecorderManagerInterceptor. Пользователи могут напрямую наследовать от RecorderManagerIntercept. Все методы в этом классе имеют пустые реализации и могут быть переписаны по мере необходимости. Публичный интерфейс IRecorderManagerInterceptor расширяет ICameraInterceptor

IRecorderHelper — это интерфейс, который реализуется в подклассах для выполнения операций записи. По умолчанию предоставляется RecorderHelper, который реализует IRecoderHelper.

Интерфейс IRecorderHelper

  • Метод recordAudio (путь)
    • Записывает аудио.
    • Путь — путь к файлу, в котором будет храниться запись.
    • Возвращает true, если запись началась успешно, иначе — false.
  • Метод recordAudio (опция)
    • Записывает аудио.
    • Опция — объект, хранящий информацию о записи.
    • Возвращает true, если запись началась успешно, иначе — false.
  • Метод recordVideo (камера, поверхность, путь)
    • Записывает видео.
    • Камера — камера.
    • Поверхность — вид поверхности.
    • Путь — путь к файлу, в котором будет храниться видеозапись.
    • Возвращает true, если видеозапись началась успешно, иначе — false.
  • Метод recordVideo (камера, поверхность, опция)
    • Записывает видео.
    • Камера — камера.
    • Поверхность — вид поверхности.
    • Опция — объект, хранящий информацию о видеозаписи.
    • Возвращает true, если видеозапись началась успешно, иначе — false.
  • Метод release
    • Освобождает ресурсы.
  • Метод getMediaRecorder
    • Получает экземпляр объекта MediaRecorder.
    • Возвращаемый тип — @NonNull.
  • Метод getRecorderOption
    • Получает объект с информацией о конфигурации.
    • Возвращаемый тип — @Nullable.
  1. После создания объекта камеры:
        if (mCamera == null) {
            mCamera = mManager.initCamera(mCameraType, svVideoRef.get().getHolder());
            mCameraType = mManager.getCameraType();
        }
  1. Запись:
isRecording = mManager.recordVideo(mCamera, svVideoRef.get().getHolder().getSurface(), mOption.getRecorderOption());
  1. Освобождение ресурсов:
        mManager.release();
            mManager = null;
            mCamera = null;

4. Заключение

В целом, это основной процесс. Для получения более подробной информации посетите Github. В будущем будут добавлены дополнительные функции, такие как вспышка. Следите за обновлениями!

Адрес Github: https://github.com/MingYueChunQiu/RecorderManager.

Адрес кода облачной платформы: https://gitee.com/MingYueChunqiu/RecorderManager. Если эта информация была вам полезна, пожалуйста, поставьте звезду. Если у вас есть какие-либо предложения или замечания, мы будем рады их услышать.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/MingYueChunQiu-RecorderManager.git
git@api.gitlife.ru:oschina-mirror/MingYueChunQiu-RecorderManager.git
oschina-mirror
MingYueChunQiu-RecorderManager
MingYueChunQiu-RecorderManager
master